Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Activity state display for plans in Gantt and Time list views #7370

Merged
merged 36 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7003f00
Add activity states domain object and interceptor to auto create one
shefalijoshi Jan 10, 2024
92a30a3
Add activity state inspector option
shefalijoshi Jan 10, 2024
4d4f83e
Only save status if we have a unique ids for activities
shefalijoshi Jan 10, 2024
02edb99
Include the id in the activity properties
shefalijoshi Jan 10, 2024
e47bfed
Don't show activity state section in the inspector if multiple activi…
shefalijoshi Jan 10, 2024
3c14025
Display activity properties when an activity row is selected in the t…
shefalijoshi Jan 10, 2024
65a7e7e
Use activity id as key if it is available
shefalijoshi Jan 18, 2024
176d344
Ensure the correct option is selected for activity states
shefalijoshi Jan 18, 2024
02c796a
Add status label
shefalijoshi Jan 19, 2024
b0a21d4
Refactor activity selection. Display activity properties
shefalijoshi Jan 19, 2024
dc1def5
Remove activity states plugin. Move the activity states interceptor t…
shefalijoshi Jan 19, 2024
b413a9b
Change activity states interceptor parameters to options
shefalijoshi Jan 19, 2024
bbf5454
Rename constants
shefalijoshi Jan 19, 2024
6def4c2
Fix activity states test
shefalijoshi Jan 19, 2024
9b85252
Merge branch 'master' of https://github.com/nasa/openmct into activit…
shefalijoshi Jan 19, 2024
5908f96
Merge branch 'master' into activity-state-display
shefalijoshi Jan 23, 2024
78f2852
Add e2e test for activity states feature.
shefalijoshi Jan 23, 2024
5c6a733
Address review comments. Rename variables, documentation.
shefalijoshi Jan 23, 2024
42ed592
No shallow copy
shefalijoshi Jan 23, 2024
122c84b
Suppress lint warning for conditionals
shefalijoshi Jan 25, 2024
6ee3177
Remove check for abort controller
shefalijoshi Jan 25, 2024
8e53866
Move classes to components
shefalijoshi Jan 25, 2024
619e253
number primitive
shefalijoshi Jan 25, 2024
6a7ea35
Closes #7369
charlesh88 Jan 25, 2024
6abfac7
Ensure 'notStarted' is the default state for activities
shefalijoshi Jan 25, 2024
fc64682
Remove extra quotes
shefalijoshi Jan 25, 2024
6d3242b
Closes #7369
charlesh88 Jan 25, 2024
795b35f
Merge remote-tracking branch 'origin/activity-state-display' into act…
charlesh88 Jan 25, 2024
875edd9
Merge branch 'master' into activity-state-display
shefalijoshi Jan 25, 2024
a5abc32
Merge branch 'activity-state-display' of https://github.com/nasa/open…
shefalijoshi Jan 25, 2024
2839246
Use generated key for vue
shefalijoshi Jan 25, 2024
e79b99e
Fix e2e tests
shefalijoshi Jan 25, 2024
4f6a792
Fix timelist test
shefalijoshi Jan 26, 2024
a494a24
Merge branch 'master' into activity-state-display
shefalijoshi Jan 26, 2024
e8637b1
Merge branch 'master' into activity-state-display
unlikelyzero Jan 26, 2024
f404116
Merge branch 'master' of https://github.com/nasa/openmct into activit…
shefalijoshi Jan 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions e2e/test-data/examplePlans/ExamplePlan_Small1.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,44 @@
"end": 1660343797000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 1
},
{
"name": "Past event 2",
"start": 1660406808000,
"end": 1660429160000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 2
},
{
"name": "Past event 3",
"start": 1660493208000,
"end": 1660503981000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 3
},
{
"name": "Past event 4",
"start": 1660579608000,
"end": 1660624108000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 4
},
{
"name": "Past event 5",
"start": 1660666008000,
"end": 1660681529000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 5
}
]
}
12 changes: 8 additions & 4 deletions e2e/test-data/examplePlans/ExamplePlan_Small3.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
"end": 1660343797000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 1
},
{
"name": "Time until supper",
"start": 1650320402000,
"end": 1650420410000,
"type": "Group 2",
"color": "blue",
"textColor": "white"
"textColor": "white",
"id": 2
}
],
"Group 2": [
Expand All @@ -24,15 +26,17 @@
"end": 1650320102001,
"type": "Group 2",
"color": "green",
"textColor": "white"
"textColor": "white",
"id": 3
},
{
"name": "Time since last accident",
"start": 1650320102002,
"end": 1650320102002,
"type": "Group 1",
"color": "yellow",
"textColor": "white"
"textColor": "white",
"id": 4
}
]
}
49 changes: 48 additions & 1 deletion e2e/tests/functional/planning/plan.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -63,4 +63,51 @@ 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 label is displayed in the inspector.
await expect(page.getByLabel('Activity Status Label')).toHaveText('Not started');

// Check that activity state dropdown selection shows the `set status` option by default
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
'- Set Status -'
);

// 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 Label')).toHaveText('Aborted');
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
'Aborted'
);
});
});
75 changes: 24 additions & 51 deletions e2e/tests/functional/planning/timelist.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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 () => {
Expand All @@ -131,6 +95,15 @@ 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 Label')).toHaveText('Not started');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice use of test.step!

});
});

Expand Down
8 changes: 7 additions & 1 deletion src/api/objects/ObjectAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions src/plugins/activityStates/activityStatesInterceptor.js
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

excellent

* @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;

Check warning on line 62 in src/plugins/activityStates/activityStatesInterceptor.js

View check run for this annotation

Codecov / codecov/patch

src/plugins/activityStates/activityStatesInterceptor.js#L62

Added line #L62 was not covered by tests
},
priority
};
}

export default activityStatesInterceptor;
30 changes: 30 additions & 0 deletions src/plugins/activityStates/createActivityStatesIdentifier.js
Original file line number Diff line number Diff line change
@@ -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';
shefalijoshi marked this conversation as resolved.
Show resolved Hide resolved

export function createActivityStatesIdentifier(namespace = '') {
return {
key: ACTIVITY_STATES_KEY,
namespace
};
}
Loading
Loading