mirror of
https://github.com/nasa/openmct.git
synced 2025-05-14 14:33:26 +00:00
Activity state display for plans in Gantt and Time list views (#7370)
* Add activity states domain object and interceptor to auto create one * Add activity state inspector option * Only save status if we have a unique ids for activities * Include the id in the activity properties * Don't show activity state section in the inspector if multiple activities are selected * Display activity properties when an activity row is selected in the timelist * Use activity id as key if it is available * Ensure the correct option is selected for activity states * Add status label * Refactor activity selection. Display activity properties * Remove activity states plugin. Move the activity states interceptor to the plan plugin. * Change activity states interceptor parameters to options * Rename constants * Fix activity states test * Add e2e test for activity states feature. * Address review comments. Rename variables, documentation. * No shallow copy * Suppress lint warning for conditionals * Remove check for abort controller * Move classes to components * number primitive * Closes #7369 - WIP tweaks to simplify the Inspector view. * Ensure 'notStarted' is the default state for activities * Remove extra quotes * Closes #7369 - Mod to `s-selected` styling to allow selection visiblity on Time List rows. * Use generated key for vue * Fix e2e tests * Fix timelist test --------- Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com> Co-authored-by: John Hill <john.c.hill@nasa.gov>
This commit is contained in:
parent
60e1eeba8e
commit
dc5a3236b3
@ -6,7 +6,8 @@
|
|||||||
"end": 1660343797000,
|
"end": 1660343797000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Past event 2",
|
"name": "Past event 2",
|
||||||
@ -14,7 +15,8 @@
|
|||||||
"end": 1660429160000,
|
"end": 1660429160000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Past event 3",
|
"name": "Past event 3",
|
||||||
@ -22,7 +24,8 @@
|
|||||||
"end": 1660503981000,
|
"end": 1660503981000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Past event 4",
|
"name": "Past event 4",
|
||||||
@ -30,7 +33,8 @@
|
|||||||
"end": 1660624108000,
|
"end": 1660624108000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Past event 5",
|
"name": "Past event 5",
|
||||||
@ -38,7 +42,8 @@
|
|||||||
"end": 1660681529000,
|
"end": 1660681529000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 5
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
"end": 1660343797000,
|
"end": 1660343797000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Time until supper",
|
"name": "Time until supper",
|
||||||
@ -14,7 +15,8 @@
|
|||||||
"end": 1650420410000,
|
"end": 1650420410000,
|
||||||
"type": "Group 2",
|
"type": "Group 2",
|
||||||
"color": "blue",
|
"color": "blue",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Group 2": [
|
"Group 2": [
|
||||||
@ -24,7 +26,8 @@
|
|||||||
"end": 1650320102001,
|
"end": 1650320102001,
|
||||||
"type": "Group 2",
|
"type": "Group 2",
|
||||||
"color": "green",
|
"color": "green",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Time since last accident",
|
"name": "Time since last accident",
|
||||||
@ -32,7 +35,8 @@
|
|||||||
"end": 1650320102002,
|
"end": 1650320102002,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "yellow",
|
"color": "yellow",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 4
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import {
|
|||||||
assertPlanActivities,
|
assertPlanActivities,
|
||||||
assertPlanOrderedSwimLanes
|
assertPlanOrderedSwimLanes
|
||||||
} from '../../../helper/planningUtils.js';
|
} from '../../../helper/planningUtils.js';
|
||||||
import { test } from '../../../pluginFixtures.js';
|
import { expect, test } from '../../../pluginFixtures.js';
|
||||||
|
|
||||||
const testPlan1 = JSON.parse(
|
const testPlan1 = JSON.parse(
|
||||||
fs.readFileSync(
|
fs.readFileSync(
|
||||||
@ -63,4 +63,47 @@ test.describe('Plan', () => {
|
|||||||
});
|
});
|
||||||
await assertPlanOrderedSwimLanes(page, testPlanWithOrderedLanes, planWithSwimLanes.url);
|
await assertPlanOrderedSwimLanes(page, testPlanWithOrderedLanes, planWithSwimLanes.url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Allows setting the state of an activity when selected.', async ({ page }) => {
|
||||||
|
const groups = Object.keys(testPlan1);
|
||||||
|
const firstGroupKey = groups[0];
|
||||||
|
const firstGroupItems = testPlan1[firstGroupKey];
|
||||||
|
const firstActivity = firstGroupItems[0];
|
||||||
|
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
|
||||||
|
const startBound = firstActivity.start;
|
||||||
|
// Set the endBound to the end time of the current activity
|
||||||
|
let endBound = lastActivity.end;
|
||||||
|
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||||
|
if (endBound === startBound) {
|
||||||
|
// Prevent oddities with setting start and end bound equal
|
||||||
|
// via URL params
|
||||||
|
endBound += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to fixed time mode with all plan events within the bounds
|
||||||
|
await page.goto(
|
||||||
|
`${plan.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`
|
||||||
|
);
|
||||||
|
|
||||||
|
// select the first activity in the list
|
||||||
|
await page.getByText('Past event 1').click();
|
||||||
|
|
||||||
|
// Find the activity state section in the inspector
|
||||||
|
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||||
|
|
||||||
|
// Check that activity state dropdown selection shows the `set status` option by default
|
||||||
|
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
|
||||||
|
'Not started'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Change the selection of the activity status
|
||||||
|
await page.getByRole('combobox').selectOption({ label: 'Aborted' });
|
||||||
|
// select a different activity and back to the previous one
|
||||||
|
await page.getByText('Past event 2').click();
|
||||||
|
await page.getByText('Past event 1').click();
|
||||||
|
// Check that activity state dropdown selection shows the previously selected option by default
|
||||||
|
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
|
||||||
|
'Aborted'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -30,6 +30,11 @@ const examplePlanSmall3 = JSON.parse(
|
|||||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
|
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const examplePlanSmall1 = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
|
||||||
|
)
|
||||||
|
);
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const START_TIME_COLUMN = 0;
|
const START_TIME_COLUMN = 0;
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
@ -40,53 +45,8 @@ const ACTIVITY_COLUMN = 3;
|
|||||||
const HEADER_ROW = 0;
|
const HEADER_ROW = 0;
|
||||||
const NUM_COLUMNS = 4;
|
const NUM_COLUMNS = 4;
|
||||||
|
|
||||||
const testPlan = {
|
|
||||||
TEST_GROUP: [
|
|
||||||
{
|
|
||||||
name: 'Past event 1',
|
|
||||||
start: 1660320408000,
|
|
||||||
end: 1660343797000,
|
|
||||||
type: 'TEST-GROUP',
|
|
||||||
color: 'orange',
|
|
||||||
textColor: 'white'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Past event 2',
|
|
||||||
start: 1660406808000,
|
|
||||||
end: 1660429160000,
|
|
||||||
type: 'TEST-GROUP',
|
|
||||||
color: 'orange',
|
|
||||||
textColor: 'white'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Past event 3',
|
|
||||||
start: 1660493208000,
|
|
||||||
end: 1660503981000,
|
|
||||||
type: 'TEST-GROUP',
|
|
||||||
color: 'orange',
|
|
||||||
textColor: 'white'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Past event 4',
|
|
||||||
start: 1660579608000,
|
|
||||||
end: 1660624108000,
|
|
||||||
type: 'TEST-GROUP',
|
|
||||||
color: 'orange',
|
|
||||||
textColor: 'white'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Past event 5',
|
|
||||||
start: 1660666008000,
|
|
||||||
end: 1660681529000,
|
|
||||||
type: 'TEST-GROUP',
|
|
||||||
color: 'orange',
|
|
||||||
textColor: 'white'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
test.describe('Time List', () => {
|
test.describe('Time List', () => {
|
||||||
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
|
test("Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties", async ({
|
||||||
page
|
page
|
||||||
}) => {
|
}) => {
|
||||||
// Goto baseURL
|
// Goto baseURL
|
||||||
@ -103,12 +63,16 @@ test.describe('Time List', () => {
|
|||||||
await test.step('Create a Plan and add it to the timelist', async () => {
|
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||||
await createPlanFromJSON(page, {
|
await createPlanFromJSON(page, {
|
||||||
name: 'Test Plan',
|
name: 'Test Plan',
|
||||||
json: testPlan,
|
json: examplePlanSmall1,
|
||||||
parent: timelist.uuid
|
parent: timelist.uuid
|
||||||
});
|
});
|
||||||
|
const groups = Object.keys(examplePlanSmall1);
|
||||||
const startBound = testPlan.TEST_GROUP[0].start;
|
const firstGroupKey = groups[0];
|
||||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
const firstGroupItems = examplePlanSmall1[firstGroupKey];
|
||||||
|
const firstActivity = firstGroupItems[0];
|
||||||
|
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
|
||||||
|
const startBound = firstActivity.start;
|
||||||
|
const endBound = lastActivity.end;
|
||||||
|
|
||||||
// Switch to fixed time mode with all plan events within the bounds
|
// Switch to fixed time mode with all plan events within the bounds
|
||||||
await page.goto(
|
await page.goto(
|
||||||
@ -118,7 +82,7 @@ test.describe('Time List', () => {
|
|||||||
// Verify all events are displayed
|
// Verify all events are displayed
|
||||||
const eventCount = await page.getByRole('row').count();
|
const eventCount = await page.getByRole('row').count();
|
||||||
// subtracting one for the header
|
// subtracting one for the header
|
||||||
await expect(eventCount - 1).toEqual(testPlan.TEST_GROUP.length);
|
await expect(eventCount - 1).toEqual(firstGroupItems.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Does not show milliseconds in times', async () => {
|
await test.step('Does not show milliseconds in times', async () => {
|
||||||
@ -131,6 +95,17 @@ test.describe('Time List', () => {
|
|||||||
await expect(row.locator('.--end')).not.toContainText('.');
|
await expect(row.locator('.--end')).not.toContainText('.');
|
||||||
await expect(row.locator('.--duration')).not.toContainText('.');
|
await expect(row.locator('.--duration')).not.toContainText('.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await test.step('Shows activity properties when a row is selected', async () => {
|
||||||
|
await page.getByRole('row').nth(2).click();
|
||||||
|
|
||||||
|
// Find the activity state section in the inspector
|
||||||
|
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||||
|
// Check that activity state label is displayed in the inspector.
|
||||||
|
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
|
||||||
|
'Not started'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -99,7 +99,13 @@ export default class ObjectAPI {
|
|||||||
this.cache = {};
|
this.cache = {};
|
||||||
this.interceptorRegistry = new InterceptorRegistry();
|
this.interceptorRegistry = new InterceptorRegistry();
|
||||||
|
|
||||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
|
this.SYNCHRONIZED_OBJECT_TYPES = [
|
||||||
|
'notebook',
|
||||||
|
'restricted-notebook',
|
||||||
|
'plan',
|
||||||
|
'annotation',
|
||||||
|
'activity-states'
|
||||||
|
];
|
||||||
|
|
||||||
this.errors = {
|
this.errors = {
|
||||||
Conflict: ConflictError
|
Conflict: ConflictError
|
||||||
|
68
src/plugins/activityStates/activityStatesInterceptor.js
Normal file
68
src/plugins/activityStates/activityStatesInterceptor.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { ACTIVITY_STATES_KEY } from './createActivityStatesIdentifier.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ActivityStatesInterceptorOptions
|
||||||
|
* @property {import('../../api/objects/ObjectAPI').Identifier} identifier the {namespace, key} to use for the activity states object.
|
||||||
|
* @property {string} name The name of the activity states model.
|
||||||
|
* @property {number} priority the priority of the interceptor. By default, it is low.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an activity states object in the persistence store. This is used to save plan activity states.
|
||||||
|
* This will only get invoked when an attempt is made to save the state for an activity and no activity states object exists in the store.
|
||||||
|
* @param {import('../../../openmct').OpenMCT} openmct
|
||||||
|
* @param {ActivityStatesInterceptorOptions} options
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
const ACTIVITY_STATES_TYPE = 'activity-states';
|
||||||
|
|
||||||
|
function activityStatesInterceptor(openmct, options) {
|
||||||
|
const { identifier, name, priority = openmct.priority.LOW } = options;
|
||||||
|
const activityStatesModel = {
|
||||||
|
identifier,
|
||||||
|
name,
|
||||||
|
type: ACTIVITY_STATES_TYPE,
|
||||||
|
activities: {},
|
||||||
|
location: null
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
appliesTo: (identifierObject) => {
|
||||||
|
return identifierObject.key === ACTIVITY_STATES_KEY;
|
||||||
|
},
|
||||||
|
invoke: (identifierObject, object) => {
|
||||||
|
if (!object || openmct.objects.isMissing(object)) {
|
||||||
|
openmct.objects.save(activityStatesModel);
|
||||||
|
|
||||||
|
return activityStatesModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return object;
|
||||||
|
},
|
||||||
|
priority
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default activityStatesInterceptor;
|
30
src/plugins/activityStates/createActivityStatesIdentifier.js
Normal file
30
src/plugins/activityStates/createActivityStatesIdentifier.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
export const ACTIVITY_STATES_KEY = 'activity-states';
|
||||||
|
|
||||||
|
export function createActivityStatesIdentifier(namespace = '') {
|
||||||
|
return {
|
||||||
|
key: ACTIVITY_STATES_KEY,
|
||||||
|
namespace
|
||||||
|
};
|
||||||
|
}
|
89
src/plugins/activityStates/pluginSpec.js
Normal file
89
src/plugins/activityStates/pluginSpec.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ACTIVITY_STATES_KEY,
|
||||||
|
createActivityStatesIdentifier
|
||||||
|
} from './createActivityStatesIdentifier.js';
|
||||||
|
|
||||||
|
const MISSING_NAME = `Missing: ${ACTIVITY_STATES_KEY}`;
|
||||||
|
const DEFAULT_NAME = 'Activity States';
|
||||||
|
const activityStatesIdentifier = createActivityStatesIdentifier();
|
||||||
|
|
||||||
|
describe('the plugin', () => {
|
||||||
|
let openmct;
|
||||||
|
let missingObj = {
|
||||||
|
identifier: activityStatesIdentifier,
|
||||||
|
type: 'unknown',
|
||||||
|
name: MISSING_NAME
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('with no arguments passed in', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
openmct.install(openmct.plugins.PlanLayout());
|
||||||
|
|
||||||
|
openmct.on('start', done);
|
||||||
|
openmct.startHeadless();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
return resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when installed, adds "Activity States"', async () => {
|
||||||
|
const activityStatesObject = await openmct.objects.get(activityStatesIdentifier);
|
||||||
|
expect(activityStatesObject.name).toBe(DEFAULT_NAME);
|
||||||
|
expect(activityStatesObject).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('adds an interceptor that returns a "Activity States" model for', () => {
|
||||||
|
let activityStatesObject;
|
||||||
|
let mockNotFoundProvider;
|
||||||
|
let activeProvider;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockNotFoundProvider = {
|
||||||
|
get: () => Promise.reject(new Error('Not found')),
|
||||||
|
create: () => Promise.resolve(missingObj),
|
||||||
|
update: () => Promise.resolve(missingObj)
|
||||||
|
};
|
||||||
|
|
||||||
|
activeProvider = mockNotFoundProvider;
|
||||||
|
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
|
||||||
|
activityStatesObject = await openmct.objects.get(activityStatesIdentifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('missing objects', () => {
|
||||||
|
let idsMatch = openmct.objects.areIdsEqual(
|
||||||
|
activityStatesObject.identifier,
|
||||||
|
activityStatesIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(activityStatesObject).toBeDefined();
|
||||||
|
expect(idsMatch).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -135,6 +135,7 @@ export default {
|
|||||||
default: 22
|
default: 22
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['activity-selected'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lineHeight: 10
|
lineHeight: 10
|
||||||
@ -142,30 +143,11 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setSelectionForActivity(activity, event) {
|
setSelectionForActivity(activity, event) {
|
||||||
const element = event.currentTarget;
|
|
||||||
const multiSelect = event.metaKey;
|
|
||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
this.$emit('activity-selected', {
|
||||||
this.openmct.selection.select(
|
event,
|
||||||
[
|
selection: activity.selection
|
||||||
{
|
});
|
||||||
element: element,
|
|
||||||
context: {
|
|
||||||
type: 'activity',
|
|
||||||
activity: activity
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: this.openmct.layout.$refs.browseObject.$el,
|
|
||||||
context: {
|
|
||||||
item: this.domainObject,
|
|
||||||
supportsMultiSelect: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
multiSelect
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
:width="group.width"
|
:width="group.width"
|
||||||
:is-nested="options.isChildObject"
|
:is-nested="options.isChildObject"
|
||||||
:status="status"
|
:status="status"
|
||||||
|
@activity-selected="selectActivity"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -134,7 +135,7 @@ export default {
|
|||||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||||
this.clipActivityNames = this.configuration.clipActivityNames;
|
this.clipActivityNames = this.configuration.clipActivityNames;
|
||||||
if (this.domainObject.type === 'plan') {
|
if (this.domainObject.type === 'plan') {
|
||||||
this.planData = getValidatedData(this.domainObject);
|
this.setPlanData(this.domainObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@ -177,6 +178,9 @@ export default {
|
|||||||
this.planViewConfiguration.destroy();
|
this.planViewConfiguration.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setPlanData(domainObject) {
|
||||||
|
this.planData = getValidatedData(domainObject);
|
||||||
|
},
|
||||||
activityNameFitsRect(activityName, rectWidth) {
|
activityNameFitsRect(activityName, rectWidth) {
|
||||||
return this.getTextWidth(activityName) + TEXT_LEFT_PADDING < rectWidth;
|
return this.getTextWidth(activityName) + TEXT_LEFT_PADDING < rectWidth;
|
||||||
},
|
},
|
||||||
@ -215,9 +219,7 @@ export default {
|
|||||||
callback: () => {
|
callback: () => {
|
||||||
this.removeFromComposition(this.planObject);
|
this.removeFromComposition(this.planObject);
|
||||||
this.planObject = domainObject;
|
this.planObject = domainObject;
|
||||||
this.planData = getValidatedData(domainObject);
|
this.handleSelectFileChange();
|
||||||
this.setStatus(this.openmct.status.get(domainObject.identifier));
|
|
||||||
this.setScaleAndGenerateActivities();
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -237,9 +239,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.planObject = domainObject;
|
this.planObject = domainObject;
|
||||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||||
this.planData = getValidatedData(domainObject);
|
this.handleSelectFileChange(domainObject);
|
||||||
this.setStatus(this.openmct.status.get(domainObject.identifier));
|
|
||||||
this.setScaleAndGenerateActivities();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleConfigurationChange(newConfiguration) {
|
handleConfigurationChange(newConfiguration) {
|
||||||
@ -259,8 +259,10 @@ export default {
|
|||||||
|
|
||||||
this.setScaleAndGenerateActivities();
|
this.setScaleAndGenerateActivities();
|
||||||
},
|
},
|
||||||
handleSelectFileChange() {
|
handleSelectFileChange(domainObject) {
|
||||||
this.planData = getValidatedData(this.domainObject);
|
const planDomainObject = domainObject || this.domainObject;
|
||||||
|
this.setPlanData(planDomainObject);
|
||||||
|
this.setStatus(this.openmct.status.get(planDomainObject.identifier));
|
||||||
this.setScaleAndGenerateActivities();
|
this.setScaleAndGenerateActivities();
|
||||||
},
|
},
|
||||||
removeFromComposition(domainObject) {
|
removeFromComposition(domainObject) {
|
||||||
@ -434,7 +436,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rawActivities.forEach((rawActivity) => {
|
rawActivities.forEach((rawActivity, index) => {
|
||||||
if (!this.isActivityInBounds(rawActivity)) {
|
if (!this.isActivityInBounds(rawActivity)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -481,13 +483,10 @@ export default {
|
|||||||
const activity = {
|
const activity = {
|
||||||
color: color,
|
color: color,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
name: rawActivity.name,
|
|
||||||
exceeds: {
|
exceeds: {
|
||||||
start: this.xScale(this.viewBounds.start) > this.xScale(rawActivity.start),
|
start: this.xScale(this.viewBounds.start) > this.xScale(rawActivity.start),
|
||||||
end: this.xScale(this.viewBounds.end) < this.xScale(rawActivity.end)
|
end: this.xScale(this.viewBounds.end) < this.xScale(rawActivity.end)
|
||||||
},
|
},
|
||||||
start: rawActivity.start,
|
|
||||||
end: rawActivity.end,
|
|
||||||
row: currentRow,
|
row: currentRow,
|
||||||
textLines: textLines,
|
textLines: textLines,
|
||||||
textStart: textStart,
|
textStart: textStart,
|
||||||
@ -496,7 +495,11 @@ export default {
|
|||||||
rectStart: rectX1,
|
rectStart: rectX1,
|
||||||
rectEnd: showTextInsideRect ? rectX2 : textStart + textWidth,
|
rectEnd: showTextInsideRect ? rectX2 : textStart + textWidth,
|
||||||
rectWidth: rectWidth,
|
rectWidth: rectWidth,
|
||||||
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow)
|
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow),
|
||||||
|
selection: {
|
||||||
|
groupName,
|
||||||
|
index
|
||||||
|
}
|
||||||
};
|
};
|
||||||
activitiesByRow[currentRow].push(activity);
|
activitiesByRow[currentRow].push(activity);
|
||||||
});
|
});
|
||||||
@ -573,6 +576,31 @@ export default {
|
|||||||
const activityName = activity.name.toLowerCase().replace(/ /g, '-');
|
const activityName = activity.name.toLowerCase().replace(/ /g, '-');
|
||||||
|
|
||||||
return `${groupName}-${activityName}-${activity.start}-${activity.end}-${row}`;
|
return `${groupName}-${activityName}-${activity.start}-${activity.end}-${row}`;
|
||||||
|
},
|
||||||
|
selectActivity({ event, selection }) {
|
||||||
|
const element = event.currentTarget;
|
||||||
|
const multiSelect = event.metaKey;
|
||||||
|
const { groupName, index } = selection;
|
||||||
|
const rawActivity = this.planData[groupName][index];
|
||||||
|
this.openmct.selection.select(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
element: element,
|
||||||
|
context: {
|
||||||
|
type: 'activity',
|
||||||
|
activity: rawActivity
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: this.openmct.layout.$refs.browseObject.$el,
|
||||||
|
context: {
|
||||||
|
item: this.domainObject,
|
||||||
|
supportsMultiSelect: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
multiSelect
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -20,21 +20,35 @@
|
|||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="c-inspector__properties c-inspect-properties">
|
<plan-activity-time-view
|
||||||
<plan-activity-view
|
v-for="activity in activities"
|
||||||
v-for="activity in activities"
|
:key="activity.key"
|
||||||
:key="activity.id"
|
:activity="activity"
|
||||||
:activity="activity"
|
:heading="heading"
|
||||||
:heading="heading"
|
/>
|
||||||
/>
|
<plan-activity-properties-view
|
||||||
</div>
|
v-for="activity in activities"
|
||||||
|
:key="activity.key"
|
||||||
|
heading="Properties"
|
||||||
|
:activity="activity"
|
||||||
|
/>
|
||||||
|
<plan-activity-status-view
|
||||||
|
v-if="canPersistState"
|
||||||
|
:key="activities[0].key"
|
||||||
|
:activity="activities[0]"
|
||||||
|
:execution-state="activityExecutionState"
|
||||||
|
heading="Activity Status"
|
||||||
|
@update-activity-state="persistActivityState"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getPreciseDuration } from 'utils/duration';
|
import { getPreciseDuration } from 'utils/duration';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import PlanActivityView from './PlanActivityView.vue';
|
import { getDisplayProperties } from '../../util.js';
|
||||||
|
import PlanActivityPropertiesView from './PlanActivityPropertiesView.vue';
|
||||||
|
import PlanActivityStatusView from './PlanActivityStatusView.vue';
|
||||||
|
import PlanActivityTimeView from './PlanActivityTimeView.vue';
|
||||||
|
|
||||||
const propertyLabels = {
|
const propertyLabels = {
|
||||||
start: 'Start DateTime',
|
start: 'Start DateTime',
|
||||||
@ -44,23 +58,34 @@ const propertyLabels = {
|
|||||||
latestEnd: 'Latest End',
|
latestEnd: 'Latest End',
|
||||||
gap: 'Gap',
|
gap: 'Gap',
|
||||||
overlap: 'Overlap',
|
overlap: 'Overlap',
|
||||||
totalTime: 'Total Time'
|
totalTime: 'Total Time',
|
||||||
|
description: 'Description'
|
||||||
};
|
};
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PlanActivityView
|
PlanActivityTimeView,
|
||||||
|
PlanActivityPropertiesView,
|
||||||
|
PlanActivityStatusView
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'selection'],
|
inject: ['openmct', 'selection'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
activities: [],
|
activities: [],
|
||||||
|
selectedActivities: [],
|
||||||
|
activityExecutionState: undefined,
|
||||||
heading: ''
|
heading: ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
canPersistState() {
|
||||||
|
return this.selectedActivities.length === 1 && this.activities?.[0]?.id;
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.setFormatters();
|
this.setFormatters();
|
||||||
this.getPlanData(this.selection);
|
this.getPlanData(this.selection);
|
||||||
|
this.getActivityStates();
|
||||||
this.getActivities();
|
this.getActivities();
|
||||||
this.openmct.selection.on('change', this.updateSelection);
|
this.openmct.selection.on('change', this.updateSelection);
|
||||||
this.openmct.time.on('timeSystem', this.setFormatters);
|
this.openmct.time.on('timeSystem', this.setFormatters);
|
||||||
@ -68,8 +93,28 @@ export default {
|
|||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.openmct.selection.off('change', this.updateSelection);
|
this.openmct.selection.off('change', this.updateSelection);
|
||||||
this.openmct.time.off('timeSystem', this.setFormatters);
|
this.openmct.time.off('timeSystem', this.setFormatters);
|
||||||
|
if (this.stopObservingActivityStatesObject) {
|
||||||
|
this.stopObservingActivityStatesObject();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async getActivityStates() {
|
||||||
|
this.activityStatesObject = await this.openmct.objects.get('activity-states');
|
||||||
|
this.setActivityStates(this.activityStatesObject);
|
||||||
|
this.stopObservingActivityStatesObject = this.openmct.objects.observe(
|
||||||
|
this.activityStatesObject,
|
||||||
|
'*',
|
||||||
|
this.setActivityStates
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setActivityStates(newActivitiesStateObject) {
|
||||||
|
if (this.activities.length) {
|
||||||
|
const id = this.activities[0].id;
|
||||||
|
this.activityExecutionState = newActivitiesStateObject.activities[id];
|
||||||
|
} else {
|
||||||
|
this.activityExecutionState = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
setFormatters() {
|
setFormatters() {
|
||||||
let timeSystem = this.openmct.time.timeSystem();
|
let timeSystem = this.openmct.time.timeSystem();
|
||||||
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
|
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
|
||||||
@ -86,6 +131,7 @@ export default {
|
|||||||
if (selectionItem[0].context.type === 'activity') {
|
if (selectionItem[0].context.type === 'activity') {
|
||||||
const activity = selectionItem[0].context.activity;
|
const activity = selectionItem[0].context.activity;
|
||||||
if (activity) {
|
if (activity) {
|
||||||
|
activity.key = activity.id ?? activity.name;
|
||||||
this.selectedActivities.push(activity);
|
this.selectedActivities.push(activity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,20 +150,37 @@ export default {
|
|||||||
this.activities.splice(0);
|
this.activities.splice(0);
|
||||||
this.selectedActivities.forEach((selectedActivity, index) => {
|
this.selectedActivities.forEach((selectedActivity, index) => {
|
||||||
const activity = {
|
const activity = {
|
||||||
id: uuid(),
|
id: selectedActivity.id,
|
||||||
start: {
|
key: selectedActivity.key,
|
||||||
label: propertyLabels.start,
|
timeProperties: {
|
||||||
value: this.formatTime(selectedActivity.start)
|
start: {
|
||||||
},
|
label: propertyLabels.start,
|
||||||
end: {
|
value: this.formatTime(selectedActivity.start)
|
||||||
label: propertyLabels.end,
|
},
|
||||||
value: this.formatTime(selectedActivity.end)
|
end: {
|
||||||
},
|
label: propertyLabels.end,
|
||||||
duration: {
|
value: this.formatTime(selectedActivity.end)
|
||||||
label: propertyLabels.duration,
|
},
|
||||||
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
|
duration: {
|
||||||
|
label: propertyLabels.duration,
|
||||||
|
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
activity.metadata = {};
|
||||||
|
if (selectedActivity.description) {
|
||||||
|
activity.metadata.description = {
|
||||||
|
label: propertyLabels.description,
|
||||||
|
value: selectedActivity.description
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayProperties = getDisplayProperties(selectedActivity);
|
||||||
|
activity.metadata = {
|
||||||
|
...activity.metadata,
|
||||||
|
...displayProperties
|
||||||
|
};
|
||||||
|
|
||||||
this.activities[index] = activity;
|
this.activities[index] = activity;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -141,6 +204,8 @@ export default {
|
|||||||
let latestEnd;
|
let latestEnd;
|
||||||
let gap;
|
let gap;
|
||||||
let overlap;
|
let overlap;
|
||||||
|
let id;
|
||||||
|
let key;
|
||||||
|
|
||||||
//Sort by start time
|
//Sort by start time
|
||||||
let selectedActivities = this.selectedActivities.sort(this.sortFn);
|
let selectedActivities = this.selectedActivities.sort(this.sortFn);
|
||||||
@ -159,6 +224,8 @@ export default {
|
|||||||
earliestStart = Math.min(earliestStart, selectedActivity.start);
|
earliestStart = Math.min(earliestStart, selectedActivity.start);
|
||||||
latestEnd = Math.max(latestEnd, selectedActivity.end);
|
latestEnd = Math.max(latestEnd, selectedActivity.end);
|
||||||
} else {
|
} else {
|
||||||
|
id = selectedActivity.id;
|
||||||
|
key = selectedActivity.id ?? selectedActivity.name;
|
||||||
earliestStart = selectedActivity.start;
|
earliestStart = selectedActivity.start;
|
||||||
latestEnd = selectedActivity.end;
|
latestEnd = selectedActivity.end;
|
||||||
}
|
}
|
||||||
@ -166,30 +233,33 @@ export default {
|
|||||||
let totalTime = latestEnd - earliestStart;
|
let totalTime = latestEnd - earliestStart;
|
||||||
|
|
||||||
const activity = {
|
const activity = {
|
||||||
id: uuid(),
|
id,
|
||||||
earliestStart: {
|
key,
|
||||||
label: propertyLabels.earliestStart,
|
timeProperties: {
|
||||||
value: this.formatTime(earliestStart)
|
earliestStart: {
|
||||||
},
|
label: propertyLabels.earliestStart,
|
||||||
latestEnd: {
|
value: this.formatTime(earliestStart)
|
||||||
label: propertyLabels.latestEnd,
|
},
|
||||||
value: this.formatTime(latestEnd)
|
latestEnd: {
|
||||||
|
label: propertyLabels.latestEnd,
|
||||||
|
value: this.formatTime(latestEnd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (gap) {
|
if (gap) {
|
||||||
activity.gap = {
|
activity.timeProperties.gap = {
|
||||||
label: propertyLabels.gap,
|
label: propertyLabels.gap,
|
||||||
value: this.formatDuration(gap)
|
value: this.formatDuration(gap)
|
||||||
};
|
};
|
||||||
} else if (overlap) {
|
} else if (overlap) {
|
||||||
activity.overlap = {
|
activity.timeProperties.overlap = {
|
||||||
label: propertyLabels.overlap,
|
label: propertyLabels.overlap,
|
||||||
value: this.formatDuration(overlap)
|
value: this.formatDuration(overlap)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.totalTime = {
|
activity.timeProperties.totalTime = {
|
||||||
label: propertyLabels.totalTime,
|
label: propertyLabels.totalTime,
|
||||||
value: this.formatDuration(totalTime)
|
value: this.formatDuration(totalTime)
|
||||||
};
|
};
|
||||||
@ -201,6 +271,11 @@ export default {
|
|||||||
},
|
},
|
||||||
formatTime(time) {
|
formatTime(time) {
|
||||||
return this.timeFormatter.format(time);
|
return this.timeFormatter.format(time);
|
||||||
|
},
|
||||||
|
persistActivityState(data) {
|
||||||
|
const { key, executionState } = data;
|
||||||
|
const activitiesPath = `activities.${key}`;
|
||||||
|
this.openmct.objects.mutate(this.activityStatesObject, activitiesPath, executionState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="c-inspector__properties c-inspect-properties">
|
||||||
|
<div v-if="properties.length" class="u-contents">
|
||||||
|
<div class="c-inspect-properties__header">{{ heading }}</div>
|
||||||
|
<ul v-for="property in properties" :key="property.id" class="c-inspect-properties__section">
|
||||||
|
<activity-property :label="property.label" :value="property.value" />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ActivityProperty from './ActivityProperty.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ActivityProperty
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
activity: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
properties: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setProperties();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setProperties() {
|
||||||
|
if (!this.activity.metadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(this.activity.metadata).forEach((key) => {
|
||||||
|
if (this.activity.metadata[key].label) {
|
||||||
|
const label = this.activity.metadata[key].label;
|
||||||
|
const value = String(this.activity.metadata[key].value);
|
||||||
|
const id = this.activity.id;
|
||||||
|
|
||||||
|
this.properties[this.properties.length] = {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
127
src/plugins/plan/inspector/components/PlanActivityStatusView.vue
Normal file
127
src/plugins/plan/inspector/components/PlanActivityStatusView.vue
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="c-inspector__properties c-inspect-properties">
|
||||||
|
<div class="u-contents">
|
||||||
|
<div class="c-inspect-properties__header">{{ heading }}</div>
|
||||||
|
<div class="c-inspect-properties__row">
|
||||||
|
<div class="c-inspect-properties__label" title="Set Status">Set Status</div>
|
||||||
|
<div class="c-inspect-properties__value" aria-label="Activity Status Label">
|
||||||
|
<select
|
||||||
|
v-model="currentStatusKey"
|
||||||
|
name="setActivityStatus"
|
||||||
|
aria-label="Activity Status"
|
||||||
|
@change="changeActivityStatus"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="status in activityStates"
|
||||||
|
:key="status.key"
|
||||||
|
:value="status.key"
|
||||||
|
:aria-selected="currentStatusKey === status.key"
|
||||||
|
>
|
||||||
|
{{ status.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const activityStates = [
|
||||||
|
{
|
||||||
|
key: 'notStarted',
|
||||||
|
label: 'Not started'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'in-progress',
|
||||||
|
label: 'In progress'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'completed',
|
||||||
|
label: 'Completed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'aborted',
|
||||||
|
label: 'Aborted'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'cancelled',
|
||||||
|
label: 'Cancelled'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
activity: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
executionState: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['updateActivityState'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activityStates: activityStates,
|
||||||
|
currentStatusKey: activityStates[0].key
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
executionState() {
|
||||||
|
this.setActivityStatus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setActivityStatus();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setActivityStatus() {
|
||||||
|
let statusKeyIndex = activityStates.findIndex((state) => state.key === this.executionState);
|
||||||
|
if (statusKeyIndex < 0) {
|
||||||
|
statusKeyIndex = 0;
|
||||||
|
}
|
||||||
|
this.currentStatusKey = this.activityStates[statusKeyIndex].key;
|
||||||
|
},
|
||||||
|
changeActivityStatus() {
|
||||||
|
if (this.currentStatusKey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.activity.executionState = this.currentStatusKey;
|
||||||
|
this.$emit('updateActivityState', {
|
||||||
|
key: this.activity.id,
|
||||||
|
executionState: this.currentStatusKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -21,23 +21,23 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="timeProperties.length" class="u-contents">
|
<div class="c-inspector__properties c-inspect-properties">
|
||||||
<div class="c-inspect-properties__header">
|
<div v-if="timeProperties.length" class="u-contents">
|
||||||
{{ heading }}
|
<div class="c-inspect-properties__header">
|
||||||
|
{{ heading }}
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
v-for="timeProperty in timeProperties"
|
||||||
|
:key="timeProperty.id"
|
||||||
|
class="c-inspect-properties__section"
|
||||||
|
>
|
||||||
|
<activity-property :label="timeProperty.label" :value="timeProperty.value" />
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
|
||||||
v-for="timeProperty in timeProperties"
|
|
||||||
:key="timeProperty.id"
|
|
||||||
class="c-inspect-properties__section"
|
|
||||||
>
|
|
||||||
<activity-property :label="timeProperty.label" :value="timeProperty.value" />
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import ActivityProperty from './ActivityProperty.vue';
|
import ActivityProperty from './ActivityProperty.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -64,13 +64,14 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setProperties() {
|
setProperties() {
|
||||||
Object.keys(this.activity).forEach((key) => {
|
Object.keys(this.activity.timeProperties).forEach((key) => {
|
||||||
if (this.activity[key].label) {
|
if (this.activity.timeProperties[key].label) {
|
||||||
const label = this.activity[key].label;
|
const label = this.activity.timeProperties[key].label;
|
||||||
const value = String(this.activity[key].value);
|
const value = String(this.activity.timeProperties[key].value);
|
||||||
|
const id = this.activity.id;
|
||||||
|
|
||||||
this.timeProperties[this.timeProperties.length] = {
|
this.timeProperties[this.timeProperties.length] = {
|
||||||
id: uuid(),
|
id,
|
||||||
label,
|
label,
|
||||||
value
|
value
|
||||||
};
|
};
|
@ -20,12 +20,28 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import activityStatesInterceptor from '../activityStates/activityStatesInterceptor.js';
|
||||||
|
import { createActivityStatesIdentifier } from '../activityStates/createActivityStatesIdentifier.js';
|
||||||
import ganttChartCompositionPolicy from './GanttChartCompositionPolicy.js';
|
import ganttChartCompositionPolicy from './GanttChartCompositionPolicy.js';
|
||||||
import ActivityInspectorViewProvider from './inspector/ActivityInspectorViewProvider.js';
|
import ActivityInspectorViewProvider from './inspector/ActivityInspectorViewProvider.js';
|
||||||
import GanttChartInspectorViewProvider from './inspector/GanttChartInspectorViewProvider.js';
|
import GanttChartInspectorViewProvider from './inspector/GanttChartInspectorViewProvider.js';
|
||||||
import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration.js';
|
import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration.js';
|
||||||
import PlanViewProvider from './PlanViewProvider.js';
|
import PlanViewProvider from './PlanViewProvider.js';
|
||||||
|
|
||||||
|
const ACTIVITY_STATES_DEFAULT_NAME = 'Activity States';
|
||||||
|
/**
|
||||||
|
* @typedef {object} PlanOptions
|
||||||
|
* @property {boolean} creatable true/false to allow creation of a plan via the Create menu.
|
||||||
|
* @property {string} name The name of the activity states model.
|
||||||
|
* @property {string} namespace the namespace to use for the activity states object.
|
||||||
|
* @property {Number} priority the priority of the interceptor. By default, it is low.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {PlanOptions} options
|
||||||
|
* @returns {*} (any)
|
||||||
|
*/
|
||||||
export default function (options = {}) {
|
export default function (options = {}) {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
openmct.types.addType('plan', {
|
openmct.types.addType('plan', {
|
||||||
@ -70,5 +86,13 @@ export default function (options = {}) {
|
|||||||
openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct));
|
openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct));
|
||||||
openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct));
|
openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct));
|
||||||
openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct));
|
openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct));
|
||||||
|
|
||||||
|
//add activity states get interceptor
|
||||||
|
const { name = ACTIVITY_STATES_DEFAULT_NAME, namespace = '', priority } = options;
|
||||||
|
const identifier = createActivityStatesIdentifier(namespace);
|
||||||
|
|
||||||
|
openmct.objects.addGetInterceptor(
|
||||||
|
activityStatesInterceptor(openmct, { identifier, name, priority })
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,18 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SourceMap allows mapping specific implementations of plan domain objects to those expected by Open MCT.
|
||||||
|
* @typedef {object} SourceMapOption
|
||||||
|
* @property {string} orderedGroups the property of the plan that lists groups/swim lanes specifying what order they will be displayed in Open MCT.
|
||||||
|
* @property {string} activities the property of the plan that has the list of activities to be displayed.
|
||||||
|
* @property {string} groupId the property of the activity that maps to the group/swim lane it should be displayed in.
|
||||||
|
* @property {string} start The start time property of the activity
|
||||||
|
* @property {string} end The end time property of the activity
|
||||||
|
* @property {string} id The unique id of the activity. This is required to allow setting activity states
|
||||||
|
* @property {object} displayProperties a list of key: value pairs that specifies which properties of the activity should be displayed when it is selected. Ex. {'location': 'Location', 'metadata.length_in_meters', 'Length (meters)'}
|
||||||
|
*/
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
export function getValidatedData(domainObject) {
|
export function getValidatedData(domainObject) {
|
||||||
const sourceMap = domainObject.sourceMap;
|
const sourceMap = domainObject.sourceMap;
|
||||||
@ -56,6 +68,14 @@ export function getValidatedData(domainObject) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sourceMap.id) {
|
||||||
|
groupActivity.id = activity[sourceMap.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceMap.displayProperties) {
|
||||||
|
groupActivity.displayProperties = sourceMap.displayProperties;
|
||||||
|
}
|
||||||
|
|
||||||
if (!mappedJson[groupIdKey]) {
|
if (!mappedJson[groupIdKey]) {
|
||||||
mappedJson[groupIdKey] = [];
|
mappedJson[groupIdKey] = [];
|
||||||
}
|
}
|
||||||
@ -110,6 +130,26 @@ export function getValidatedGroups(domainObject, planData) {
|
|||||||
return orderedGroupNames;
|
return orderedGroupNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDisplayProperties(activity) {
|
||||||
|
let displayProperties = {};
|
||||||
|
function extractProperties(properties, useKeyAsLabel = false) {
|
||||||
|
Object.keys(properties).forEach((key) => {
|
||||||
|
const label = useKeyAsLabel ? key : properties[key];
|
||||||
|
const value = _.get(activity, key);
|
||||||
|
if (value) {
|
||||||
|
displayProperties[key] = { label, value };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activity?.displayProperties) {
|
||||||
|
extractProperties(activity.displayProperties);
|
||||||
|
} else if (activity?.properties) {
|
||||||
|
extractProperties(activity.properties, true);
|
||||||
|
}
|
||||||
|
return displayProperties;
|
||||||
|
}
|
||||||
|
|
||||||
export function getFilteredValues(activity) {
|
export function getFilteredValues(activity) {
|
||||||
let values = [];
|
let values = [];
|
||||||
if (Array.isArray(activity.filterMetadataValues)) {
|
if (Array.isArray(activity.filterMetadataValues)) {
|
||||||
|
@ -123,15 +123,6 @@
|
|||||||
.is-editing .l-layout__frame & {
|
.is-editing .l-layout__frame & {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-selected {
|
|
||||||
background-color: $colorSelectedBg !important;
|
|
||||||
color: $colorSelectedFg !important;
|
|
||||||
td {
|
|
||||||
background: none !important;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
@ -186,6 +177,18 @@ td {
|
|||||||
@include isLimit();
|
@include isLimit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-table tr {
|
||||||
|
&[s-selected],
|
||||||
|
&.is-selected {
|
||||||
|
background-color: $colorSelectedBg !important;
|
||||||
|
color: $colorSelectedFg !important;
|
||||||
|
td {
|
||||||
|
background: none !important;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/******************************* SPECIFIC CASE WRAPPERS */
|
/******************************* SPECIFIC CASE WRAPPERS */
|
||||||
.is-editing {
|
.is-editing {
|
||||||
.c-telemetry-table__headers__labels {
|
.c-telemetry-table__headers__labels {
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
:header-items="headerItems"
|
:header-items="headerItems"
|
||||||
:default-sort="defaultSort"
|
:default-sort="defaultSort"
|
||||||
class="sticky"
|
class="sticky"
|
||||||
|
@item-selection-changed="setSelectionForActivity"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -541,6 +542,29 @@ export default {
|
|||||||
setEditState(isEditing) {
|
setEditState(isEditing) {
|
||||||
this.isEditing = isEditing;
|
this.isEditing = isEditing;
|
||||||
this.setViewFromConfig(this.domainObject.configuration);
|
this.setViewFromConfig(this.domainObject.configuration);
|
||||||
|
},
|
||||||
|
setSelectionForActivity(activity, element) {
|
||||||
|
const multiSelect = false;
|
||||||
|
|
||||||
|
this.openmct.selection.select(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
element: element,
|
||||||
|
context: {
|
||||||
|
type: 'activity',
|
||||||
|
activity: activity
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: this.openmct.layout.$refs.browseObject.$el,
|
||||||
|
context: {
|
||||||
|
item: this.domainObject,
|
||||||
|
supportsMultiSelect: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
multiSelect
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -28,7 +28,7 @@ import TimelistPropertiesView from './TimelistPropertiesView.vue';
|
|||||||
export default function TimeListInspectorViewProvider(openmct) {
|
export default function TimeListInspectorViewProvider(openmct) {
|
||||||
return {
|
return {
|
||||||
key: 'timelist-inspector',
|
key: 'timelist-inspector',
|
||||||
name: 'Timelist Inspector View',
|
name: 'Config',
|
||||||
canView: function (selection) {
|
canView: function (selection) {
|
||||||
if (selection.length === 0 || selection[0].length === 0) {
|
if (selection.length === 0 || selection[0].length === 0) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
:key="item.key"
|
:key="item.key"
|
||||||
:item="item"
|
:item="item"
|
||||||
:item-properties="itemProperties"
|
:item-properties="itemProperties"
|
||||||
|
@click.stop="itemSelected(item, $event)"
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -86,6 +87,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['item-selection-changed'],
|
||||||
data() {
|
data() {
|
||||||
let sortBy = this.defaultSort.property;
|
let sortBy = this.defaultSort.property;
|
||||||
let ascending = this.defaultSort.defaultDirection;
|
let ascending = this.defaultSort.defaultDirection;
|
||||||
@ -156,6 +158,9 @@ export default {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
itemSelected(item, event) {
|
||||||
|
this.$emit('item-selection-changed', item, event.currentTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user