mirror of
https://github.com/nasa/openmct.git
synced 2024-12-19 21:27:52 +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,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"name": "Past event 2",
|
||||
@ -14,7 +15,8 @@
|
||||
"end": 1660429160000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"name": "Past event 3",
|
||||
@ -22,7 +24,8 @@
|
||||
"end": 1660503981000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"name": "Past event 4",
|
||||
@ -30,7 +33,8 @@
|
||||
"end": 1660624108000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 4
|
||||
},
|
||||
{
|
||||
"name": "Past event 5",
|
||||
@ -38,7 +42,8 @@
|
||||
"end": 1660681529000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -6,7 +6,8 @@
|
||||
"end": 1660343797000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"name": "Time until supper",
|
||||
@ -14,7 +15,8 @@
|
||||
"end": 1650420410000,
|
||||
"type": "Group 2",
|
||||
"color": "blue",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 2
|
||||
}
|
||||
],
|
||||
"Group 2": [
|
||||
@ -24,7 +26,8 @@
|
||||
"end": 1650320102001,
|
||||
"type": "Group 2",
|
||||
"color": "green",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"name": "Time since last accident",
|
||||
@ -32,7 +35,8 @@
|
||||
"end": 1650320102002,
|
||||
"type": "Group 1",
|
||||
"color": "yellow",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import {
|
||||
assertPlanActivities,
|
||||
assertPlanOrderedSwimLanes
|
||||
} from '../../../helper/planningUtils.js';
|
||||
import { test } from '../../../pluginFixtures.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const testPlan1 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
@ -63,4 +63,47 @@ test.describe('Plan', () => {
|
||||
});
|
||||
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)
|
||||
)
|
||||
);
|
||||
const examplePlanSmall1 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
|
||||
)
|
||||
);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const START_TIME_COLUMN = 0;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@ -40,53 +45,8 @@ const ACTIVITY_COLUMN = 3;
|
||||
const HEADER_ROW = 0;
|
||||
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('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
|
||||
}) => {
|
||||
// Goto baseURL
|
||||
@ -103,12 +63,16 @@ test.describe('Time List', () => {
|
||||
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||
await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan,
|
||||
json: examplePlanSmall1,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
const groups = Object.keys(examplePlanSmall1);
|
||||
const firstGroupKey = groups[0];
|
||||
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
|
||||
await page.goto(
|
||||
@ -118,7 +82,7 @@ test.describe('Time List', () => {
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.getByRole('row').count();
|
||||
// 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 () => {
|
||||
@ -131,6 +95,17 @@ test.describe('Time List', () => {
|
||||
await expect(row.locator('.--end')).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.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 = {
|
||||
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
|
||||
}
|
||||
},
|
||||
emits: ['activity-selected'],
|
||||
data() {
|
||||
return {
|
||||
lineHeight: 10
|
||||
@ -142,30 +143,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
setSelectionForActivity(activity, event) {
|
||||
const element = event.currentTarget;
|
||||
const multiSelect = event.metaKey;
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
this.openmct.selection.select(
|
||||
[
|
||||
{
|
||||
element: element,
|
||||
context: {
|
||||
type: 'activity',
|
||||
activity: activity
|
||||
}
|
||||
},
|
||||
{
|
||||
element: this.openmct.layout.$refs.browseObject.$el,
|
||||
context: {
|
||||
item: this.domainObject,
|
||||
supportsMultiSelect: true
|
||||
}
|
||||
}
|
||||
],
|
||||
multiSelect
|
||||
);
|
||||
this.$emit('activity-selected', {
|
||||
event,
|
||||
selection: activity.selection
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -47,6 +47,7 @@
|
||||
:width="group.width"
|
||||
:is-nested="options.isChildObject"
|
||||
:status="status"
|
||||
@activity-selected="selectActivity"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -134,7 +135,7 @@ export default {
|
||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||
this.clipActivityNames = this.configuration.clipActivityNames;
|
||||
if (this.domainObject.type === 'plan') {
|
||||
this.planData = getValidatedData(this.domainObject);
|
||||
this.setPlanData(this.domainObject);
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
@ -177,6 +178,9 @@ export default {
|
||||
this.planViewConfiguration.destroy();
|
||||
},
|
||||
methods: {
|
||||
setPlanData(domainObject) {
|
||||
this.planData = getValidatedData(domainObject);
|
||||
},
|
||||
activityNameFitsRect(activityName, rectWidth) {
|
||||
return this.getTextWidth(activityName) + TEXT_LEFT_PADDING < rectWidth;
|
||||
},
|
||||
@ -215,9 +219,7 @@ export default {
|
||||
callback: () => {
|
||||
this.removeFromComposition(this.planObject);
|
||||
this.planObject = domainObject;
|
||||
this.planData = getValidatedData(domainObject);
|
||||
this.setStatus(this.openmct.status.get(domainObject.identifier));
|
||||
this.setScaleAndGenerateActivities();
|
||||
this.handleSelectFileChange();
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
@ -237,9 +239,7 @@ export default {
|
||||
} else {
|
||||
this.planObject = domainObject;
|
||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||
this.planData = getValidatedData(domainObject);
|
||||
this.setStatus(this.openmct.status.get(domainObject.identifier));
|
||||
this.setScaleAndGenerateActivities();
|
||||
this.handleSelectFileChange(domainObject);
|
||||
}
|
||||
},
|
||||
handleConfigurationChange(newConfiguration) {
|
||||
@ -259,8 +259,10 @@ export default {
|
||||
|
||||
this.setScaleAndGenerateActivities();
|
||||
},
|
||||
handleSelectFileChange() {
|
||||
this.planData = getValidatedData(this.domainObject);
|
||||
handleSelectFileChange(domainObject) {
|
||||
const planDomainObject = domainObject || this.domainObject;
|
||||
this.setPlanData(planDomainObject);
|
||||
this.setStatus(this.openmct.status.get(planDomainObject.identifier));
|
||||
this.setScaleAndGenerateActivities();
|
||||
},
|
||||
removeFromComposition(domainObject) {
|
||||
@ -434,7 +436,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
rawActivities.forEach((rawActivity) => {
|
||||
rawActivities.forEach((rawActivity, index) => {
|
||||
if (!this.isActivityInBounds(rawActivity)) {
|
||||
return;
|
||||
}
|
||||
@ -481,13 +483,10 @@ export default {
|
||||
const activity = {
|
||||
color: color,
|
||||
textColor: textColor,
|
||||
name: rawActivity.name,
|
||||
exceeds: {
|
||||
start: this.xScale(this.viewBounds.start) > this.xScale(rawActivity.start),
|
||||
end: this.xScale(this.viewBounds.end) < this.xScale(rawActivity.end)
|
||||
},
|
||||
start: rawActivity.start,
|
||||
end: rawActivity.end,
|
||||
row: currentRow,
|
||||
textLines: textLines,
|
||||
textStart: textStart,
|
||||
@ -496,7 +495,11 @@ export default {
|
||||
rectStart: rectX1,
|
||||
rectEnd: showTextInsideRect ? rectX2 : textStart + textWidth,
|
||||
rectWidth: rectWidth,
|
||||
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow)
|
||||
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow),
|
||||
selection: {
|
||||
groupName,
|
||||
index
|
||||
}
|
||||
};
|
||||
activitiesByRow[currentRow].push(activity);
|
||||
});
|
||||
@ -573,6 +576,31 @@ export default {
|
||||
const activityName = activity.name.toLowerCase().replace(/ /g, '-');
|
||||
|
||||
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.
|
||||
-->
|
||||
<template>
|
||||
<div class="c-inspector__properties c-inspect-properties">
|
||||
<plan-activity-view
|
||||
v-for="activity in activities"
|
||||
:key="activity.id"
|
||||
:activity="activity"
|
||||
:heading="heading"
|
||||
/>
|
||||
</div>
|
||||
<plan-activity-time-view
|
||||
v-for="activity in activities"
|
||||
:key="activity.key"
|
||||
:activity="activity"
|
||||
:heading="heading"
|
||||
/>
|
||||
<plan-activity-properties-view
|
||||
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>
|
||||
|
||||
<script>
|
||||
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 = {
|
||||
start: 'Start DateTime',
|
||||
@ -44,23 +58,34 @@ const propertyLabels = {
|
||||
latestEnd: 'Latest End',
|
||||
gap: 'Gap',
|
||||
overlap: 'Overlap',
|
||||
totalTime: 'Total Time'
|
||||
totalTime: 'Total Time',
|
||||
description: 'Description'
|
||||
};
|
||||
export default {
|
||||
components: {
|
||||
PlanActivityView
|
||||
PlanActivityTimeView,
|
||||
PlanActivityPropertiesView,
|
||||
PlanActivityStatusView
|
||||
},
|
||||
inject: ['openmct', 'selection'],
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
activities: [],
|
||||
selectedActivities: [],
|
||||
activityExecutionState: undefined,
|
||||
heading: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canPersistState() {
|
||||
return this.selectedActivities.length === 1 && this.activities?.[0]?.id;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setFormatters();
|
||||
this.getPlanData(this.selection);
|
||||
this.getActivityStates();
|
||||
this.getActivities();
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.openmct.time.on('timeSystem', this.setFormatters);
|
||||
@ -68,8 +93,28 @@ export default {
|
||||
beforeUnmount() {
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
this.openmct.time.off('timeSystem', this.setFormatters);
|
||||
if (this.stopObservingActivityStatesObject) {
|
||||
this.stopObservingActivityStatesObject();
|
||||
}
|
||||
},
|
||||
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() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
|
||||
@ -86,6 +131,7 @@ export default {
|
||||
if (selectionItem[0].context.type === 'activity') {
|
||||
const activity = selectionItem[0].context.activity;
|
||||
if (activity) {
|
||||
activity.key = activity.id ?? activity.name;
|
||||
this.selectedActivities.push(activity);
|
||||
}
|
||||
}
|
||||
@ -104,20 +150,37 @@ export default {
|
||||
this.activities.splice(0);
|
||||
this.selectedActivities.forEach((selectedActivity, index) => {
|
||||
const activity = {
|
||||
id: uuid(),
|
||||
start: {
|
||||
label: propertyLabels.start,
|
||||
value: this.formatTime(selectedActivity.start)
|
||||
},
|
||||
end: {
|
||||
label: propertyLabels.end,
|
||||
value: this.formatTime(selectedActivity.end)
|
||||
},
|
||||
duration: {
|
||||
label: propertyLabels.duration,
|
||||
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
|
||||
id: selectedActivity.id,
|
||||
key: selectedActivity.key,
|
||||
timeProperties: {
|
||||
start: {
|
||||
label: propertyLabels.start,
|
||||
value: this.formatTime(selectedActivity.start)
|
||||
},
|
||||
end: {
|
||||
label: propertyLabels.end,
|
||||
value: this.formatTime(selectedActivity.end)
|
||||
},
|
||||
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;
|
||||
});
|
||||
},
|
||||
@ -141,6 +204,8 @@ export default {
|
||||
let latestEnd;
|
||||
let gap;
|
||||
let overlap;
|
||||
let id;
|
||||
let key;
|
||||
|
||||
//Sort by start time
|
||||
let selectedActivities = this.selectedActivities.sort(this.sortFn);
|
||||
@ -159,6 +224,8 @@ export default {
|
||||
earliestStart = Math.min(earliestStart, selectedActivity.start);
|
||||
latestEnd = Math.max(latestEnd, selectedActivity.end);
|
||||
} else {
|
||||
id = selectedActivity.id;
|
||||
key = selectedActivity.id ?? selectedActivity.name;
|
||||
earliestStart = selectedActivity.start;
|
||||
latestEnd = selectedActivity.end;
|
||||
}
|
||||
@ -166,30 +233,33 @@ export default {
|
||||
let totalTime = latestEnd - earliestStart;
|
||||
|
||||
const activity = {
|
||||
id: uuid(),
|
||||
earliestStart: {
|
||||
label: propertyLabels.earliestStart,
|
||||
value: this.formatTime(earliestStart)
|
||||
},
|
||||
latestEnd: {
|
||||
label: propertyLabels.latestEnd,
|
||||
value: this.formatTime(latestEnd)
|
||||
id,
|
||||
key,
|
||||
timeProperties: {
|
||||
earliestStart: {
|
||||
label: propertyLabels.earliestStart,
|
||||
value: this.formatTime(earliestStart)
|
||||
},
|
||||
latestEnd: {
|
||||
label: propertyLabels.latestEnd,
|
||||
value: this.formatTime(latestEnd)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (gap) {
|
||||
activity.gap = {
|
||||
activity.timeProperties.gap = {
|
||||
label: propertyLabels.gap,
|
||||
value: this.formatDuration(gap)
|
||||
};
|
||||
} else if (overlap) {
|
||||
activity.overlap = {
|
||||
activity.timeProperties.overlap = {
|
||||
label: propertyLabels.overlap,
|
||||
value: this.formatDuration(overlap)
|
||||
};
|
||||
}
|
||||
|
||||
activity.totalTime = {
|
||||
activity.timeProperties.totalTime = {
|
||||
label: propertyLabels.totalTime,
|
||||
value: this.formatDuration(totalTime)
|
||||
};
|
||||
@ -201,6 +271,11 @@ export default {
|
||||
},
|
||||
formatTime(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>
|
||||
<div v-if="timeProperties.length" class="u-contents">
|
||||
<div class="c-inspect-properties__header">
|
||||
{{ heading }}
|
||||
<div class="c-inspector__properties c-inspect-properties">
|
||||
<div v-if="timeProperties.length" class="u-contents">
|
||||
<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>
|
||||
<ul
|
||||
v-for="timeProperty in timeProperties"
|
||||
:key="timeProperty.id"
|
||||
class="c-inspect-properties__section"
|
||||
>
|
||||
<activity-property :label="timeProperty.label" :value="timeProperty.value" />
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import ActivityProperty from './ActivityProperty.vue';
|
||||
|
||||
export default {
|
||||
@ -64,13 +64,14 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
setProperties() {
|
||||
Object.keys(this.activity).forEach((key) => {
|
||||
if (this.activity[key].label) {
|
||||
const label = this.activity[key].label;
|
||||
const value = String(this.activity[key].value);
|
||||
Object.keys(this.activity.timeProperties).forEach((key) => {
|
||||
if (this.activity.timeProperties[key].label) {
|
||||
const label = this.activity.timeProperties[key].label;
|
||||
const value = String(this.activity.timeProperties[key].value);
|
||||
const id = this.activity.id;
|
||||
|
||||
this.timeProperties[this.timeProperties.length] = {
|
||||
id: uuid(),
|
||||
id,
|
||||
label,
|
||||
value
|
||||
};
|
@ -20,12 +20,28 @@
|
||||
* 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 ActivityInspectorViewProvider from './inspector/ActivityInspectorViewProvider.js';
|
||||
import GanttChartInspectorViewProvider from './inspector/GanttChartInspectorViewProvider.js';
|
||||
import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration.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 = {}) {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('plan', {
|
||||
@ -70,5 +86,13 @@ export default function (options = {}) {
|
||||
openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct));
|
||||
openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* 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';
|
||||
export function getValidatedData(domainObject) {
|
||||
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]) {
|
||||
mappedJson[groupIdKey] = [];
|
||||
}
|
||||
@ -110,6 +130,26 @@ export function getValidatedGroups(domainObject, planData) {
|
||||
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) {
|
||||
let values = [];
|
||||
if (Array.isArray(activity.filterMetadataValues)) {
|
||||
|
@ -123,15 +123,6 @@
|
||||
.is-editing .l-layout__frame & {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: $colorSelectedBg !important;
|
||||
color: $colorSelectedFg !important;
|
||||
td {
|
||||
background: none !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
@ -186,6 +177,18 @@ td {
|
||||
@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 */
|
||||
.is-editing {
|
||||
.c-telemetry-table__headers__labels {
|
||||
|
@ -27,6 +27,7 @@
|
||||
:header-items="headerItems"
|
||||
:default-sort="defaultSort"
|
||||
class="sticky"
|
||||
@item-selection-changed="setSelectionForActivity"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -541,6 +542,29 @@ export default {
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
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) {
|
||||
return {
|
||||
key: 'timelist-inspector',
|
||||
name: 'Timelist Inspector View',
|
||||
name: 'Config',
|
||||
canView: function (selection) {
|
||||
if (selection.length === 0 || selection[0].length === 0) {
|
||||
return false;
|
||||
|
@ -43,6 +43,7 @@
|
||||
:key="item.key"
|
||||
:item="item"
|
||||
:item-properties="itemProperties"
|
||||
@click.stop="itemSelected(item, $event)"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -86,6 +87,7 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: ['item-selection-changed'],
|
||||
data() {
|
||||
let sortBy = this.defaultSort.property;
|
||||
let ascending = this.defaultSort.defaultDirection;
|
||||
@ -156,6 +158,9 @@ export default {
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
itemSelected(item, event) {
|
||||
this.$emit('item-selection-changed', item, event.currentTarget);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user