diff --git a/e2e/appActions.js b/e2e/appActions.js index b80f30f599..c6ab55485d 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -159,24 +159,26 @@ async function expandTreePaneItemByName(page, name) { * @returns {Promise} An object containing information about the newly created domain object. */ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) { + if (!name) { + name = `Plan:${genUuid()}`; + } + const parentUrl = await getHashUrlToDomainObject(page, parent); // Navigate to the parent object. This is necessary to create the object // in the correct location, such as a folder, layout, or plot. await page.goto(`${parentUrl}?hideTree=true`); - //Click the Create button + // Click the Create button await page.click('button:has-text("Create")'); // Click 'Plan' menu option await page.click(`li:text("Plan")`); // Modify the name input field of the domain object to accept 'name' - if (name) { - const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); - await nameInput.fill(""); - await nameInput.fill(name); - } + const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); + await nameInput.fill(""); + await nameInput.fill(name); // Upload buffer from memory await page.locator('input#fileElem').setInputFiles({ @@ -194,7 +196,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) { ]); // Wait until the URL is updated - await page.waitForURL(`**/mine/*`); + await page.waitForURL(`**/${parent}/*`); const uuid = await getFocusedObjectUuid(page); const objectUrl = await getHashUrlToDomainObject(page, uuid); diff --git a/e2e/helper/planningUtils.js b/e2e/helper/planningUtils.js new file mode 100644 index 0000000000..74075487fe --- /dev/null +++ b/e2e/helper/planningUtils.js @@ -0,0 +1,92 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +import { expect } from '../pluginFixtures'; + +/** + * Asserts that the number of activities in the plan view matches the number of + * activities in the plan data within the specified time bounds. Performs an assertion + * for each activity in the plan data per group, using the earliest activity's + * start time as the start bound and the current activity's end time as the end bound. + * @param {import('@playwright/test').Page} page the page + * @param {object} plan The raw plan json to assert against + * @param {string} objectUrl The URL of the object to assert against (plan or gantt chart) + */ +export async function assertPlanActivities(page, plan, objectUrl) { + const groups = Object.keys(plan); + for (const group of groups) { + for (let i = 0; i < plan[group].length; i++) { + // Set the startBound to the start time of the first activity in the group + const startBound = plan[group][0].start; + // Set the endBound to the end time of the current activity + let endBound = plan[group][i].end; + 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(`${objectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`); + + // Assert that the number of activities in the plan view matches the number of + // activities in the plan data within the specified time bounds + const eventCount = await page.locator('.activity-bounds').count(); + expect(eventCount).toEqual(Object.values(plan) + .flat() + .filter(event => + activitiesWithinTimeBounds(event.start, event.end, startBound, endBound)).length); + } + } +} + +/** + * Returns true if the activities time bounds overlap, false otherwise. +* @param {number} start1 the start time of the first activity +* @param {number} end1 the end time of the first activity +* @param {number} start2 the start time of the second activity +* @param {number} end2 the end time of the second activity +* @returns {boolean} true if the activities overlap, false otherwise +*/ +function activitiesWithinTimeBounds(start1, end1, start2, end2) { + return (start1 >= start2 && start1 <= end2) + || (end1 >= start2 && end1 <= end2) + || (start2 >= start1 && start2 <= end1) + || (end2 >= start1 && end2 <= end1); +} + +/** + * Navigate to the plan view, switch to fixed time mode, + * and set the bounds to span all activities. + * @param {import('@playwright/test').Page} page + * @param {object} planJson + * @param {string} planObjectUrl + */ +export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl) { + const activities = Object.values(planJson).flat(); + // Get the earliest start value + const start = Math.min(...activities.map(activity => activity.start)); + // Get the latest end value + const end = Math.max(...activities.map(activity => activity.end)); + // Set the start and end bounds to the earliest start and latest end + await page.goto(`${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view`); +} diff --git a/e2e/test-data/examplePlans/ExamplePlan_Large.json b/e2e/test-data/examplePlans/ExamplePlan_Large.json new file mode 100644 index 0000000000..a81f800947 --- /dev/null +++ b/e2e/test-data/examplePlans/ExamplePlan_Large.json @@ -0,0 +1,1080 @@ +{ + "Lorem": [ + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829001", + "name": "Lorem ipsum dolor 2023-03-15 21:00", + "start": 1678914000000, + "end": 1678920203000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829002", + "name": "Nam vehicula magna 2023-03-15 23:08", + "start": 1678921680000, + "end": 1678927886000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829003", + "name": "Praesent pharetra risus 2023-03-15 23:28", + "start": 1678922880000, + "end": 1678927341000, + "color": "#1A6398", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829004", + "name": "Duis eget arcu 2023-03-16 01:30", + "start": 1678930200000, + "end": 1678933775000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829005", + "name": "Maecenas elementum purus 2023-03-16 02:00", + "start": 1678932000000, + "end": 1678936537000, + "color": "#cc9900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829006", + "name": "Praesent in mauris 2023-03-16 03:27", + "start": 1678937220000, + "end": 1678943380000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829007", + "name": "Donec id nunc 2023-03-16 04:08", + "start": 1678939680000, + "end": 1678942180000, + "color": "#398129A", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829008", + "name": "Donec quis nunc 2023-03-16 04:45", + "start": 1678941900000, + "end": 1678942146000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829009", + "name": "Maecenas in purus 2023-03-16 06:45", + "start": 1678949100000, + "end": 1678954538000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829010", + "name": "Proin vehicula elit 2023-03-16 07:01", + "start": 1678950060000, + "end": 1678955494000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829011", + "name": "Nunc ac lorem 2023-03-16 07:04", + "start": 1678950240000, + "end": 1678957383000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829012", + "name": "Aenean mollis lorem 2023-03-16 07:55", + "start": 1678953300000, + "end": 1678954984000, + "color": "#cc9900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829013", + "name": "Nunc hendrerit ipsum 2023-03-16 08:53", + "start": 1678956780000, + "end": 1678962831000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829014", + "name": "Vestibulum placerat velit 2023-03-16 10:35", + "start": 1678962900000, + "end": 1678968570000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829015", + "name": "Praesent eu augue 2023-03-16 11:02", + "start": 1678964520000, + "end": 1678966583000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829016", + "name": "Quisque mattis ligula 2023-03-16 11:53", + "start": 1678967580000, + "end": 1678974730000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829017", + "name": "Quisque a lectus 2023-03-16 13:33", + "start": 1678973580000, + "end": 1678977996000, + "color": "#cc9900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829018", + "name": "Nam mattis risus 2023-03-16 15:02", + "start": 1678978920000, + "end": 1678982935000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829019", + "name": "Aenean rutrum libero 2023-03-16 16:26", + "start": 1678983960000, + "end": 1678990712000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829020", + "name": "Fusce tincidunt lorem 2023-03-16 17:45", + "start": 1678988700000, + "end": 1678995915000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829021", + "name": "Donec et nibh 2023-03-16 19:09", + "start": 1678993740000, + "end": 1678995474000, + "color": "#009900", + "textColor": "#ffffff" + } + ], + "Ipsum": [ + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829022", + "name": "Praesent blandit urna 2023-03-16 20:10", + "start": 1678997400000, + "end": 1679003749000, + "color": "#1A6398", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829023", + "name": "Nullam congue est 2023-03-16 21:57", + "start": 1679003820000, + "end": 1679008250000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829024", + "name": "Sed sed enim 2023-03-16 22:34", + "start": 1679006040000, + "end": 1679012412000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829025", + "name": "Donec tincidunt ante 2023-03-16 23:09", + "start": 1679008140000, + "end": 1679011218000, + "color": "#1A6398", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829026", + "name": "Morbi bibendum tellus 2023-03-17 00:17", + "start": 1679012220000, + "end": 1679014528000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829027", + "name": "Quisque id est 2023-03-17 01:59", + "start": 1679018340000, + "end": 1679025535000, + "color": "#398129A", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829028", + "name": "Vivamus imperdiet tellus 2023-03-17 04:01", + "start": 1679025660000, + "end": 1679031500000, + "color": "#1A6398", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829029", + "name": "Pellentesque tempus augue 2023-03-17 04:10", + "start": 1679026200000, + "end": 1679029090000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829030", + "name": "Donec vel mauris 2023-03-17 05:21", + "start": 1679030460000, + "end": 1679033163000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829031", + "name": "Morbi ac ipsum 2023-03-17 06:54", + "start": 1679036040000, + "end": 1679043127000, + "color": "#1A6398", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829032", + "name": "Sed elementum nisi 2023-03-17 07:26", + "start": 1679037960000, + "end": 1679042424000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829033", + "name": "Proin at ipsum 2023-03-17 09:07", + "start": 1679044020000, + "end": 1679044445000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829034", + "name": "Sed cursus ipsum 2023-03-17 11:12", + "start": 1679051520000, + "end": 1679056773000, + "color": "#1A6398", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829035", + "name": "Nullam mollis purus 2023-03-17 11:42", + "start": 1679053320000, + "end": 1679057845000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829036", + "name": "Aliquam vitae erat 2023-03-17 13:02", + "start": 1679058120000, + "end": 1679059116000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829037", + "name": "Aenean congue orci 2023-03-17 13:48", + "start": 1679060880000, + "end": 1679066284000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829038", + "name": "Praesent vel orci 2023-03-17 15:58", + "start": 1679068680000, + "end": 1679070694000, + "color": "#398129A", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829039", + "name": "In non diam 2023-03-17 16:19", + "start": 1679069940000, + "end": 1679072641000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829040", + "name": "Suspendisse sit amet 2023-03-17 17:51", + "start": 1679075460000, + "end": 1679078232000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829041", + "name": "Sed sit amet 2023-03-17 18:33", + "start": 1679077980000, + "end": 1679083509000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829042", + "name": "Donec dictum justo 2023-03-17 19:17", + "start": 1679080620000, + "end": 1679086107000, + "color": "#398129A", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829043", + "name": "Etiam vestibulum justo 2023-03-17 19:39", + "start": 1679081940000, + "end": 1679086413000, + "color": "#cc9900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829044", + "name": "Sed ullamcorper diam 2023-03-17 20:18", + "start": 1679084280000, + "end": 1679088693000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829045", + "name": "Cras aliquet dolor 2023-03-17 20:27", + "start": 1679084820000, + "end": 1679086883000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829046", + "name": "Duis et orci 2023-03-17 22:29", + "start": 1679092140000, + "end": 1679092436000, + "color": "#398129A", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829047", + "name": "Pellentesque quis augue 2023-03-17 23:01", + "start": 1679094060000, + "end": 1679099511000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829048", + "name": "Mauris sed ligula 2023-03-17 23:46", + "start": 1679096760000, + "end": 1679100276000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829049", + "name": "Vestibulum non quam 2023-03-17 23:52", + "start": 1679097120000, + "end": 1679099465000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829050", + "name": "Nulla ut ante 2023-03-18 01:00", + "start": 1679101200000, + "end": 1679105368000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829051", + "name": "Vivamus scelerisque est 2023-03-18 02:26", + "start": 1679106360000, + "end": 1679107303000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829052", + "name": "Pellentesque viverra lectus 2023-03-18 04:28", + "start": 1679113680000, + "end": 1679120663000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829053", + "name": "Vivamus placerat metus 2023-03-18 05:54", + "start": 1679118840000, + "end": 1679120052000, + "color": "#cc9900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829054", + "name": "Pellentesque nec quam 2023-03-18 06:20", + "start": 1679120400000, + "end": 1679124820000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829055", + "name": "Ut semper dui 2023-03-18 08:11", + "start": 1679127060000, + "end": 1679131793000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829056", + "name": "Praesent condimentum purus 2023-03-18 09:42", + "start": 1679132520000, + "end": 1679136293000, + "color": "#cc9900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829057", + "name": "Morbi varius leo 2023-03-18 10:18", + "start": 1679134680000, + "end": 1679135462000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829058", + "name": "Etiam non nulla 2023-03-18 11:14", + "start": 1679138040000, + "end": 1679144793000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829059", + "name": "Integer vitae quam 2023-03-18 11:27", + "start": 1679138820000, + "end": 1679145512000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829060", + "name": "Ut bibendum sapien 2023-03-18 13:26", + "start": 1679145960000, + "end": 1679151196000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829061", + "name": "Praesent in nibh 2023-03-18 14:09", + "start": 1679148540000, + "end": 1679152592000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829062", + "name": "Mauris tempus risus 2023-03-18 15:53", + "start": 1679154780000, + "end": 1679157952000, + "color": "#398129A", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829063", + "name": "Vivamus eu massa 2023-03-18 16:10", + "start": 1679155800000, + "end": 1679157039000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829064", + "name": "Aenean pharetra nulla 2023-03-18 17:08", + "start": 1679159280000, + "end": 1679163693000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829065", + "name": "Vivamus elementum mauris 2023-03-18 18:54", + "start": 1679165640000, + "end": 1679171765000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829066", + "name": "Curabitur vel nunc 2023-03-18 20:27", + "start": 1679171220000, + "end": 1679176508000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829067", + "name": "Donec condimentum quam 2023-03-18 22:06", + "start": 1679177160000, + "end": 1679179341000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829068", + "name": "Etiam tristique velit 2023-03-18 23:45", + "start": 1679183100000, + "end": 1679183457000, + "color": "#cc9900", + "textColor": "#ffffff" + } + ], + "Nonsectetur": [ + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829069", + "name": "Praesent at purus 2023-03-19 00:32", + "start": 1679185920000, + "end": 1679188046000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829070", + "name": "Morbi convallis nunc 2023-03-19 01:34", + "start": 1679189640000, + "end": 1679194168000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829071", + "name": "Vestibulum at augue 2023-03-19 03:04", + "start": 1679195040000, + "end": 1679201691000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829072", + "name": "Morbi eget ante 2023-03-19 04:29", + "start": 1679200140000, + "end": 1679200843000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829073", + "name": "Aenean mattis ante 2023-03-19 04:40", + "start": 1679200800000, + "end": 1679203030000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829074", + "name": "Nam vitae ante 2023-03-19 04:43", + "start": 1679200980000, + "end": 1679201798000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829075", + "name": "Suspendisse accumsan lacus 2023-03-19 06:24", + "start": 1679207040000, + "end": 1679207797000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829076", + "name": "In rutrum mauris 2023-03-19 08:27", + "start": 1679214420000, + "end": 1679216767000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829077", + "name": "Vivamus posuere tellus 2023-03-19 10:20", + "start": 1679221200000, + "end": 1679226475000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829078", + "name": "Ut non turpis 2023-03-19 12:10", + "start": 1679227800000, + "end": 1679229012000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829079", + "name": "Nunc tincidunt leo 2023-03-19 12:12", + "start": 1679227920000, + "end": 1679229913000, + "color": "#398129A", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829080", + "name": "Nunc egestas eros 2023-03-19 13:51", + "start": 1679233860000, + "end": 1679235101000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829081", + "name": "Sed feugiat tortor 2023-03-19 14:25", + "start": 1679235900000, + "end": 1679239630000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829082", + "name": "Pellentesque non dolor 2023-03-19 15:38", + "start": 1679240280000, + "end": 1679245396000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829083", + "name": "Nullam ut lorem 2023-03-19 15:50", + "start": 1679241000000, + "end": 1679244205000, + "color": "#1A6398", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829084", + "name": "Nam faucibus risus 2023-03-19 16:51", + "start": 1679244660000, + "end": 1679245213000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829085", + "name": "Proin euismod ligula 2023-03-19 17:14", + "start": 1679246040000, + "end": 1679249781000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829086", + "name": "Aenean congue orci 2023-03-19 17:23", + "start": 1679246580000, + "end": 1679248687000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829087", + "name": "Praesent vel orci 2023-03-19 18:55", + "start": 1679252100000, + "end": 1679256957000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829088", + "name": "In non diam 2023-03-19 19:06", + "start": 1679252760000, + "end": 1679252878000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829089", + "name": "Suspendisse sit amet 2023-03-19 20:42", + "start": 1679258520000, + "end": 1679259158000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829090", + "name": "Sed sit amet 2023-03-19 22:37", + "start": 1679265420000, + "end": 1679268745000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829091", + "name": "Donec dictum justo 2023-03-19 23:19", + "start": 1679267940000, + "end": 1679272128000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829092", + "name": "Etiam vestibulum justo 2023-03-19 23:53", + "start": 1679269980000, + "end": 1679273096000, + "color": "#cc9900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829093", + "name": "Sed ullamcorper diam 2023-03-20 00:15", + "start": 1679271300000, + "end": 1679275297000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829094", + "name": "Cras aliquet dolor 2023-03-20 00:30", + "start": 1679272200000, + "end": 1679278448000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829095", + "name": "Duis et orci 2023-03-20 02:00", + "start": 1679277600000, + "end": 1679278981000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829096", + "name": "Pellentesque quis augue 2023-03-20 03:58", + "start": 1679284680000, + "end": 1679291126000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829097", + "name": "Mauris sed ligula 2023-03-20 04:35", + "start": 1679286900000, + "end": 1679294138000, + "color": "#398129A", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829098", + "name": "Vestibulum non quam 2023-03-20 04:52", + "start": 1679287920000, + "end": 1679291835000, + "color": "#cc9900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829099", + "name": "Nulla ut ante 2023-03-20 06:10", + "start": 1679292600000, + "end": 1679295800000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829100", + "name": "Vivamus scelerisque est 2023-03-20 07:38", + "start": 1679297880000, + "end": 1679304545000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829101", + "name": "Pellentesque viverra lectus 2023-03-20 08:14", + "start": 1679300040000, + "end": 1679305543000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829102", + "name": "Vivamus placerat metus 2023-03-20 09:58", + "start": 1679306280000, + "end": 1679313528000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829103", + "name": "Pellentesque nec quam 2023-03-20 11:08", + "start": 1679310480000, + "end": 1679311567000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829104", + "name": "Ut semper dui 2023-03-20 12:45", + "start": 1679316300000, + "end": 1679321703000, + "color": "#cc9900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829105", + "name": "Praesent condimentum purus 2023-03-20 14:32", + "start": 1679322720000, + "end": 1679326451000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829106", + "name": "Morbi varius leo 2023-03-20 14:42", + "start": 1679323320000, + "end": 1679329739000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829107", + "name": "Etiam non nulla 2023-03-20 16:10", + "start": 1679328600000, + "end": 1679330910000, + "color": "#1A6398", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829108", + "name": "Integer vitae quam 2023-03-20 17:22", + "start": 1679332920000, + "end": 1679333055000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829109", + "name": "Ut bibendum sapien 2023-03-20 17:28", + "start": 1679333280000, + "end": 1679334377000, + "color": "#398129A", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829110", + "name": "Praesent in nibh 2023-03-20 18:09", + "start": 1679335740000, + "end": 1679339834000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829111", + "name": "Mauris tempus risus 2023-03-20 18:45", + "start": 1679337900000, + "end": 1679338054000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829112", + "name": "Vivamus eu massa 2023-03-20 19:07", + "start": 1679339220000, + "end": 1679344356000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829113", + "name": "Aenean pharetra nulla 2023-03-20 21:00", + "start": 1679346000000, + "end": 1679349652000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829114", + "name": "Vivamus elementum mauris 2023-03-20 22:48", + "start": 1679352480000, + "end": 1679358089000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829115", + "name": "Curabitur vel nunc 2023-03-21 00:54", + "start": 1679360040000, + "end": 1679361673000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829116", + "name": "Donec condimentum quam 2023-03-21 02:14", + "start": 1679364840000, + "end": 1679370470000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829117", + "name": "Etiam tristique velit 2023-03-21 03:12", + "start": 1679368320000, + "end": 1679373263000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829118", + "name": "Praesent at purus 2023-03-21 05:19", + "start": 1679375940000, + "end": 1679376500000, + "color": "#1A6398", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829119", + "name": "Morbi convallis nunc 2023-03-21 06:39", + "start": 1679380740000, + "end": 1679383240000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829120", + "name": "Vestibulum at augue 2023-03-21 07:12", + "start": 1679382720000, + "end": 1679383807000, + "color": "#009900", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829121", + "name": "Morbi eget ante 2023-03-21 08:52", + "start": 1679388720000, + "end": 1679394965000, + "color": "#1A6398", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829122", + "name": "Aenean mattis ante 2023-03-21 10:52", + "start": 1679395920000, + "end": 1679396480000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829123", + "name": "Nam vitae ante 2023-03-21 11:40", + "start": 1679398800000, + "end": 1679401571000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829124", + "name": "Suspendisse accumsan lacus 2023-03-21 12:08", + "start": 1679400480000, + "end": 1679402991000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829125", + "name": "In rutrum mauris 2023-03-21 12:44", + "start": 1679402640000, + "end": 1679405235000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829126", + "name": "Vivamus posuere tellus 2023-03-21 13:27", + "start": 1679405220000, + "end": 1679407622000, + "color": "#28172", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829127", + "name": "Ut non turpis 2023-03-21 14:16", + "start": 1679408160000, + "end": 1679415007000, + "color": "#807C19", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829128", + "name": "Nunc tincidunt leo 2023-03-21 15:44", + "start": 1679413440000, + "end": 1679418071000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829129", + "name": "Nunc egestas eros 2023-03-21 16:42", + "start": 1679416920000, + "end": 1679421025000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829130", + "name": "Sed feugiat tortor 2023-03-21 17:04", + "start": 1679418240000, + "end": 1679422217000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829131", + "name": "Pellentesque non dolor 2023-03-21 19:03", + "start": 1679425380000, + "end": 1679428591000, + "color": "#6700CD", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829132", + "name": "Nullam ut lorem 2023-03-21 20:25", + "start": 1679430300000, + "end": 1679434622000, + "color": "#CD6300", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829133", + "name": "Nam faucibus risus 2023-03-21 21:16", + "start": 1679433360000, + "end": 1679434647000, + "color": "#62640B", + "textColor": "#ffffff" + }, + { + "uuid": "ef59547d-50ed-45c6-a5eb-a49629829134", + "name": "Proin euismod ligula 2023-03-21 23:00", + "start": 1679439600000, + "end": 1679443378000, + "color": "#398129A", + "textColor": "#ffffff" + } + ] +} \ No newline at end of file diff --git a/e2e/test-data/examplePlans/ExamplePlan_Small1.json b/e2e/test-data/examplePlans/ExamplePlan_Small1.json new file mode 100644 index 0000000000..de32fba310 --- /dev/null +++ b/e2e/test-data/examplePlans/ExamplePlan_Small1.json @@ -0,0 +1,44 @@ +{ + "Group 1": [ + { + "name": "Past event 1", + "start": 1660320408000, + "end": 1660343797000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Past event 2", + "start": 1660406808000, + "end": 1660429160000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Past event 3", + "start": 1660493208000, + "end": 1660503981000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Past event 4", + "start": 1660579608000, + "end": 1660624108000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Past event 5", + "start": 1660666008000, + "end": 1660681529000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + } + ] +} \ No newline at end of file diff --git a/e2e/test-data/examplePlans/ExamplePlan_Small2.json b/e2e/test-data/examplePlans/ExamplePlan_Small2.json new file mode 100644 index 0000000000..af744f4cc6 --- /dev/null +++ b/e2e/test-data/examplePlans/ExamplePlan_Small2.json @@ -0,0 +1,38 @@ +{ + "Group 1": [ + { + "name": "Group 1 event 1", + "start": 1650320408000, + "end": 1660343797000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Group 1 event 2", + "start": 1660005808000, + "end": 1660429160000, + "type": "Group 1", + "color": "yellow", + "textColor": "white" + } + ], + "Group 2": [ + { + "name": "Group 2 event 1", + "start": 1660320408000, + "end": 1660420408000, + "type": "Group 2", + "color": "green", + "textColor": "white" + }, + { + "name": "Group 2 event 2", + "start": 1660406808000, + "end": 1690429160000, + "type": "Group 2", + "color": "blue", + "textColor": "white" + } + ] +} \ No newline at end of file diff --git a/e2e/tests/functional/planning/ganttChart.e2e.spec.js b/e2e/tests/functional/planning/ganttChart.e2e.spec.js new file mode 100644 index 0000000000..755ced7f32 --- /dev/null +++ b/e2e/tests/functional/planning/ganttChart.e2e.spec.js @@ -0,0 +1,85 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ +const { test, expect } = require('../../../pluginFixtures'); +const { createPlanFromJSON, createDomainObjectWithDefaults, selectInspectorTab } = require('../../../appActions'); +const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json'); +const testPlan2 = require('../../../test-data/examplePlans/ExamplePlan_Small2.json'); +const { assertPlanActivities, setBoundsToSpanAllActivities } = require('../../../helper/planningUtils'); +const { getPreciseDuration } = require('../../../../src/utils/duration'); + +test.describe("Gantt Chart", () => { + let ganttChart; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'networkidle' }); + ganttChart = await createDomainObjectWithDefaults(page, { + type: 'Gantt Chart' + }); + await createPlanFromJSON(page, { + json: testPlan1, + parent: ganttChart.uuid + }); + }); + + test("Displays all plan events", async ({ page }) => { + await page.goto(ganttChart.url); + + await assertPlanActivities(page, testPlan1, ganttChart.url); + }); + test("Replaces a plan with a new plan", async ({ page }) => { + await assertPlanActivities(page, testPlan1, ganttChart.url); + await createPlanFromJSON(page, { + json: testPlan2, + parent: ganttChart.uuid + }); + const replaceModal = page.getByRole('dialog').filter({ hasText: "This action will replace the current Plan. Do you want to continue?" }); + await expect(replaceModal).toBeVisible(); + await page.getByRole('button', { name: 'OK' }).click(); + + await assertPlanActivities(page, testPlan2, ganttChart.url); + }); + test("Can select a single activity and display its details in the inspector", async ({ page }) => { + test.slow(); + await page.goto(ganttChart.url); + + await setBoundsToSpanAllActivities(page, testPlan1, ganttChart.url); + + const activities = Object.values(testPlan1).flat(); + const activity = activities[0]; + await page.locator('g').filter({ hasText: new RegExp(activity.name) }).click(); + await selectInspectorTab(page, 'Activity'); + + const startDateTime = await page.locator('.c-inspect-properties__label:has-text("Start DateTime")+.c-inspect-properties__value').innerText(); + const endDateTime = await page.locator('.c-inspect-properties__label:has-text("End DateTime")+.c-inspect-properties__value').innerText(); + const duration = await page.locator('.c-inspect-properties__label:has-text("duration")+.c-inspect-properties__value').innerText(); + + const expectedStartDate = new Date(activity.start).toISOString(); + const actualStartDate = new Date(startDateTime).toISOString(); + const expectedEndDate = new Date(activity.end).toISOString(); + const actualEndDate = new Date(endDateTime).toISOString(); + const expectedDuration = getPreciseDuration(activity.end - activity.start); + const actualDuration = duration; + + expect(expectedStartDate).toEqual(actualStartDate); + expect(expectedEndDate).toEqual(actualEndDate); + expect(expectedDuration).toEqual(actualDuration); + }); +}); diff --git a/e2e/tests/functional/planning/plan.e2e.spec.js b/e2e/tests/functional/planning/plan.e2e.spec.js index 11911b4247..552bace3f7 100644 --- a/e2e/tests/functional/planning/plan.e2e.spec.js +++ b/e2e/tests/functional/planning/plan.e2e.spec.js @@ -19,69 +19,21 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -const { test, expect } = require('../../../pluginFixtures'); +const { test } = require('../../../pluginFixtures'); const { createPlanFromJSON } = require('../../../appActions'); - -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" - } - ] -}; +const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json'); +const { assertPlanActivities } = require('../../../helper/planningUtils'); test.describe("Plan", () => { - test("Create a Plan and display all plan events @unstable", async ({ page }) => { + let plan; + test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'networkidle' }); - - const plan = await createPlanFromJSON(page, { - name: 'Test Plan', - json: testPlan + plan = await createPlanFromJSON(page, { + json: testPlan1 }); - const startBound = testPlan.TEST_GROUP[0].start; - const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end; + }); - // 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`); - const eventCount = await page.locator('.activity-bounds').count(); - expect(eventCount).toEqual(testPlan.TEST_GROUP.length); + test("Displays all plan events", async ({ page }) => { + await assertPlanActivities(page, testPlan1, plan.url); }); }); - diff --git a/e2e/tests/functional/planning/plan.visual.spec.js b/e2e/tests/functional/planning/plan.visual.spec.js new file mode 100644 index 0000000000..74109fafb0 --- /dev/null +++ b/e2e/tests/functional/planning/plan.visual.spec.js @@ -0,0 +1,52 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +const { test } = require('../../../pluginFixtures'); +const { setBoundsToSpanAllActivities } = require('../../../helper/planningUtils'); +const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions'); +const percySnapshot = require('@percy/playwright'); +const examplePlanLarge = require('../../../test-data/ExamplePlan_Large.json'); + +test.describe('Visual - Planning', () => { + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'networkidle' }); + }); + test('Plan View', async ({ page, theme }) => { + const plan = await createPlanFromJSON(page, { + json: examplePlanLarge + }); + + await setBoundsToSpanAllActivities(page, examplePlanLarge, plan.url); + await percySnapshot(page, `Plan View (theme: ${theme})`); + }); + test('Gantt Chart View', async ({ page, theme }) => { + const ganttChart = await createDomainObjectWithDefaults(page, { + type: 'Gantt Chart' + }); + await createPlanFromJSON(page, { + json: examplePlanLarge, + parent: ganttChart.uuid + }); + await setBoundsToSpanAllActivities(page, examplePlanLarge, ganttChart.url); + await percySnapshot(page, `Gantt Chart View (theme: ${theme})`); + }); +}); diff --git a/index.html b/index.html index b498b274dd..d80d0c9037 100644 --- a/index.html +++ b/index.html @@ -84,7 +84,9 @@ openmct.install(openmct.plugins.Espresso()); openmct.install(openmct.plugins.MyItems()); - openmct.install(openmct.plugins.PlanLayout()); + openmct.install(openmct.plugins.PlanLayout({ + creatable: true + })); openmct.install(openmct.plugins.Timeline()); openmct.install(openmct.plugins.Hyperlink()); openmct.install(openmct.plugins.UTCTimeSystem()); diff --git a/src/api/Editor.js b/src/api/Editor.js index 43c9a8fd6d..2973f3242f 100644 --- a/src/api/Editor.js +++ b/src/api/Editor.js @@ -46,7 +46,7 @@ export default class Editor extends EventEmitter { } /** - * @returns true if the application is in edit mode, false otherwise. + * @returns {boolean} true if the application is in edit mode, false otherwise. */ isEditing() { return this.editing; diff --git a/src/api/api.js b/src/api/api.js index c569af5ba7..d6e5684398 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -71,7 +71,7 @@ function ( StatusAPI: StatusAPI.default, TelemetryAPI: TelemetryAPI, TimeAPI: TimeAPI.default, - TypeRegistry: TypeRegistry, + TypeRegistry: TypeRegistry.default, UserAPI: UserAPI.default, AnnotationAPI: AnnotationAPI.default }; diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index a242b83feb..b4b0dbc338 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -62,6 +62,8 @@ import InMemorySearchProvider from './InMemorySearchProvider'; * @property {Identifier[]} [composition] if * present, this will be used by the default composition provider * to load domain objects + * @property {Object.} [configuration] A key-value map containing configuration + * settings for this domain object. * @memberof module:openmct.ObjectAPI~ */ diff --git a/src/api/types/Type.js b/src/api/types/Type.js index 289b118863..4c0f052684 100644 --- a/src/api/types/Type.js +++ b/src/api/types/Type.js @@ -20,63 +20,25 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define(function () { - - /** - * A Type describes a kind of domain object that may appear or be - * created within Open MCT. - * - * @param {module:opemct.TypeRegistry~TypeDefinition} definition - * @class Type - * @memberof module:openmct - */ - function Type(definition) { +/** + * A Type describes a kind of domain object that may appear or be + * created within Open MCT. + * + * @param {module:opemct.TypeRegistry~TypeDefinition} definition + * @class Type + * @memberof module:openmct + */ +export default class Type { + constructor(definition) { this.definition = definition; if (definition.key) { this.key = definition.key; } } - - /** - * Check if a domain object is an instance of this type. - * @param domainObject - * @returns {boolean} true if the domain object is of this type - * @memberof module:openmct.Type# - * @method check - */ - Type.prototype.check = function (domainObject) { - // Depends on assignment from MCT. - return domainObject.type === this.key; - }; - - /** - * Get a definition for this type that can be registered using the - * legacy bundle format. - * @private - */ - Type.prototype.toLegacyDefinition = function () { - const def = {}; - def.name = this.definition.name; - def.cssClass = this.definition.cssClass; - def.description = this.definition.description; - def.properties = this.definition.form; - - if (this.definition.initialize) { - def.model = {}; - this.definition.initialize(def.model); - } - - if (this.definition.creatable) { - def.features = ['creation']; - } - - return def; - }; - /** * Create a type definition from a legacy definition. */ - Type.definitionFromLegacyDefinition = function (legacyDefinition) { + static definitionFromLegacyDefinition(legacyDefinition) { let definition = {}; definition.name = legacyDefinition.name; definition.cssClass = legacyDefinition.cssClass; @@ -121,7 +83,39 @@ define(function () { } return definition; - }; + } + /** + * Check if a domain object is an instance of this type. + * @param domainObject + * @returns {boolean} true if the domain object is of this type + * @memberof module:openmct.Type# + * @method check + */ + check(domainObject) { + // Depends on assignment from MCT. + return domainObject.type === this.key; + } + /** + * Get a definition for this type that can be registered using the + * legacy bundle format. + * @private + */ + toLegacyDefinition() { + const def = {}; + def.name = this.definition.name; + def.cssClass = this.definition.cssClass; + def.description = this.definition.description; + def.properties = this.definition.form; - return Type; -}); + if (this.definition.initialize) { + def.model = {}; + this.definition.initialize(def.model); + } + + if (this.definition.creatable) { + def.features = ['creation']; + } + + return def; + } +} diff --git a/src/api/types/TypeRegistry.js b/src/api/types/TypeRegistry.js index 0e1b873fb5..a5b3731048 100644 --- a/src/api/types/TypeRegistry.js +++ b/src/api/types/TypeRegistry.js @@ -19,35 +19,36 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -define(['./Type'], function (Type) { - const UNKNOWN_TYPE = new Type({ - key: "unknown", - name: "Unknown Type", - cssClass: "icon-object-unknown" - }); +import Type from './Type'; - /** - * @typedef TypeDefinition - * @memberof module:openmct.TypeRegistry~ - * @property {string} label the name for this type of object - * @property {string} description a longer-form description of this type - * @property {function (object)} [initialize] a function which initializes - * the model for new domain objects of this type - * @property {boolean} [creatable] true if users should be allowed to - * create this type (default: false) - * @property {string} [cssClass] the CSS class to apply for icons - */ +const UNKNOWN_TYPE = new Type({ + key: "unknown", + name: "Unknown Type", + cssClass: "icon-object-unknown" +}); - /** - * A TypeRegistry maintains the definitions for different types - * that domain objects may have. - * @interface TypeRegistry - * @memberof module:openmct - */ - function TypeRegistry() { +/** + * @typedef TypeDefinition + * @memberof module:openmct.TypeRegistry~ + * @property {string} label the name for this type of object + * @property {string} description a longer-form description of this type + * @property {function (object)} [initialize] a function which initializes + * the model for new domain objects of this type + * @property {boolean} [creatable] true if users should be allowed to + * create this type (default: false) + * @property {string} [cssClass] the CSS class to apply for icons + */ + +/** + * A TypeRegistry maintains the definitions for different types + * that domain objects may have. + * @interface TypeRegistry + * @memberof module:openmct + */ +export default class TypeRegistry { + constructor() { this.types = {}; } - /** * Register a new object type. * @@ -56,17 +57,16 @@ define(['./Type'], function (Type) { * @method addType * @memberof module:openmct.TypeRegistry# */ - TypeRegistry.prototype.addType = function (typeKey, typeDef) { + addType(typeKey, typeDef) { this.standardizeType(typeDef); this.types[typeKey] = new Type(typeDef); - }; - + } /** * Takes a typeDef, standardizes it, and logs warnings about unsupported * usage. * @private */ - TypeRegistry.prototype.standardizeType = function (typeDef) { + standardizeType(typeDef) { if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) { if (!typeDef.name) { typeDef.name = typeDef.label; @@ -74,18 +74,16 @@ define(['./Type'], function (Type) { delete typeDef.label; } - }; - + } /** * List keys for all registered types. * @method listKeys * @memberof module:openmct.TypeRegistry# * @returns {string[]} all registered type keys */ - TypeRegistry.prototype.listKeys = function () { + listKeys() { return Object.keys(this.types); - }; - + } /** * Retrieve a registered type by its key. * @method get @@ -93,18 +91,15 @@ define(['./Type'], function (Type) { * @memberof module:openmct.TypeRegistry# * @returns {module:openmct.Type} the registered type */ - TypeRegistry.prototype.get = function (typeKey) { + get(typeKey) { return this.types[typeKey] || UNKNOWN_TYPE; - }; - - TypeRegistry.prototype.importLegacyTypes = function (types) { + } + importLegacyTypes(types) { types.filter((t) => this.get(t.key) === UNKNOWN_TYPE) .forEach((type) => { let def = Type.definitionFromLegacyDefinition(type); this.addType(type.key, def); }); - }; - - return TypeRegistry; -}); + } +} diff --git a/src/api/types/TypeRegistrySpec.js b/src/api/types/TypeRegistrySpec.js index 9f524f94c3..83ef11e116 100644 --- a/src/api/types/TypeRegistrySpec.js +++ b/src/api/types/TypeRegistrySpec.js @@ -20,36 +20,36 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define(['./TypeRegistry', './Type'], function (TypeRegistry, Type) { - describe('The Type API', function () { - let typeRegistryInstance; +import TypeRegistry from './TypeRegistry'; - beforeEach(function () { - typeRegistryInstance = new TypeRegistry (); - typeRegistryInstance.addType('testType', { - name: 'Test Type', - description: 'This is a test type.', - creatable: true - }); - }); +describe('The Type API', function () { + let typeRegistryInstance; - it('types can be standardized', function () { - typeRegistryInstance.addType('standardizationTestType', { - label: 'Test Type', - description: 'This is a test type.', - creatable: true - }); - typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType); - expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined(); - expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type'); - }); - - it('new types are registered successfully and can be retrieved', function () { - expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type'); - }); - - it('type registry contains new keys', function () { - expect(typeRegistryInstance.listKeys ()).toContain('testType'); + beforeEach(function () { + typeRegistryInstance = new TypeRegistry (); + typeRegistryInstance.addType('testType', { + name: 'Test Type', + description: 'This is a test type.', + creatable: true }); }); + + it('types can be standardized', function () { + typeRegistryInstance.addType('standardizationTestType', { + label: 'Test Type', + description: 'This is a test type.', + creatable: true + }); + typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType); + expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined(); + expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type'); + }); + + it('new types are registered successfully and can be retrieved', function () { + expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type'); + }); + + it('type registry contains new keys', function () { + expect(typeRegistryInstance.listKeys ()).toContain('testType'); + }); }); diff --git a/src/plugins/plan/GanttChartCompositionPolicy.js b/src/plugins/plan/GanttChartCompositionPolicy.js new file mode 100644 index 0000000000..1c149bd25d --- /dev/null +++ b/src/plugins/plan/GanttChartCompositionPolicy.js @@ -0,0 +1,35 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ +const ALLOWED_TYPES = [ + 'plan' +]; + +export default function ganttChartCompositionPolicy(openmct) { + return function (parent, child) { + if (parent.type === 'gantt-chart') { + return ALLOWED_TYPES.includes(child.type); + } + + return true; + }; +} + diff --git a/src/plugins/plan/Plan.vue b/src/plugins/plan/Plan.vue deleted file mode 100644 index 5a28f61ac0..0000000000 --- a/src/plugins/plan/Plan.vue +++ /dev/null @@ -1,591 +0,0 @@ - - - - - diff --git a/src/plugins/plan/PlanViewConfiguration.js b/src/plugins/plan/PlanViewConfiguration.js new file mode 100644 index 0000000000..249d97be27 --- /dev/null +++ b/src/plugins/plan/PlanViewConfiguration.js @@ -0,0 +1,110 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +import EventEmitter from 'EventEmitter'; + +export const DEFAULT_CONFIGURATION = { + clipActivityNames: false, + swimlaneVisibility: {} +}; + +export default class PlanViewConfiguration extends EventEmitter { + constructor(domainObject, openmct) { + super(); + + this.domainObject = domainObject; + this.openmct = openmct; + + this.configurationChanged = this.configurationChanged.bind(this); + this.unlistenFromMutation = openmct.objects.observe(domainObject, 'configuration', this.configurationChanged); + } + + /** + * @returns {Object.} + */ + getConfiguration() { + const configuration = this.domainObject.configuration ?? {}; + for (const configKey of Object.keys(DEFAULT_CONFIGURATION)) { + configuration[configKey] = configuration[configKey] ?? DEFAULT_CONFIGURATION[configKey]; + } + + return configuration; + } + + #updateConfiguration(configuration) { + this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); + } + + /** + * @param {string} swimlaneName + * @param {boolean} isVisible + */ + setSwimlaneVisibility(swimlaneName, isVisible) { + const configuration = this.getConfiguration(); + const { swimlaneVisibility } = configuration; + swimlaneVisibility[swimlaneName] = isVisible; + this.#updateConfiguration(configuration); + } + + resetSwimlaneVisibility() { + const configuration = this.getConfiguration(); + const swimlaneVisibility = {}; + configuration.swimlaneVisibility = swimlaneVisibility; + this.#updateConfiguration(configuration); + } + + initializeSwimlaneVisibility(swimlaneNames) { + const configuration = this.getConfiguration(); + const { swimlaneVisibility } = configuration; + let shouldMutate = false; + for (const swimlaneName of swimlaneNames) { + if (swimlaneVisibility[swimlaneName] === undefined) { + swimlaneVisibility[swimlaneName] = true; + shouldMutate = true; + } + } + + if (shouldMutate) { + configuration.swimlaneVisibility = swimlaneVisibility; + this.#updateConfiguration(configuration); + } + } + + /** + * @param {boolean} isEnabled + */ + setClipActivityNames(isEnabled) { + const configuration = this.getConfiguration(); + configuration.clipActivityNames = isEnabled; + this.#updateConfiguration(configuration); + } + + configurationChanged(configuration) { + if (configuration !== undefined) { + this.emit('change', configuration); + } + } + + destroy() { + this.unlistenFromMutation(); + } +} diff --git a/src/plugins/plan/PlanViewProvider.js b/src/plugins/plan/PlanViewProvider.js index 7d8b237e65..dcf3ac1056 100644 --- a/src/plugins/plan/PlanViewProvider.js +++ b/src/plugins/plan/PlanViewProvider.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import Plan from './Plan.vue'; +import Plan from './components/Plan.vue'; import Vue from 'vue'; export default function PlanViewProvider(openmct) { @@ -35,11 +35,11 @@ export default function PlanViewProvider(openmct) { name: 'Plan', cssClass: 'icon-plan', canView(domainObject) { - return domainObject.type === 'plan'; + return domainObject.type === 'plan' || domainObject.type === 'gantt-chart'; }, canEdit(domainObject) { - return false; + return domainObject.type === 'gantt-chart'; }, view: function (domainObject, objectPath) { diff --git a/src/plugins/plan/components/ActivityTimeline.vue b/src/plugins/plan/components/ActivityTimeline.vue new file mode 100644 index 0000000000..ec0120d392 --- /dev/null +++ b/src/plugins/plan/components/ActivityTimeline.vue @@ -0,0 +1,187 @@ + + + + diff --git a/src/plugins/plan/components/Plan.vue b/src/plugins/plan/components/Plan.vue new file mode 100644 index 0000000000..fc7005b2e6 --- /dev/null +++ b/src/plugins/plan/components/Plan.vue @@ -0,0 +1,558 @@ +* 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. +*****************************************************************************/ + + + + diff --git a/src/plugins/plan/inspector/PlanInspectorViewProvider.js b/src/plugins/plan/inspector/ActivityInspectorViewProvider.js similarity index 90% rename from src/plugins/plan/inspector/PlanInspectorViewProvider.js rename to src/plugins/plan/inspector/ActivityInspectorViewProvider.js index 019125fb69..2dfb756911 100644 --- a/src/plugins/plan/inspector/PlanInspectorViewProvider.js +++ b/src/plugins/plan/inspector/ActivityInspectorViewProvider.js @@ -20,13 +20,13 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import PlanActivitiesView from "./PlanActivitiesView.vue"; +import PlanActivitiesView from "./components/PlanActivitiesView.vue"; import Vue from 'vue'; -export default function PlanInspectorViewProvider(openmct) { +export default function ActivityInspectorViewProvider(openmct) { return { - key: 'plan-inspector', - name: 'Plan Inspector View', + key: 'activity-inspector', + name: 'Activity', canView: function (selection) { if (selection.length === 0 || selection[0].length === 0) { return false; @@ -44,6 +44,7 @@ export default function PlanInspectorViewProvider(openmct) { show: function (element) { component = new Vue({ el: element, + name: "PlanActivitiesView", components: { PlanActivitiesView: PlanActivitiesView }, diff --git a/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js b/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js new file mode 100644 index 0000000000..56f66b4a5b --- /dev/null +++ b/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js @@ -0,0 +1,68 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +import PlanViewConfiguration from './components/PlanViewConfiguration.vue'; +import Vue from 'vue'; + +export default function GanttChartInspectorViewProvider(openmct) { + return { + key: 'plan-inspector', + name: 'Config', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } + + const domainObject = selection[0][0].context.item; + + return domainObject?.type === 'gantt-chart'; + }, + view: function (selection) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + PlanViewConfiguration + }, + provide: { + openmct, + selection: selection + }, + template: '' + }); + }, + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } + } + }; + } + }; +} diff --git a/src/plugins/plan/inspector/ActivityProperty.vue b/src/plugins/plan/inspector/components/ActivityProperty.vue similarity index 100% rename from src/plugins/plan/inspector/ActivityProperty.vue rename to src/plugins/plan/inspector/components/ActivityProperty.vue diff --git a/src/plugins/plan/inspector/PlanActivitiesView.vue b/src/plugins/plan/inspector/components/PlanActivitiesView.vue similarity index 96% rename from src/plugins/plan/inspector/PlanActivitiesView.vue rename to src/plugins/plan/inspector/components/PlanActivitiesView.vue index 29ae522c35..8e7b5c1641 100644 --- a/src/plugins/plan/inspector/PlanActivitiesView.vue +++ b/src/plugins/plan/inspector/components/PlanActivitiesView.vue @@ -36,16 +36,15 @@ import { getPreciseDuration } from "utils/duration"; import { v4 as uuid } from 'uuid'; const propertyLabels = { - 'start': 'Start DateTime', - 'end': 'End DateTime', - 'duration': 'Duration', - 'earliestStart': 'Earliest Start', - 'latestEnd': 'Latest End', - 'gap': 'Gap', - 'overlap': 'Overlap', - 'totalTime': 'Total Time' + start: 'Start DateTime', + end: 'End DateTime', + duration: 'Duration', + earliestStart: 'Earliest Start', + latestEnd: 'Latest End', + gap: 'Gap', + overlap: 'Overlap', + totalTime: 'Total Time' }; - export default { components: { PlanActivityView diff --git a/src/plugins/plan/inspector/PlanActivityView.vue b/src/plugins/plan/inspector/components/PlanActivityView.vue similarity index 100% rename from src/plugins/plan/inspector/PlanActivityView.vue rename to src/plugins/plan/inspector/components/PlanActivityView.vue diff --git a/src/plugins/plan/inspector/components/PlanViewConfiguration.vue b/src/plugins/plan/inspector/components/PlanViewConfiguration.vue new file mode 100644 index 0000000000..786e97ed4b --- /dev/null +++ b/src/plugins/plan/inspector/components/PlanViewConfiguration.vue @@ -0,0 +1,144 @@ + + + diff --git a/src/plugins/plan/plan.scss b/src/plugins/plan/plan.scss index 45bd9b2938..a582260fa9 100644 --- a/src/plugins/plan/plan.scss +++ b/src/plugins/plan/plan.scss @@ -21,21 +21,34 @@ *****************************************************************************/ .c-plan { - svg { - text-rendering: geometricPrecision; + svg { + text-rendering: geometricPrecision; - text { - stroke: none; + text { + stroke: none; + } } - .activity-label { - &--outside-rect { - fill: $colorBodyFg !important; - } - } - } + &__activity { + cursor: pointer; - canvas { - display: none; - } + &[s-selected] { + rect, use { + outline-style: dotted; + outline-width: 2px; + stroke: $colorGanttSelectedBorder; + stroke-width: 2px; + } + } + } + + &__activity-label { + &--outside-rect { + fill: $colorBodyFg !important; + } + } + + canvas { + display: none; + } } diff --git a/src/plugins/plan/plugin.js b/src/plugins/plan/plugin.js index d44a267da8..85022fd61b 100644 --- a/src/plugins/plan/plugin.js +++ b/src/plugins/plan/plugin.js @@ -21,15 +21,18 @@ *****************************************************************************/ import PlanViewProvider from './PlanViewProvider'; -import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider"; +import ActivityInspectorViewProvider from "./inspector/ActivityInspectorViewProvider"; +import GanttChartInspectorViewProvider from "./inspector/GanttChartInspectorViewProvider"; +import ganttChartCompositionPolicy from './GanttChartCompositionPolicy'; +import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration'; -export default function (configuration) { +export default function (options = {}) { return function install(openmct) { openmct.types.addType('plan', { name: 'Plan', key: 'plan', - description: 'A configurable timeline-like view for a compatible mission plan file.', - creatable: true, + description: 'A non-configurable timeline-like view for a compatible plan file.', + creatable: options.creatable ?? false, cssClass: 'icon-plan', form: [ { @@ -45,10 +48,30 @@ export default function (configuration) { } ], initialize: function (domainObject) { + domainObject.configuration = { + clipActivityNames: DEFAULT_CONFIGURATION.clipActivityNames + }; + } + }); + // Name TBD and subject to change + openmct.types.addType('gantt-chart', { + name: 'Gantt Chart', + key: 'gantt-chart', + description: 'A configurable timeline-like view for a compatible plan file.', + creatable: true, + cssClass: 'icon-plan', + form: [], + initialize(domainObject) { + domainObject.configuration = { + clipActivityNames: true + }; + domainObject.composition = []; } }); openmct.objectViews.addProvider(new PlanViewProvider(openmct)); - openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct)); + openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct)); + openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct)); + openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct)); }; } diff --git a/src/plugins/plan/pluginSpec.js b/src/plugins/plan/pluginSpec.js index e7603ee24a..25db86289f 100644 --- a/src/plugins/plan/pluginSpec.js +++ b/src/plugins/plan/pluginSpec.js @@ -27,6 +27,7 @@ import Properties from "../inspectorViews/properties/Properties.vue"; describe('the plugin', function () { let planDefinition; + let ganttDefinition; let element; let child; let openmct; @@ -50,6 +51,7 @@ describe('the plugin', function () { openmct.install(new PlanPlugin()); planDefinition = openmct.types.get('plan').definition; + ganttDefinition = openmct.types.get('gantt-chart').definition; element = document.createElement('div'); element.style.width = '640px'; @@ -74,15 +76,30 @@ describe('the plugin', function () { let mockPlanObject = { name: 'Plan', key: 'plan', + creatable: false + }; + + let mockGanttObject = { + name: 'Gantt', + key: 'gantt-chart', creatable: true }; - it('defines a plan object type with the correct key', () => { - expect(planDefinition.key).toEqual(mockPlanObject.key); + describe('the plan type', () => { + it('defines a plan object type with the correct key', () => { + expect(planDefinition.key).toEqual(mockPlanObject.key); + }); + it('is not creatable', () => { + expect(planDefinition.creatable).toEqual(mockPlanObject.creatable); + }); }); - - it('is creatable', () => { - expect(planDefinition.creatable).toEqual(mockPlanObject.creatable); + describe('the gantt-chart type', () => { + it('defines a gantt-chart object type with the correct key', () => { + expect(ganttDefinition.key).toEqual(mockGanttObject.key); + }); + it('is creatable', () => { + expect(ganttDefinition.creatable).toEqual(mockGanttObject.creatable); + }); }); describe('the plan view', () => { @@ -107,7 +124,7 @@ describe('the plugin', function () { const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); - expect(planView.canEdit()).toBeFalse(); + expect(planView.canEdit(testViewObject)).toBeFalse(); }); }); @@ -179,10 +196,10 @@ describe('the plugin', function () { it('displays the group label', () => { const labelEl = element.querySelector('.c-plan__contents .c-object-label .c-object-label__name'); - expect(labelEl.innerHTML).toEqual('TEST-GROUP'); + expect(labelEl.innerHTML).toMatch(/TEST-GROUP/); }); - it('displays the activities and their labels', (done) => { + it('displays the activities and their labels', async () => { const bounds = { start: 1597160002854, end: 1597181232854 @@ -190,27 +207,22 @@ describe('the plugin', function () { openmct.time.bounds(bounds); - Vue.nextTick(() => { - const rectEls = element.querySelectorAll('.c-plan__contents rect'); - expect(rectEls.length).toEqual(2); - const textEls = element.querySelectorAll('.c-plan__contents text'); - expect(textEls.length).toEqual(3); - - done(); - }); + await Vue.nextTick(); + const rectEls = element.querySelectorAll('.c-plan__contents use'); + expect(rectEls.length).toEqual(2); + const textEls = element.querySelectorAll('.c-plan__contents text'); + expect(textEls.length).toEqual(3); }); - it ('shows the status indicator when available', (done) => { + it ('shows the status indicator when available', async () => { openmct.status.set({ key: "test-object", namespace: '' }, 'draft'); - Vue.nextTick(() => { - const statusEl = element.querySelector('.c-plan__contents .is-status--draft'); - expect(statusEl).toBeDefined(); - done(); - }); + await Vue.nextTick(); + const statusEl = element.querySelector('.c-plan__contents .is-status--draft'); + expect(statusEl).toBeDefined(); }); }); @@ -224,10 +236,12 @@ describe('the plugin', function () { key: 'test-plan', namespace: '' }, + created: 123456789, + modified: 123456790, version: 'v1' }; - beforeEach(() => { + beforeEach(async () => { openmct.selection.select([{ element: element, context: { @@ -241,19 +255,18 @@ describe('the plugin', function () { } }], false); - return Vue.nextTick().then(() => { - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - Properties - }, - provide: { - openmct: openmct - }, - template: '' - }); + await Vue.nextTick(); + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + Properties + }, + provide: { + openmct: openmct + }, + template: '' }); }); @@ -264,7 +277,6 @@ describe('the plugin', function () { it('provides an inspector view with the version information if available', () => { componentObject = component.$root.$children[0]; const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row'); - expect(propertiesEls.length).toEqual(7); const found = Array.from(propertiesEls).some((propertyEl) => { return (propertyEl.children[0].innerHTML.trim() === 'Version' && propertyEl.children[1].innerHTML.trim() === 'v1'); diff --git a/src/plugins/plan/util.js b/src/plugins/plan/util.js index 5b3934bd79..27cbcb5491 100644 --- a/src/plugins/plan/util.js +++ b/src/plugins/plan/util.js @@ -21,8 +21,8 @@ *****************************************************************************/ export function getValidatedData(domainObject) { - let sourceMap = domainObject.sourceMap; - let body = domainObject.selectFile?.body; + const sourceMap = domainObject.sourceMap; + const body = domainObject.selectFile?.body; let json = {}; if (typeof body === 'string') { try { @@ -64,3 +64,27 @@ export function getValidatedData(domainObject) { return json; } } + +export function getContrastingColor(hexColor) { + function cutHex(h, start, end) { + const hStr = (h.charAt(0) === '#') ? h.substring(1, 7) : h; + + return parseInt(hStr.substring(start, end), 16); + } + + // https://codepen.io/davidhalford/pen/ywEva/ + const cThreshold = 130; + + if (hexColor.indexOf('#') === -1) { + // We weren't given a hex color + return "#ff0000"; + } + + const hR = cutHex(hexColor, 0, 2); + const hG = cutHex(hexColor, 2, 4); + const hB = cutHex(hexColor, 4, 6); + + const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000; + + return cBrightness > cThreshold ? "#000000" : "#ffffff"; +} diff --git a/src/plugins/timeline/TimelineCompositionPolicy.js b/src/plugins/timeline/TimelineCompositionPolicy.js index 49f7d5ed8a..4d2dc675e9 100644 --- a/src/plugins/timeline/TimelineCompositionPolicy.js +++ b/src/plugins/timeline/TimelineCompositionPolicy.js @@ -19,10 +19,12 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ + const ALLOWED_TYPES = [ 'telemetry.plot.overlay', 'telemetry.plot.stacked', - 'plan' + 'plan', + 'gantt-chart' ]; const DISALLOWED_TYPES = [ 'telemetry.plot.bar-graph', diff --git a/src/plugins/timeline/TimelineObjectView.vue b/src/plugins/timeline/TimelineObjectView.vue index 039ac3e4eb..6855ac22c1 100644 --- a/src/plugins/timeline/TimelineObjectView.vue +++ b/src/plugins/timeline/TimelineObjectView.vue @@ -19,12 +19,13 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +