mirror of
https://github.com/nasa/openmct.git
synced 2025-06-20 08:03:49 +00:00
feat: configurable Plan Views for reducing vertical scroll distance (#6415)
* refactor: convert Type API to ES6 module - Another AMD module bites the dust 🧹 * feat: add initial configurable plan type - Name change TBD * feat: add `clipActivityNames` property - refactor: initialize data to `null` * refactor: general code cleanup * feat(WIP): name clipping via clipPath elements * feat: compose a Gantt Chart using a Plan - Allows Plans to be dragged into Gantt Charts (name tentative) to create a configurable Activity View - Clip/Unclip activity names by editing domainObject property * feat: replace Plan if another is dragged in - SImilar to Gauges or Scatter Plots, launch a confirmation dialog to replace the existing Plan with another, if another Plan is dragged into the chart. * test: fix tests, add basic tests for gantt * tes(e2e): fix plan test * docs: add TODO * refactor: clean up more string literals * style: remove `rx`, increase min width - round widths to nearest integer * refactor: extract timeline creation logic - extracts the logic for creating the timeline into its own component, `ActivityTimeline.vue`. This will save us a lot of re-renders, as we were manually creating elements / clearing them on each tick * style: fix text y-pos and don't round * fix: make activities clickable again * docs: add copyright docs * feat: swimlane visibility - configure plan view from inspector fix: update plans when file changes - fix gantt chart display in time strips - code cleanup * fix: gantt chart embed in time strip * remove viewBox for now * fix: make `clipPath` ids more unique * refactor: more code cleanup * refactor: more code cleanup * test: fix existing Plan unit tests * refactor: rename variables * fix: respond to code review comments - Move config manipulation to PlanViewConfiguration.js/.vue - Variable renames, code refactoring * fix: unique, reproducible clipPathIds * fix: only mutate swimlaneVisibility once on init * fix: really make clipPathId unique this time * refactor: use default config * Closes #6113 - Refined CSS class naming and application. - Set cursor to pointer for Activity elements. - Added <title> node to Activity elements. - Styling for selected Activities. - Better Inspector tab name. * fix: make Plan creatability configurable and false by default * test: fix existing tests and add a couple new ones * Closes #6113 - Now uses SVG <symbol> instead of rect within Activity element. - Passes in `rowHeight` as a prop from Plan.vue. - SWIMLANE_PADDING const added and used to create margin at top and bottom edges of swimlanes. - Refined styling for selected activities. - New `$colorGanttSelectedBorder` theme constant. - Smoke tested in Espresso and Snow themes. * fix: default swimlaneWidth to clientWidth * test: fix test * feat: display selected activity name as header * fix: remove redundant listener * refactor: move `examplePlans.js` into `test-data/` * docs: remove copyright header * refactor: move `helper.js` into `helper/` * refactor: `helper.js` -> `planningUtils.js` * fix: update pathing * test: add tests for gantt/plan - add visual tests for gantt / plan - add test for clicking a single activity and verifying its contents in the inspector --------- Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
This commit is contained in:
@ -159,24 +159,26 @@ async function expandTreePaneItemByName(page, name) {
|
|||||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||||
*/
|
*/
|
||||||
async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
||||||
|
if (!name) {
|
||||||
|
name = `Plan:${genUuid()}`;
|
||||||
|
}
|
||||||
|
|
||||||
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||||
|
|
||||||
// Navigate to the parent object. This is necessary to create the object
|
// Navigate to the parent object. This is necessary to create the object
|
||||||
// in the correct location, such as a folder, layout, or plot.
|
// in the correct location, such as a folder, layout, or plot.
|
||||||
await page.goto(`${parentUrl}?hideTree=true`);
|
await page.goto(`${parentUrl}?hideTree=true`);
|
||||||
|
|
||||||
//Click the Create button
|
// Click the Create button
|
||||||
await page.click('button:has-text("Create")');
|
await page.click('button:has-text("Create")');
|
||||||
|
|
||||||
// Click 'Plan' menu option
|
// Click 'Plan' menu option
|
||||||
await page.click(`li:text("Plan")`);
|
await page.click(`li:text("Plan")`);
|
||||||
|
|
||||||
// Modify the name input field of the domain object to accept 'name'
|
// 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"]');
|
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||||
await nameInput.fill("");
|
await nameInput.fill("");
|
||||||
await nameInput.fill(name);
|
await nameInput.fill(name);
|
||||||
}
|
|
||||||
|
|
||||||
// Upload buffer from memory
|
// Upload buffer from memory
|
||||||
await page.locator('input#fileElem').setInputFiles({
|
await page.locator('input#fileElem').setInputFiles({
|
||||||
@ -194,7 +196,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Wait until the URL is updated
|
// Wait until the URL is updated
|
||||||
await page.waitForURL(`**/mine/*`);
|
await page.waitForURL(`**/${parent}/*`);
|
||||||
const uuid = await getFocusedObjectUuid(page);
|
const uuid = await getFocusedObjectUuid(page);
|
||||||
const objectUrl = await getHashUrlToDomainObject(page, uuid);
|
const objectUrl = await getHashUrlToDomainObject(page, uuid);
|
||||||
|
|
||||||
|
92
e2e/helper/planningUtils.js
Normal file
92
e2e/helper/planningUtils.js
Normal file
@ -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`);
|
||||||
|
}
|
1080
e2e/test-data/examplePlans/ExamplePlan_Large.json
Normal file
1080
e2e/test-data/examplePlans/ExamplePlan_Large.json
Normal file
File diff suppressed because it is too large
Load Diff
44
e2e/test-data/examplePlans/ExamplePlan_Small1.json
Normal file
44
e2e/test-data/examplePlans/ExamplePlan_Small1.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
38
e2e/test-data/examplePlans/ExamplePlan_Small2.json
Normal file
38
e2e/test-data/examplePlans/ExamplePlan_Small2.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
85
e2e/tests/functional/planning/ganttChart.e2e.spec.js
Normal file
85
e2e/tests/functional/planning/ganttChart.e2e.spec.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -19,69 +19,21 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
const { test, expect } = require('../../../pluginFixtures');
|
const { test } = require('../../../pluginFixtures');
|
||||||
const { createPlanFromJSON } = require('../../../appActions');
|
const { createPlanFromJSON } = require('../../../appActions');
|
||||||
|
const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json');
|
||||||
const testPlan = {
|
const { assertPlanActivities } = require('../../../helper/planningUtils');
|
||||||
"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("Plan", () => {
|
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' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
plan = await createPlanFromJSON(page, {
|
||||||
const plan = await createPlanFromJSON(page, {
|
json: testPlan1
|
||||||
name: 'Test Plan',
|
});
|
||||||
json: testPlan
|
|
||||||
});
|
});
|
||||||
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
|
test("Displays all plan events", async ({ page }) => {
|
||||||
await page.goto(`${plan.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`);
|
await assertPlanActivities(page, testPlan1, plan.url);
|
||||||
const eventCount = await page.locator('.activity-bounds').count();
|
|
||||||
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
52
e2e/tests/functional/planning/plan.visual.spec.js
Normal file
52
e2e/tests/functional/planning/plan.visual.spec.js
Normal file
@ -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})`);
|
||||||
|
});
|
||||||
|
});
|
@ -84,7 +84,9 @@
|
|||||||
|
|
||||||
openmct.install(openmct.plugins.Espresso());
|
openmct.install(openmct.plugins.Espresso());
|
||||||
openmct.install(openmct.plugins.MyItems());
|
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.Timeline());
|
||||||
openmct.install(openmct.plugins.Hyperlink());
|
openmct.install(openmct.plugins.Hyperlink());
|
||||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||||
|
@ -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() {
|
isEditing() {
|
||||||
return this.editing;
|
return this.editing;
|
||||||
|
@ -71,7 +71,7 @@ function (
|
|||||||
StatusAPI: StatusAPI.default,
|
StatusAPI: StatusAPI.default,
|
||||||
TelemetryAPI: TelemetryAPI,
|
TelemetryAPI: TelemetryAPI,
|
||||||
TimeAPI: TimeAPI.default,
|
TimeAPI: TimeAPI.default,
|
||||||
TypeRegistry: TypeRegistry,
|
TypeRegistry: TypeRegistry.default,
|
||||||
UserAPI: UserAPI.default,
|
UserAPI: UserAPI.default,
|
||||||
AnnotationAPI: AnnotationAPI.default
|
AnnotationAPI: AnnotationAPI.default
|
||||||
};
|
};
|
||||||
|
@ -62,6 +62,8 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
|||||||
* @property {Identifier[]} [composition] if
|
* @property {Identifier[]} [composition] if
|
||||||
* present, this will be used by the default composition provider
|
* present, this will be used by the default composition provider
|
||||||
* to load domain objects
|
* to load domain objects
|
||||||
|
* @property {Object.<string, any>} [configuration] A key-value map containing configuration
|
||||||
|
* settings for this domain object.
|
||||||
* @memberof module:openmct.ObjectAPI~
|
* @memberof module:openmct.ObjectAPI~
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -20,9 +20,7 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(function () {
|
/**
|
||||||
|
|
||||||
/**
|
|
||||||
* A Type describes a kind of domain object that may appear or be
|
* A Type describes a kind of domain object that may appear or be
|
||||||
* created within Open MCT.
|
* created within Open MCT.
|
||||||
*
|
*
|
||||||
@ -30,53 +28,17 @@ define(function () {
|
|||||||
* @class Type
|
* @class Type
|
||||||
* @memberof module:openmct
|
* @memberof module:openmct
|
||||||
*/
|
*/
|
||||||
function Type(definition) {
|
export default class Type {
|
||||||
|
constructor(definition) {
|
||||||
this.definition = definition;
|
this.definition = definition;
|
||||||
if (definition.key) {
|
if (definition.key) {
|
||||||
this.key = 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.
|
* Create a type definition from a legacy definition.
|
||||||
*/
|
*/
|
||||||
Type.definitionFromLegacyDefinition = function (legacyDefinition) {
|
static definitionFromLegacyDefinition(legacyDefinition) {
|
||||||
let definition = {};
|
let definition = {};
|
||||||
definition.name = legacyDefinition.name;
|
definition.name = legacyDefinition.name;
|
||||||
definition.cssClass = legacyDefinition.cssClass;
|
definition.cssClass = legacyDefinition.cssClass;
|
||||||
@ -121,7 +83,39 @@ define(function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return definition;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,14 +19,15 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
define(['./Type'], function (Type) {
|
import Type from './Type';
|
||||||
const UNKNOWN_TYPE = new Type({
|
|
||||||
|
const UNKNOWN_TYPE = new Type({
|
||||||
key: "unknown",
|
key: "unknown",
|
||||||
name: "Unknown Type",
|
name: "Unknown Type",
|
||||||
cssClass: "icon-object-unknown"
|
cssClass: "icon-object-unknown"
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef TypeDefinition
|
* @typedef TypeDefinition
|
||||||
* @memberof module:openmct.TypeRegistry~
|
* @memberof module:openmct.TypeRegistry~
|
||||||
* @property {string} label the name for this type of object
|
* @property {string} label the name for this type of object
|
||||||
@ -38,16 +39,16 @@ define(['./Type'], function (Type) {
|
|||||||
* @property {string} [cssClass] the CSS class to apply for icons
|
* @property {string} [cssClass] the CSS class to apply for icons
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A TypeRegistry maintains the definitions for different types
|
* A TypeRegistry maintains the definitions for different types
|
||||||
* that domain objects may have.
|
* that domain objects may have.
|
||||||
* @interface TypeRegistry
|
* @interface TypeRegistry
|
||||||
* @memberof module:openmct
|
* @memberof module:openmct
|
||||||
*/
|
*/
|
||||||
function TypeRegistry() {
|
export default class TypeRegistry {
|
||||||
|
constructor() {
|
||||||
this.types = {};
|
this.types = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new object type.
|
* Register a new object type.
|
||||||
*
|
*
|
||||||
@ -56,17 +57,16 @@ define(['./Type'], function (Type) {
|
|||||||
* @method addType
|
* @method addType
|
||||||
* @memberof module:openmct.TypeRegistry#
|
* @memberof module:openmct.TypeRegistry#
|
||||||
*/
|
*/
|
||||||
TypeRegistry.prototype.addType = function (typeKey, typeDef) {
|
addType(typeKey, typeDef) {
|
||||||
this.standardizeType(typeDef);
|
this.standardizeType(typeDef);
|
||||||
this.types[typeKey] = new Type(typeDef);
|
this.types[typeKey] = new Type(typeDef);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a typeDef, standardizes it, and logs warnings about unsupported
|
* Takes a typeDef, standardizes it, and logs warnings about unsupported
|
||||||
* usage.
|
* usage.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
TypeRegistry.prototype.standardizeType = function (typeDef) {
|
standardizeType(typeDef) {
|
||||||
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
|
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
|
||||||
if (!typeDef.name) {
|
if (!typeDef.name) {
|
||||||
typeDef.name = typeDef.label;
|
typeDef.name = typeDef.label;
|
||||||
@ -74,18 +74,16 @@ define(['./Type'], function (Type) {
|
|||||||
|
|
||||||
delete typeDef.label;
|
delete typeDef.label;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List keys for all registered types.
|
* List keys for all registered types.
|
||||||
* @method listKeys
|
* @method listKeys
|
||||||
* @memberof module:openmct.TypeRegistry#
|
* @memberof module:openmct.TypeRegistry#
|
||||||
* @returns {string[]} all registered type keys
|
* @returns {string[]} all registered type keys
|
||||||
*/
|
*/
|
||||||
TypeRegistry.prototype.listKeys = function () {
|
listKeys() {
|
||||||
return Object.keys(this.types);
|
return Object.keys(this.types);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a registered type by its key.
|
* Retrieve a registered type by its key.
|
||||||
* @method get
|
* @method get
|
||||||
@ -93,18 +91,15 @@ define(['./Type'], function (Type) {
|
|||||||
* @memberof module:openmct.TypeRegistry#
|
* @memberof module:openmct.TypeRegistry#
|
||||||
* @returns {module:openmct.Type} the registered type
|
* @returns {module:openmct.Type} the registered type
|
||||||
*/
|
*/
|
||||||
TypeRegistry.prototype.get = function (typeKey) {
|
get(typeKey) {
|
||||||
return this.types[typeKey] || UNKNOWN_TYPE;
|
return this.types[typeKey] || UNKNOWN_TYPE;
|
||||||
};
|
}
|
||||||
|
importLegacyTypes(types) {
|
||||||
TypeRegistry.prototype.importLegacyTypes = function (types) {
|
|
||||||
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
|
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
|
||||||
.forEach((type) => {
|
.forEach((type) => {
|
||||||
let def = Type.definitionFromLegacyDefinition(type);
|
let def = Type.definitionFromLegacyDefinition(type);
|
||||||
this.addType(type.key, def);
|
this.addType(type.key, def);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
}
|
||||||
return TypeRegistry;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
@ -20,8 +20,9 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(['./TypeRegistry', './Type'], function (TypeRegistry, Type) {
|
import TypeRegistry from './TypeRegistry';
|
||||||
describe('The Type API', function () {
|
|
||||||
|
describe('The Type API', function () {
|
||||||
let typeRegistryInstance;
|
let typeRegistryInstance;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
@ -51,5 +52,4 @@ define(['./TypeRegistry', './Type'], function (TypeRegistry, Type) {
|
|||||||
it('type registry contains new keys', function () {
|
it('type registry contains new keys', function () {
|
||||||
expect(typeRegistryInstance.listKeys ()).toContain('testType');
|
expect(typeRegistryInstance.listKeys ()).toContain('testType');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
35
src/plugins/plan/GanttChartCompositionPolicy.js
Normal file
35
src/plugins/plan/GanttChartCompositionPolicy.js
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,591 +0,0 @@
|
|||||||
<!--
|
|
||||||
Open MCT, Copyright (c) 2014-2023, United States Government
|
|
||||||
as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
Administration. All rights reserved.
|
|
||||||
|
|
||||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
"License"); you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
License for the specific language governing permissions and limitations
|
|
||||||
under the License.
|
|
||||||
|
|
||||||
Open MCT includes source code licensed under additional open source
|
|
||||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
this source code distribution or the Licensing information page available
|
|
||||||
at runtime from the About dialog for additional information.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="plan"
|
|
||||||
class="c-plan c-timeline-holder"
|
|
||||||
>
|
|
||||||
<template v-if="viewBounds && !options.compact">
|
|
||||||
<swim-lane>
|
|
||||||
<template slot="label">{{ timeSystem.name }}</template>
|
|
||||||
<timeline-axis
|
|
||||||
slot="object"
|
|
||||||
:bounds="viewBounds"
|
|
||||||
:time-system="timeSystem"
|
|
||||||
:content-height="height"
|
|
||||||
:rendering-engine="renderingEngine"
|
|
||||||
/>
|
|
||||||
</swim-lane>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
ref="planHolder"
|
|
||||||
class="c-plan__contents u-contents"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import * as d3Scale from 'd3-scale';
|
|
||||||
import TimelineAxis from "../../ui/components/TimeSystemAxis.vue";
|
|
||||||
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
|
|
||||||
import { getValidatedData } from "./util";
|
|
||||||
import Vue from "vue";
|
|
||||||
|
|
||||||
const PADDING = 1;
|
|
||||||
const OUTER_TEXT_PADDING = 12;
|
|
||||||
const INNER_TEXT_PADDING = 17;
|
|
||||||
const TEXT_LEFT_PADDING = 5;
|
|
||||||
const ROW_PADDING = 12;
|
|
||||||
const RESIZE_POLL_INTERVAL = 200;
|
|
||||||
const ROW_HEIGHT = 25;
|
|
||||||
const LINE_HEIGHT = 12;
|
|
||||||
const MAX_TEXT_WIDTH = 300;
|
|
||||||
const EDGE_ROUNDING = 5;
|
|
||||||
const DEFAULT_COLOR = '#cc9922';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
TimelineAxis,
|
|
||||||
SwimLane
|
|
||||||
},
|
|
||||||
inject: ['openmct', 'domainObject', 'path'],
|
|
||||||
props: {
|
|
||||||
options: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {
|
|
||||||
compact: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderingEngine: {
|
|
||||||
type: String,
|
|
||||||
default() {
|
|
||||||
return 'svg';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
viewBounds: undefined,
|
|
||||||
timeSystem: undefined,
|
|
||||||
height: 0
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getPlanData(this.domainObject);
|
|
||||||
|
|
||||||
this.canvas = this.$refs.plan.appendChild(document.createElement('canvas'));
|
|
||||||
this.canvas.height = 0;
|
|
||||||
this.canvasContext = this.canvas.getContext('2d');
|
|
||||||
|
|
||||||
this.setDimensions();
|
|
||||||
this.setTimeContext();
|
|
||||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
|
||||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
|
||||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
|
|
||||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
clearInterval(this.resizeTimer);
|
|
||||||
this.stopFollowingTimeContext();
|
|
||||||
if (this.unlisten) {
|
|
||||||
this.unlisten();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.removeStatusListener) {
|
|
||||||
this.removeStatusListener();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setTimeContext() {
|
|
||||||
this.stopFollowingTimeContext();
|
|
||||||
this.timeContext = this.openmct.time.getContextForView(this.path);
|
|
||||||
this.followTimeContext();
|
|
||||||
},
|
|
||||||
followTimeContext() {
|
|
||||||
this.updateViewBounds(this.timeContext.bounds());
|
|
||||||
|
|
||||||
this.timeContext.on("timeSystem", this.setScaleAndPlotActivities);
|
|
||||||
this.timeContext.on("bounds", this.updateViewBounds);
|
|
||||||
this.timeContext.on("clock", this.updateBounds);
|
|
||||||
},
|
|
||||||
stopFollowingTimeContext() {
|
|
||||||
if (this.timeContext) {
|
|
||||||
this.timeContext.off("timeSystem", this.setScaleAndPlotActivities);
|
|
||||||
this.timeContext.off("bounds", this.updateViewBounds);
|
|
||||||
this.timeContext.off("clock", this.updateBounds);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
observeForChanges(mutatedObject) {
|
|
||||||
this.getPlanData(mutatedObject);
|
|
||||||
this.setScaleAndPlotActivities();
|
|
||||||
},
|
|
||||||
resize() {
|
|
||||||
let clientWidth = this.getClientWidth();
|
|
||||||
let clientHeight = this.getClientHeight();
|
|
||||||
if (clientWidth !== this.width) {
|
|
||||||
this.setDimensions();
|
|
||||||
this.updateViewBounds();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientHeight !== this.height) {
|
|
||||||
this.setDimensions();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getClientWidth() {
|
|
||||||
let clientWidth = this.$refs.plan.clientWidth;
|
|
||||||
|
|
||||||
if (!clientWidth) {
|
|
||||||
//this is a hack - need a better way to find the parent of this component
|
|
||||||
let parent = this.openmct.layout.$refs.browseObject.$el;
|
|
||||||
if (parent) {
|
|
||||||
clientWidth = parent.getBoundingClientRect().width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientWidth - 200;
|
|
||||||
},
|
|
||||||
getClientHeight() {
|
|
||||||
let clientHeight = this.$refs.plan.clientHeight;
|
|
||||||
|
|
||||||
if (!clientHeight) {
|
|
||||||
//this is a hack - need a better way to find the parent of this component
|
|
||||||
let parent = this.openmct.layout.$refs.browseObject.$el;
|
|
||||||
if (parent) {
|
|
||||||
clientHeight = parent.getBoundingClientRect().height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientHeight;
|
|
||||||
},
|
|
||||||
getPlanData(domainObject) {
|
|
||||||
this.planData = getValidatedData(domainObject);
|
|
||||||
},
|
|
||||||
updateBounds(clock) {
|
|
||||||
if (clock === undefined) {
|
|
||||||
this.viewBounds = Object.create(this.timeContext.bounds());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateViewBounds(bounds) {
|
|
||||||
if (bounds) {
|
|
||||||
this.viewBounds = Object.create(bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.timeSystem === undefined) {
|
|
||||||
this.timeSystem = this.openmct.time.timeSystem();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setScaleAndPlotActivities();
|
|
||||||
},
|
|
||||||
setScaleAndPlotActivities(timeSystem) {
|
|
||||||
if (timeSystem !== undefined) {
|
|
||||||
this.timeSystem = timeSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setScale(this.timeSystem);
|
|
||||||
this.clearPreviousActivities();
|
|
||||||
if (this.xScale) {
|
|
||||||
this.calculatePlanLayout();
|
|
||||||
this.drawPlan();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clearPreviousActivities() {
|
|
||||||
let activities = this.$el.querySelectorAll(".c-plan__contents > div");
|
|
||||||
activities.forEach(activity => activity.remove());
|
|
||||||
},
|
|
||||||
setDimensions() {
|
|
||||||
this.width = this.getClientWidth();
|
|
||||||
this.height = this.getClientHeight();
|
|
||||||
},
|
|
||||||
setScale(timeSystem) {
|
|
||||||
if (!this.width) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeSystem === undefined) {
|
|
||||||
timeSystem = this.openmct.time.timeSystem();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeSystem.isUTCBased) {
|
|
||||||
this.xScale = d3Scale.scaleUtc();
|
|
||||||
this.xScale.domain(
|
|
||||||
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.xScale = d3Scale.scaleLinear();
|
|
||||||
this.xScale.domain(
|
|
||||||
[this.viewBounds.start, this.viewBounds.end]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
|
||||||
},
|
|
||||||
isActivityInBounds(activity) {
|
|
||||||
return (activity.start < this.viewBounds.end) && (activity.end > this.viewBounds.start);
|
|
||||||
},
|
|
||||||
getTextWidth(name) {
|
|
||||||
let metrics = this.canvasContext.measureText(name);
|
|
||||||
|
|
||||||
return parseInt(metrics.width, 10);
|
|
||||||
},
|
|
||||||
sortFn(a, b) {
|
|
||||||
const numA = parseInt(a, 10);
|
|
||||||
const numB = parseInt(b, 10);
|
|
||||||
if (numA > numB) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numA < numB) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
// Get the row where the next activity will land.
|
|
||||||
getRowForActivity(rectX, width, activitiesByRow) {
|
|
||||||
let currentRow;
|
|
||||||
let sortedActivityRows = Object.keys(activitiesByRow).sort(this.sortFn);
|
|
||||||
|
|
||||||
function getOverlap(rects) {
|
|
||||||
return rects.every(rect => {
|
|
||||||
const { start, end } = rect;
|
|
||||||
const calculatedEnd = rectX + width;
|
|
||||||
const hasOverlap = (rectX >= start && rectX <= end) || (calculatedEnd >= start && calculatedEnd <= end) || (rectX <= start && calculatedEnd >= end);
|
|
||||||
|
|
||||||
return !hasOverlap;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < sortedActivityRows.length; i++) {
|
|
||||||
let row = sortedActivityRows[i];
|
|
||||||
if (getOverlap(activitiesByRow[row])) {
|
|
||||||
currentRow = row;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentRow === undefined && sortedActivityRows.length) {
|
|
||||||
let row = parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10);
|
|
||||||
currentRow = row + ROW_HEIGHT + ROW_PADDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (currentRow || 0);
|
|
||||||
},
|
|
||||||
calculatePlanLayout() {
|
|
||||||
let groups = Object.keys(this.planData);
|
|
||||||
this.groupActivities = {};
|
|
||||||
|
|
||||||
groups.forEach((key, index) => {
|
|
||||||
let activitiesByRow = {};
|
|
||||||
let currentRow = 0;
|
|
||||||
|
|
||||||
let activities = this.planData[key];
|
|
||||||
activities.forEach((activity) => {
|
|
||||||
if (this.isActivityInBounds(activity)) {
|
|
||||||
const currentStart = Math.max(this.viewBounds.start, activity.start);
|
|
||||||
const currentEnd = Math.min(this.viewBounds.end, activity.end);
|
|
||||||
const rectX = this.xScale(currentStart);
|
|
||||||
const rectY = this.xScale(currentEnd);
|
|
||||||
const rectWidth = rectY - rectX;
|
|
||||||
|
|
||||||
const activityNameWidth = this.getTextWidth(activity.name) + TEXT_LEFT_PADDING;
|
|
||||||
//TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text
|
|
||||||
const activityNameFitsRect = (rectWidth >= activityNameWidth);
|
|
||||||
const textStart = (activityNameFitsRect ? rectX : rectY) + TEXT_LEFT_PADDING;
|
|
||||||
const color = activity.color || DEFAULT_COLOR;
|
|
||||||
let textColor = '';
|
|
||||||
if (activity.textColor) {
|
|
||||||
textColor = activity.textColor;
|
|
||||||
} else if (activityNameFitsRect) {
|
|
||||||
textColor = this.getContrastingColor(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
let textLines = this.getActivityDisplayText(this.canvasContext, activity.name, activityNameFitsRect);
|
|
||||||
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
|
|
||||||
|
|
||||||
if (activityNameFitsRect) {
|
|
||||||
currentRow = this.getRowForActivity(rectX, rectWidth, activitiesByRow);
|
|
||||||
} else {
|
|
||||||
currentRow = this.getRowForActivity(rectX, textWidth, activitiesByRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
let textY = parseInt(currentRow, 10) + (activityNameFitsRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);
|
|
||||||
|
|
||||||
if (!activitiesByRow[currentRow]) {
|
|
||||||
activitiesByRow[currentRow] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
activitiesByRow[currentRow].push({
|
|
||||||
activity: {
|
|
||||||
color: color,
|
|
||||||
textColor: textColor,
|
|
||||||
name: activity.name,
|
|
||||||
exceeds: {
|
|
||||||
start: this.xScale(this.viewBounds.start) > this.xScale(activity.start),
|
|
||||||
end: this.xScale(this.viewBounds.end) < this.xScale(activity.end)
|
|
||||||
},
|
|
||||||
start: activity.start,
|
|
||||||
end: activity.end
|
|
||||||
},
|
|
||||||
textLines: textLines,
|
|
||||||
textStart: textStart,
|
|
||||||
textClass: activityNameFitsRect ? "" : "activity-label--outside-rect",
|
|
||||||
textY: textY,
|
|
||||||
start: rectX,
|
|
||||||
end: activityNameFitsRect ? rectY : textStart + textWidth,
|
|
||||||
rectWidth: rectWidth
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.groupActivities[key] = {
|
|
||||||
heading: key,
|
|
||||||
activitiesByRow
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getActivityDisplayText(context, text, activityNameFitsRect) {
|
|
||||||
//TODO: If the activity start is less than viewBounds.start then the text should be cropped on the left/should be off-screen)
|
|
||||||
let words = text.split(' ');
|
|
||||||
let line = '';
|
|
||||||
let activityText = [];
|
|
||||||
let rows = 1;
|
|
||||||
|
|
||||||
for (let n = 0; (n < words.length) && (rows <= 2); n++) {
|
|
||||||
let testLine = line + words[n] + ' ';
|
|
||||||
let metrics = context.measureText(testLine);
|
|
||||||
let testWidth = metrics.width;
|
|
||||||
if (!activityNameFitsRect && (testWidth > MAX_TEXT_WIDTH && n > 0)) {
|
|
||||||
activityText.push(line);
|
|
||||||
line = words[n] + ' ';
|
|
||||||
testLine = line + words[n] + ' ';
|
|
||||||
rows = rows + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
line = testLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
return activityText.length ? activityText : [line];
|
|
||||||
},
|
|
||||||
getGroupContainer(activityRows, heading) {
|
|
||||||
let svgHeight = 30;
|
|
||||||
let svgWidth = 200;
|
|
||||||
|
|
||||||
const rows = Object.keys(activityRows);
|
|
||||||
const isNested = this.options.isChildObject;
|
|
||||||
const status = isNested ? '' : this.status;
|
|
||||||
|
|
||||||
if (rows.length) {
|
|
||||||
const lastActivityRow = rows[rows.length - 1];
|
|
||||||
svgHeight = parseInt(lastActivityRow, 10) + ROW_HEIGHT;
|
|
||||||
svgWidth = this.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
let component = new Vue({
|
|
||||||
components: {
|
|
||||||
SwimLane
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
openmct: this.openmct
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
heading,
|
|
||||||
isNested,
|
|
||||||
status,
|
|
||||||
height: svgHeight,
|
|
||||||
width: svgWidth
|
|
||||||
};
|
|
||||||
},
|
|
||||||
template: `<swim-lane :is-nested="isNested" :status="status"><template slot="label">{{heading}}</template><template slot="object"><svg :height="height" :width="width"></svg></template></swim-lane>`
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$refs.planHolder.appendChild(component.$mount().$el);
|
|
||||||
|
|
||||||
let groupLabel = component.$el.querySelector('div:nth-child(1)');
|
|
||||||
let groupSVG = component.$el.querySelector('svg');
|
|
||||||
|
|
||||||
return {
|
|
||||||
groupLabel,
|
|
||||||
groupSVG
|
|
||||||
};
|
|
||||||
},
|
|
||||||
drawPlan() {
|
|
||||||
|
|
||||||
Object.keys(this.groupActivities).forEach((group, index) => {
|
|
||||||
const activitiesByRow = this.groupActivities[group].activitiesByRow;
|
|
||||||
const heading = this.groupActivities[group].heading;
|
|
||||||
const groupElements = this.getGroupContainer(activitiesByRow, heading);
|
|
||||||
let groupSVG = groupElements.groupSVG;
|
|
||||||
|
|
||||||
let activityRows = Object.keys(activitiesByRow);
|
|
||||||
if (activityRows.length <= 0) {
|
|
||||||
this.plotNoItems(groupSVG);
|
|
||||||
}
|
|
||||||
|
|
||||||
activityRows.forEach((row) => {
|
|
||||||
const items = activitiesByRow[row];
|
|
||||||
items.forEach(item => {
|
|
||||||
this.plotActivity(item, parseInt(row, 10), groupSVG);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
|
||||||
plotNoItems(svgElement) {
|
|
||||||
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
||||||
this.setNSAttributesForElement(textElement, {
|
|
||||||
x: "10",
|
|
||||||
y: "20",
|
|
||||||
class: "activity-label--outside-rect"
|
|
||||||
});
|
|
||||||
textElement.innerHTML = 'No activities within timeframe';
|
|
||||||
|
|
||||||
svgElement.appendChild(textElement);
|
|
||||||
},
|
|
||||||
setNSAttributesForElement(element, attributes) {
|
|
||||||
Object.keys(attributes).forEach((key) => {
|
|
||||||
element.setAttributeNS(null, key, attributes[key]);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getNSAttributesForElement(element, attribute) {
|
|
||||||
return element.getAttributeNS(null, attribute);
|
|
||||||
},
|
|
||||||
// Experimental for now - unused
|
|
||||||
addForeignElement(svgElement, label, x, y) {
|
|
||||||
let foreign = document.createElementNS('http://www.w3.org/2000/svg', "foreignObject");
|
|
||||||
this.setNSAttributesForElement(foreign, {
|
|
||||||
width: String(MAX_TEXT_WIDTH),
|
|
||||||
height: String(LINE_HEIGHT * 2),
|
|
||||||
x: x,
|
|
||||||
y: y
|
|
||||||
});
|
|
||||||
|
|
||||||
let textEl = document.createElement('div');
|
|
||||||
let textNode = document.createTextNode(label);
|
|
||||||
textEl.appendChild(textNode);
|
|
||||||
|
|
||||||
foreign.appendChild(textEl);
|
|
||||||
|
|
||||||
svgElement.appendChild(foreign);
|
|
||||||
},
|
|
||||||
plotActivity(item, row, svgElement) {
|
|
||||||
const activity = item.activity;
|
|
||||||
let width = item.rectWidth;
|
|
||||||
let rectElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
||||||
|
|
||||||
if (item.activity.exceeds.start) {
|
|
||||||
width = width + EDGE_ROUNDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.activity.exceeds.end) {
|
|
||||||
width = width + EDGE_ROUNDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
width = Math.max(width, 1); // Set width to a minimum of 1
|
|
||||||
|
|
||||||
// rx: don't round corners if the width of the rect is smaller than the rounding radius
|
|
||||||
this.setNSAttributesForElement(rectElement, {
|
|
||||||
class: 'activity-bounds',
|
|
||||||
x: item.activity.exceeds.start ? item.start - EDGE_ROUNDING : item.start,
|
|
||||||
y: row,
|
|
||||||
rx: (width < EDGE_ROUNDING * 2) ? 0 : EDGE_ROUNDING,
|
|
||||||
width: width,
|
|
||||||
height: String(ROW_HEIGHT),
|
|
||||||
fill: activity.color
|
|
||||||
});
|
|
||||||
|
|
||||||
rectElement.addEventListener('click', (event) => {
|
|
||||||
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
svgElement.appendChild(rectElement);
|
|
||||||
|
|
||||||
item.textLines.forEach((line, index) => {
|
|
||||||
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
||||||
this.setNSAttributesForElement(textElement, {
|
|
||||||
class: `activity-label ${item.textClass}`,
|
|
||||||
x: item.textStart,
|
|
||||||
y: item.textY + (index * LINE_HEIGHT),
|
|
||||||
fill: activity.textColor
|
|
||||||
});
|
|
||||||
|
|
||||||
const textNode = document.createTextNode(line);
|
|
||||||
textElement.appendChild(textNode);
|
|
||||||
textElement.addEventListener('click', (event) => {
|
|
||||||
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
|
|
||||||
});
|
|
||||||
svgElement.appendChild(textElement);
|
|
||||||
});
|
|
||||||
// this.addForeignElement(svgElement, activity.name, item.textStart, item.textY - LINE_HEIGHT);
|
|
||||||
},
|
|
||||||
cutHex(h, start, end) {
|
|
||||||
const hStr = (h.charAt(0) === '#') ? h.substring(1, 7) : h;
|
|
||||||
|
|
||||||
return parseInt(hStr.substring(start, end), 16);
|
|
||||||
},
|
|
||||||
getContrastingColor(hexColor) {
|
|
||||||
// https://codepen.io/davidhalford/pen/ywEva/
|
|
||||||
// TODO: move this into a general utility function?
|
|
||||||
const cThreshold = 130;
|
|
||||||
|
|
||||||
if (hexColor.indexOf('#') === -1) {
|
|
||||||
// We weren't given a hex color
|
|
||||||
return "#ff0000";
|
|
||||||
}
|
|
||||||
|
|
||||||
const hR = this.cutHex(hexColor, 0, 2);
|
|
||||||
const hG = this.cutHex(hexColor, 2, 4);
|
|
||||||
const hB = this.cutHex(hexColor, 4, 6);
|
|
||||||
|
|
||||||
const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000;
|
|
||||||
|
|
||||||
return cBrightness > cThreshold ? "#000000" : "#ffffff";
|
|
||||||
},
|
|
||||||
setSelectionForActivity(element, activity, multiSelect) {
|
|
||||||
this.openmct.selection.select([{
|
|
||||||
element: element,
|
|
||||||
context: {
|
|
||||||
type: 'activity',
|
|
||||||
activity: activity
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
element: this.openmct.layout.$refs.browseObject.$el,
|
|
||||||
context: {
|
|
||||||
item: this.domainObject,
|
|
||||||
supportsMultiSelect: true
|
|
||||||
}
|
|
||||||
}], multiSelect);
|
|
||||||
event.stopPropagation();
|
|
||||||
},
|
|
||||||
|
|
||||||
setStatus(status) {
|
|
||||||
this.status = status;
|
|
||||||
if (this.xScale) {
|
|
||||||
this.drawPlan();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
110
src/plugins/plan/PlanViewConfiguration.js
Normal file
110
src/plugins/plan/PlanViewConfiguration.js
Normal file
@ -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.<string, any>}
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import Plan from './Plan.vue';
|
import Plan from './components/Plan.vue';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default function PlanViewProvider(openmct) {
|
export default function PlanViewProvider(openmct) {
|
||||||
@ -35,11 +35,11 @@ export default function PlanViewProvider(openmct) {
|
|||||||
name: 'Plan',
|
name: 'Plan',
|
||||||
cssClass: 'icon-plan',
|
cssClass: 'icon-plan',
|
||||||
canView(domainObject) {
|
canView(domainObject) {
|
||||||
return domainObject.type === 'plan';
|
return domainObject.type === 'plan' || domainObject.type === 'gantt-chart';
|
||||||
},
|
},
|
||||||
|
|
||||||
canEdit(domainObject) {
|
canEdit(domainObject) {
|
||||||
return false;
|
return domainObject.type === 'gantt-chart';
|
||||||
},
|
},
|
||||||
|
|
||||||
view: function (domainObject, objectPath) {
|
view: function (domainObject, objectPath) {
|
||||||
|
187
src/plugins/plan/components/ActivityTimeline.vue
Normal file
187
src/plugins/plan/components/ActivityTimeline.vue
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<swim-lane
|
||||||
|
:is-nested="isNested"
|
||||||
|
:status="status"
|
||||||
|
>
|
||||||
|
<template slot="label">
|
||||||
|
{{ heading }}
|
||||||
|
</template>
|
||||||
|
<template slot="object">
|
||||||
|
<svg
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
>
|
||||||
|
<symbol
|
||||||
|
id="activity-bar-bg"
|
||||||
|
:height="rowHeight"
|
||||||
|
width="2"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="100%"
|
||||||
|
y1="0"
|
||||||
|
x2="100%"
|
||||||
|
y2="100%"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="1"
|
||||||
|
opacity="0.3"
|
||||||
|
transform="translate(-0.5, 0)"
|
||||||
|
/>
|
||||||
|
</symbol>
|
||||||
|
<template v-for="(activity, index) in activities">
|
||||||
|
<template v-if="clipActivityNames === true">
|
||||||
|
<clipPath
|
||||||
|
:id="activity.clipPathId"
|
||||||
|
:key="activity.clipPathId"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
:x="activity.rectStart"
|
||||||
|
:y="activity.row"
|
||||||
|
:width="activity.rectWidth - 1"
|
||||||
|
:height="rowHeight"
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</template>
|
||||||
|
<g
|
||||||
|
:key="`g-${activity.clipPathId}`"
|
||||||
|
class="c-plan__activity activity-bounds"
|
||||||
|
@click="setSelectionForActivity(activity, $event)"
|
||||||
|
>
|
||||||
|
<title>{{ activity.name }}</title>
|
||||||
|
<use
|
||||||
|
:key="`rect-${index}`"
|
||||||
|
href="#activity-bar-bg"
|
||||||
|
:x="activity.rectStart"
|
||||||
|
:y="activity.row"
|
||||||
|
:width="activity.rectWidth"
|
||||||
|
:height="rowHeight"
|
||||||
|
:class="activity.class"
|
||||||
|
:color="activity.color"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
v-for="(textLine, textIndex) in activity.textLines"
|
||||||
|
:key="`text-${index}-${textIndex}`"
|
||||||
|
:class="`c-plan__activity-label ${activity.textClass}`"
|
||||||
|
:x="activity.textStart"
|
||||||
|
:y="activity.textY + (textIndex * lineHeight)"
|
||||||
|
:fill="activity.textColor"
|
||||||
|
:clip-path="clipActivityNames === true ? `url(#${activity.clipPathId})` : ''"
|
||||||
|
>
|
||||||
|
{{ textLine }}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
|
<text
|
||||||
|
v-if="activities.length === 0"
|
||||||
|
x="10"
|
||||||
|
y="20"
|
||||||
|
class="c-plan__activity-label--outside-rect"
|
||||||
|
>
|
||||||
|
No activities within current timeframe
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</swim-lane>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SwimLane
|
||||||
|
},
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
props: {
|
||||||
|
activities: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
clipActivityNames: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 30
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 200
|
||||||
|
},
|
||||||
|
isNested: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rowHeight: {
|
||||||
|
type: Number,
|
||||||
|
default: 22
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lineHeight: 10
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setSelectionForActivity(activity, event) {
|
||||||
|
const element = event.currentTarget;
|
||||||
|
const multiSelect = event.metaKey;
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.openmct.selection.select([{
|
||||||
|
element: element,
|
||||||
|
context: {
|
||||||
|
type: 'activity',
|
||||||
|
activity: activity
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
element: this.openmct.layout.$refs.browseObject.$el,
|
||||||
|
context: {
|
||||||
|
item: this.domainObject,
|
||||||
|
supportsMultiSelect: true
|
||||||
|
}
|
||||||
|
}], multiSelect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
558
src/plugins/plan/components/Plan.vue
Normal file
558
src/plugins/plan/components/Plan.vue
Normal file
@ -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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="plan"
|
||||||
|
class="c-plan c-timeline-holder"
|
||||||
|
>
|
||||||
|
<template v-if="viewBounds && !options.compact">
|
||||||
|
<swim-lane>
|
||||||
|
<template slot="label">{{ timeSystem.name }}</template>
|
||||||
|
<timeline-axis
|
||||||
|
slot="object"
|
||||||
|
:bounds="viewBounds"
|
||||||
|
:time-system="timeSystem"
|
||||||
|
:content-height="height"
|
||||||
|
:rendering-engine="renderingEngine"
|
||||||
|
/>
|
||||||
|
</swim-lane>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
class="c-plan__contents u-contents"
|
||||||
|
>
|
||||||
|
<ActivityTimeline
|
||||||
|
v-for="(group, index) in visibleActivityGroups"
|
||||||
|
:key="`activityGroup-${group.heading}-${index}`"
|
||||||
|
:activities="group.activities"
|
||||||
|
:clip-activity-names="clipActivityNames"
|
||||||
|
:heading="group.heading"
|
||||||
|
:height="group.height"
|
||||||
|
:row-height="rowHeight"
|
||||||
|
:width="group.width"
|
||||||
|
:is-nested="options.isChildObject"
|
||||||
|
:status="status"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as d3Scale from 'd3-scale';
|
||||||
|
import TimelineAxis from "../../../ui/components/TimeSystemAxis.vue";
|
||||||
|
import ActivityTimeline from "./ActivityTimeline.vue";
|
||||||
|
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
|
||||||
|
import { getValidatedData, getContrastingColor } from "../util";
|
||||||
|
import PlanViewConfiguration from '../PlanViewConfiguration';
|
||||||
|
|
||||||
|
const PADDING = 1;
|
||||||
|
const OUTER_TEXT_PADDING = 12;
|
||||||
|
const INNER_TEXT_PADDING = 15;
|
||||||
|
const TEXT_LEFT_PADDING = 5;
|
||||||
|
const ROW_PADDING = 5;
|
||||||
|
const SWIMLANE_PADDING = 3;
|
||||||
|
const RESIZE_POLL_INTERVAL = 200;
|
||||||
|
const ROW_HEIGHT = 22;
|
||||||
|
const MAX_TEXT_WIDTH = 300;
|
||||||
|
const MIN_ACTIVITY_WIDTH = 2;
|
||||||
|
const DEFAULT_COLOR = '#999';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
TimelineAxis,
|
||||||
|
SwimLane,
|
||||||
|
ActivityTimeline
|
||||||
|
},
|
||||||
|
inject: ['openmct', 'domainObject', 'path'],
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {
|
||||||
|
compact: false,
|
||||||
|
isChildObject: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderingEngine: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return 'svg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activityGroups: [],
|
||||||
|
viewBounds: null,
|
||||||
|
timeSystem: null,
|
||||||
|
planData: {},
|
||||||
|
swimlaneVisibility: {},
|
||||||
|
clipActivityNames: false,
|
||||||
|
height: 0,
|
||||||
|
rowHeight: ROW_HEIGHT
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visibleActivityGroups() {
|
||||||
|
if (this.domainObject.type === 'plan') {
|
||||||
|
return this.activityGroups;
|
||||||
|
} else {
|
||||||
|
return this.activityGroups.filter(group =>
|
||||||
|
this.swimlaneVisibility[group.heading] === true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
clipActivityNames() {
|
||||||
|
this.setScaleAndGenerateActivities();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.composition = this.openmct.composition.get(this.domainObject);
|
||||||
|
this.planViewConfiguration = new PlanViewConfiguration(this.domainObject, this.openmct);
|
||||||
|
this.configuration = this.planViewConfiguration.getConfiguration();
|
||||||
|
this.isNested = this.options.isChildObject;
|
||||||
|
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||||
|
this.clipActivityNames = this.configuration.clipActivityNames;
|
||||||
|
if (this.domainObject.type === 'plan') {
|
||||||
|
this.planData = getValidatedData(this.domainObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
this.canvasContext = canvas.getContext('2d');
|
||||||
|
this.setDimensions();
|
||||||
|
this.setTimeContext();
|
||||||
|
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||||
|
this.setStatus(this.openmct.status.get(this.domainObject.identifier));
|
||||||
|
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
|
||||||
|
this.handleConfigurationChange(this.configuration);
|
||||||
|
this.planViewConfiguration.on('change', this.handleConfigurationChange);
|
||||||
|
this.stopObservingSelectFile = this.openmct.objects.observe(this.domainObject, 'selectFile', this.handleSelectFileChange);
|
||||||
|
this.loadComposition();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
clearInterval(this.resizeTimer);
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
if (this.unlisten) {
|
||||||
|
this.unlisten();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.removeStatusListener) {
|
||||||
|
this.removeStatusListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.composition) {
|
||||||
|
this.composition.off('add', this.handleCompositionAdd);
|
||||||
|
this.composition.off('remove', this.handleCompositionRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.planViewConfiguration.off('change', this.handleConfigurationChange);
|
||||||
|
this.stopObservingSelectFile();
|
||||||
|
this.planViewConfiguration.destroy();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
activityNameFitsRect(activityName, rectWidth) {
|
||||||
|
return (this.getTextWidth(activityName) + TEXT_LEFT_PADDING) < rectWidth;
|
||||||
|
},
|
||||||
|
setTimeContext() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||||
|
this.followTimeContext();
|
||||||
|
},
|
||||||
|
followTimeContext() {
|
||||||
|
this.updateViewBounds(this.timeContext.bounds());
|
||||||
|
|
||||||
|
this.timeContext.on("timeSystem", this.setScaleAndGenerateActivities);
|
||||||
|
this.timeContext.on("bounds", this.updateViewBounds);
|
||||||
|
},
|
||||||
|
loadComposition() {
|
||||||
|
if (this.composition) {
|
||||||
|
this.composition.on('add', this.handleCompositionAdd);
|
||||||
|
this.composition.on('remove', this.handleCompositionRemove);
|
||||||
|
this.composition.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off("timeSystem", this.setScaleAndGenerateActivities);
|
||||||
|
this.timeContext.off("bounds", this.updateViewBounds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showReplacePlanDialog(domainObject) {
|
||||||
|
const dialog = this.openmct.overlays.dialog({
|
||||||
|
iconClass: 'alert',
|
||||||
|
message: 'This action will replace the current Plan. Do you want to continue?',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: 'Ok',
|
||||||
|
emphasis: true,
|
||||||
|
callback: () => {
|
||||||
|
this.removeFromComposition(this.planObject);
|
||||||
|
this.planObject = domainObject;
|
||||||
|
this.planData = getValidatedData(domainObject);
|
||||||
|
this.setScaleAndGenerateActivities();
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cancel',
|
||||||
|
callback: () => {
|
||||||
|
this.removeFromComposition(domainObject);
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleCompositionAdd(domainObject) {
|
||||||
|
if (this.planObject) {
|
||||||
|
this.showReplacePlanDialog(domainObject);
|
||||||
|
} else {
|
||||||
|
this.planObject = domainObject;
|
||||||
|
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||||
|
this.planData = getValidatedData(domainObject);
|
||||||
|
this.setScaleAndGenerateActivities();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleConfigurationChange(newConfiguration) {
|
||||||
|
Object.keys(newConfiguration).forEach((key) => {
|
||||||
|
this[key] = newConfiguration[key];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleCompositionRemove(identifier) {
|
||||||
|
if (this.planObject && this.openmct.objects.areIdsEqual(identifier, this.planObject?.identifier)) {
|
||||||
|
this.planObject = null;
|
||||||
|
this.planData = {};
|
||||||
|
this.planViewConfiguration.resetSwimlaneVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setScaleAndGenerateActivities();
|
||||||
|
},
|
||||||
|
handleSelectFileChange() {
|
||||||
|
this.planData = getValidatedData(this.domainObject);
|
||||||
|
this.setScaleAndGenerateActivities();
|
||||||
|
},
|
||||||
|
removeFromComposition(domainObject) {
|
||||||
|
this.composition.remove(domainObject);
|
||||||
|
},
|
||||||
|
resize() {
|
||||||
|
let clientWidth = this.getClientWidth();
|
||||||
|
let clientHeight = this.getClientHeight();
|
||||||
|
if (clientWidth !== this.width) {
|
||||||
|
this.setDimensions();
|
||||||
|
this.updateViewBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientHeight !== this.height) {
|
||||||
|
this.setDimensions();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getClientWidth() {
|
||||||
|
let clientWidth = this.$refs.plan.clientWidth;
|
||||||
|
|
||||||
|
if (!clientWidth) {
|
||||||
|
//this is a hack - need a better way to find the parent of this component
|
||||||
|
let parent = this.openmct.layout.$refs.browseObject.$el;
|
||||||
|
if (parent) {
|
||||||
|
clientWidth = parent.getBoundingClientRect().width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientWidth - 200;
|
||||||
|
},
|
||||||
|
getClientHeight() {
|
||||||
|
let clientHeight = this.$refs.plan.clientHeight;
|
||||||
|
|
||||||
|
if (!clientHeight) {
|
||||||
|
//this is a hack - need a better way to find the parent of this component
|
||||||
|
let parent = this.openmct.layout.$refs.browseObject.$el;
|
||||||
|
if (parent) {
|
||||||
|
clientHeight = parent.getBoundingClientRect().height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientHeight;
|
||||||
|
},
|
||||||
|
updateViewBounds(bounds) {
|
||||||
|
if (bounds) {
|
||||||
|
this.viewBounds = bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timeSystem === null) {
|
||||||
|
this.timeSystem = this.openmct.time.timeSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setScaleAndGenerateActivities();
|
||||||
|
},
|
||||||
|
setScaleAndGenerateActivities(timeSystem) {
|
||||||
|
if (timeSystem) {
|
||||||
|
this.timeSystem = timeSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setScale(this.timeSystem);
|
||||||
|
if (this.xScale) {
|
||||||
|
this.generateActivities();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setDimensions() {
|
||||||
|
this.width = this.getClientWidth();
|
||||||
|
this.height = this.getClientHeight();
|
||||||
|
},
|
||||||
|
setScale(timeSystem) {
|
||||||
|
if (!this.width) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timeSystem) {
|
||||||
|
timeSystem = this.openmct.time.timeSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeSystem.isUTCBased) {
|
||||||
|
this.xScale = d3Scale.scaleUtc();
|
||||||
|
this.xScale.domain(
|
||||||
|
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.xScale = d3Scale.scaleLinear();
|
||||||
|
this.xScale.domain(
|
||||||
|
[this.viewBounds.start, this.viewBounds.end]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
||||||
|
},
|
||||||
|
isActivityInBounds(activity) {
|
||||||
|
return (activity.start < this.viewBounds.end) && (activity.end > this.viewBounds.start);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get the width of the given text in pixels.
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {number} width of the text in pixels (as a double)
|
||||||
|
*/
|
||||||
|
getTextWidth(text) {
|
||||||
|
const textMetrics = this.canvasContext.measureText(text);
|
||||||
|
|
||||||
|
return textMetrics.width;
|
||||||
|
},
|
||||||
|
sortIntegerAsc(a, b) {
|
||||||
|
const numA = parseInt(a, 10);
|
||||||
|
const numB = parseInt(b, 10);
|
||||||
|
if (numA > numB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numA < numB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get the row where the next activity will land.
|
||||||
|
* @param {number} rectX the x coordinate of the activity rect
|
||||||
|
* @param {number} width the width of the activity rect
|
||||||
|
* @param {Object.<string, Array.<Object>>} activitiesByRow activity arrays mapped by row value
|
||||||
|
*/
|
||||||
|
getRowForActivity(rectX, rectWidth, activitiesByRow) {
|
||||||
|
const sortedActivityRows = Object.keys(activitiesByRow).sort(this.sortIntegerAsc);
|
||||||
|
let currentRow;
|
||||||
|
|
||||||
|
function activitiesHaveOverlap(rects) {
|
||||||
|
return rects.some(rect => {
|
||||||
|
const { rectStart, rectEnd } = rect;
|
||||||
|
const calculatedEnd = rectX + rectWidth;
|
||||||
|
const hasOverlap = (rectX >= rectStart && rectX <= rectEnd)
|
||||||
|
|| (calculatedEnd >= rectStart && calculatedEnd <= rectEnd)
|
||||||
|
|| (rectX <= rectStart && calculatedEnd >= rectEnd);
|
||||||
|
|
||||||
|
return hasOverlap;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < sortedActivityRows.length; i++) {
|
||||||
|
let row = sortedActivityRows[i];
|
||||||
|
if (!activitiesHaveOverlap(activitiesByRow[row])) {
|
||||||
|
currentRow = row;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRow === undefined && sortedActivityRows.length) {
|
||||||
|
let row = parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10);
|
||||||
|
currentRow = row + ROW_HEIGHT + ROW_PADDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentRow || SWIMLANE_PADDING;
|
||||||
|
},
|
||||||
|
generateActivities() {
|
||||||
|
const groupNames = Object.keys(this.planData);
|
||||||
|
|
||||||
|
if (!groupNames.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activityGroups = [];
|
||||||
|
this.planViewConfiguration.initializeSwimlaneVisibility(groupNames);
|
||||||
|
|
||||||
|
groupNames.forEach((groupName) => {
|
||||||
|
let activitiesByRow = {};
|
||||||
|
let currentRow = 0;
|
||||||
|
|
||||||
|
const rawActivities = this.planData[groupName];
|
||||||
|
rawActivities.forEach((rawActivity) => {
|
||||||
|
if (!this.isActivityInBounds(rawActivity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStart = Math.max(this.viewBounds.start, rawActivity.start);
|
||||||
|
const currentEnd = Math.min(this.viewBounds.end, rawActivity.end);
|
||||||
|
const rectX1 = this.xScale(currentStart);
|
||||||
|
const rectX2 = this.xScale(currentEnd);
|
||||||
|
const rectWidth = Math.max(rectX2 - rectX1, MIN_ACTIVITY_WIDTH);
|
||||||
|
|
||||||
|
//TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text
|
||||||
|
const showTextInsideRect = this.clipActivityNames || this.activityNameFitsRect(rawActivity.name, rectWidth);
|
||||||
|
const textStart = (showTextInsideRect ? rectX1 : rectX2) + TEXT_LEFT_PADDING;
|
||||||
|
const color = rawActivity.color || DEFAULT_COLOR;
|
||||||
|
let textColor = '';
|
||||||
|
if (rawActivity.textColor) {
|
||||||
|
textColor = rawActivity.textColor;
|
||||||
|
} else if (showTextInsideRect) {
|
||||||
|
textColor = getContrastingColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
const textLines = this.getActivityDisplayText(this.canvasContext, rawActivity.name, showTextInsideRect);
|
||||||
|
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
|
||||||
|
|
||||||
|
if (showTextInsideRect) {
|
||||||
|
currentRow = this.getRowForActivity(rectX1, rectWidth, activitiesByRow);
|
||||||
|
} else {
|
||||||
|
currentRow = this.getRowForActivity(rectX1, textWidth, activitiesByRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
let textY = parseInt(currentRow, 10) + (showTextInsideRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);
|
||||||
|
|
||||||
|
if (!activitiesByRow[currentRow]) {
|
||||||
|
activitiesByRow[currentRow] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const activity = {
|
||||||
|
color: color,
|
||||||
|
textColor: textColor,
|
||||||
|
name: rawActivity.name,
|
||||||
|
exceeds: {
|
||||||
|
start: this.xScale(this.viewBounds.start) > this.xScale(rawActivity.start),
|
||||||
|
end: this.xScale(this.viewBounds.end) < this.xScale(rawActivity.end)
|
||||||
|
},
|
||||||
|
start: rawActivity.start,
|
||||||
|
end: rawActivity.end,
|
||||||
|
row: currentRow,
|
||||||
|
textLines: textLines,
|
||||||
|
textStart: textStart,
|
||||||
|
textClass: showTextInsideRect ? "" : "c-plan__activity-label--outside-rect",
|
||||||
|
textY: textY,
|
||||||
|
rectStart: rectX1,
|
||||||
|
rectEnd: showTextInsideRect ? rectX2 : textStart + textWidth,
|
||||||
|
rectWidth: rectWidth,
|
||||||
|
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow)
|
||||||
|
};
|
||||||
|
activitiesByRow[currentRow].push(activity);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { swimlaneHeight, swimlaneWidth } = this.getGroupDimensions(activitiesByRow);
|
||||||
|
const activities = Array.from(Object.values(activitiesByRow)).flat();
|
||||||
|
activityGroups.push({
|
||||||
|
heading: groupName,
|
||||||
|
activities,
|
||||||
|
height: swimlaneHeight,
|
||||||
|
width: swimlaneWidth,
|
||||||
|
status: this.isNested ? '' : this.status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activityGroups = activityGroups;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Format the activity name to fit within the activity rect with a max of 2 lines
|
||||||
|
* @param {CanvasRenderingContext2D} canvasContext
|
||||||
|
* @param {string} activityName
|
||||||
|
* @param {boolean} activityNameFitsRect
|
||||||
|
*/
|
||||||
|
getActivityDisplayText(canvasContext, activityName, activityNameFitsRect) {
|
||||||
|
// TODO: If the activity start is less than viewBounds.start then the text should be cropped on the left/should be off-screen)
|
||||||
|
let words = activityName.split(' ');
|
||||||
|
let line = '';
|
||||||
|
let activityLines = [];
|
||||||
|
|
||||||
|
for (let n = 0; (n < words.length) && (activityLines.length <= 2); n++) {
|
||||||
|
let tempLine = line + words[n] + ' ';
|
||||||
|
let textMetrics = canvasContext.measureText(tempLine);
|
||||||
|
const textWidth = textMetrics.width;
|
||||||
|
if (!activityNameFitsRect && (textWidth > MAX_TEXT_WIDTH && n > 0)) {
|
||||||
|
activityLines.push(line);
|
||||||
|
line = words[n] + ' ';
|
||||||
|
tempLine = line + words[n] + ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
line = tempLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
return activityLines.length ? activityLines : [line];
|
||||||
|
},
|
||||||
|
getGroupDimensions(activityRows) {
|
||||||
|
let swimlaneHeight = 30;
|
||||||
|
let swimlaneWidth = this.width;
|
||||||
|
|
||||||
|
if (!activityRows) {
|
||||||
|
return {
|
||||||
|
swimlaneHeight,
|
||||||
|
swimlaneWidth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = Object.keys(activityRows);
|
||||||
|
|
||||||
|
if (rows.length) {
|
||||||
|
const lastActivityRow = rows[rows.length - 1];
|
||||||
|
swimlaneHeight = parseInt(lastActivityRow, 10) + ROW_HEIGHT + SWIMLANE_PADDING;
|
||||||
|
swimlaneWidth = this.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
swimlaneHeight,
|
||||||
|
swimlaneWidth
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setStatus(status) {
|
||||||
|
this.status = status;
|
||||||
|
},
|
||||||
|
getClipPathId(groupName, activity, row) {
|
||||||
|
groupName = groupName.toLowerCase().replace(/ /g, '-');
|
||||||
|
const activityName = activity.name.toLowerCase().replace(/ /g, '-');
|
||||||
|
|
||||||
|
return `${groupName}-${activityName}-${activity.start}-${activity.end}-${row}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -20,13 +20,13 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import PlanActivitiesView from "./PlanActivitiesView.vue";
|
import PlanActivitiesView from "./components/PlanActivitiesView.vue";
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default function PlanInspectorViewProvider(openmct) {
|
export default function ActivityInspectorViewProvider(openmct) {
|
||||||
return {
|
return {
|
||||||
key: 'plan-inspector',
|
key: 'activity-inspector',
|
||||||
name: 'Plan Inspector View',
|
name: 'Activity',
|
||||||
canView: function (selection) {
|
canView: function (selection) {
|
||||||
if (selection.length === 0 || selection[0].length === 0) {
|
if (selection.length === 0 || selection[0].length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@ -44,6 +44,7 @@ export default function PlanInspectorViewProvider(openmct) {
|
|||||||
show: function (element) {
|
show: function (element) {
|
||||||
component = new Vue({
|
component = new Vue({
|
||||||
el: element,
|
el: element,
|
||||||
|
name: "PlanActivitiesView",
|
||||||
components: {
|
components: {
|
||||||
PlanActivitiesView: PlanActivitiesView
|
PlanActivitiesView: PlanActivitiesView
|
||||||
},
|
},
|
@ -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: '<plan-view-configuration></plan-view-configuration>'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.HIGH + 1;
|
||||||
|
},
|
||||||
|
destroy: function () {
|
||||||
|
if (component) {
|
||||||
|
component.$destroy();
|
||||||
|
component = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -36,16 +36,15 @@ import { getPreciseDuration } from "utils/duration";
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
const propertyLabels = {
|
const propertyLabels = {
|
||||||
'start': 'Start DateTime',
|
start: 'Start DateTime',
|
||||||
'end': 'End DateTime',
|
end: 'End DateTime',
|
||||||
'duration': 'Duration',
|
duration: 'Duration',
|
||||||
'earliestStart': 'Earliest Start',
|
earliestStart: 'Earliest Start',
|
||||||
'latestEnd': 'Latest End',
|
latestEnd: 'Latest End',
|
||||||
'gap': 'Gap',
|
gap: 'Gap',
|
||||||
'overlap': 'Overlap',
|
overlap: 'Overlap',
|
||||||
'totalTime': 'Total Time'
|
totalTime: 'Total Time'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PlanActivityView
|
PlanActivityView
|
144
src/plugins/plan/inspector/components/PlanViewConfiguration.vue
Normal file
144
src/plugins/plan/inspector/components/PlanViewConfiguration.vue
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="c-inspect-properties">
|
||||||
|
|
||||||
|
<div class="c-inspect-properties__header">
|
||||||
|
Swimlane Visibility
|
||||||
|
</div>
|
||||||
|
<ul class="c-inspect-properties__section">
|
||||||
|
<li
|
||||||
|
v-for="(visible, swimlaneName) in configuration.swimlaneVisibility"
|
||||||
|
:key="swimlaneName"
|
||||||
|
class="c-inspect-properties__row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c-inspect-properties__label"
|
||||||
|
title="Show or hide swimlane"
|
||||||
|
>
|
||||||
|
<label :for="swimlaneName + 'ColumnControl'">{{ swimlaneName }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="c-inspect-properties__value">
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
:id="swimlaneName + 'ColumnControl'"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="visible === true"
|
||||||
|
@change="toggleHideSwimlane(swimlaneName)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="value"
|
||||||
|
>
|
||||||
|
{{ visible === true ? 'Visible' : 'Hidden' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="c-inspect-properties__header">
|
||||||
|
Display settings
|
||||||
|
</div>
|
||||||
|
<ul class="c-inspect-properties__section">
|
||||||
|
<li class="c-inspect-properties__row">
|
||||||
|
<div
|
||||||
|
class="c-inspect-properties__label"
|
||||||
|
title="Clip Activity Names"
|
||||||
|
>
|
||||||
|
<label for="clipActivityNames">Clip Activity Names</label>
|
||||||
|
</div>
|
||||||
|
<div class="c-inspect-properties__value">
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
id="clipActivityNames"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="configuration.clipActivityNames === true"
|
||||||
|
@change="toggleClipActivityNames"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="value"
|
||||||
|
>
|
||||||
|
{{ configuration.clipActivityNames === true ? 'On' : 'Off' }}
|
||||||
|
</div>
|
||||||
|
</div></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import PlanViewConfiguration from '../../PlanViewConfiguration';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
data() {
|
||||||
|
const selection = this.openmct.selection.get();
|
||||||
|
/** @type {import('../../../api/objects/ObjectAPI').DomainObject} */
|
||||||
|
const domainObject = selection[0][0].context.item;
|
||||||
|
const planViewConfiguration = new PlanViewConfiguration(domainObject, this.openmct);
|
||||||
|
|
||||||
|
return {
|
||||||
|
planViewConfiguration,
|
||||||
|
isEditing: this.openmct.editor.isEditing(),
|
||||||
|
configuration: planViewConfiguration.getConfiguration()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canEdit() {
|
||||||
|
return this.isEditing;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.openmct.editor.on('isEditing', this.setIsEditing);
|
||||||
|
this.planViewConfiguration.on('change', this.handleConfigurationChange);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.openmct.editor.off('isEditing', this.setIsEditing);
|
||||||
|
this.planViewConfiguration.off('change', this.handleConfigurationChange);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* @param {Object.<string, any>} newConfiguration
|
||||||
|
*/
|
||||||
|
handleConfigurationChange(newConfiguration) {
|
||||||
|
this.configuration = newConfiguration;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {boolean} isEditing
|
||||||
|
*/
|
||||||
|
setIsEditing(isEditing) {
|
||||||
|
this.isEditing = isEditing;
|
||||||
|
},
|
||||||
|
toggleClipActivityNames() {
|
||||||
|
this.planViewConfiguration.setClipActivityNames(!this.configuration.clipActivityNames);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {string} swimlaneName
|
||||||
|
*/
|
||||||
|
toggleHideSwimlane(swimlaneName) {
|
||||||
|
this.planViewConfiguration.setSwimlaneVisibility(swimlaneName, !this.configuration.swimlaneVisibility[swimlaneName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -27,13 +27,26 @@
|
|||||||
text {
|
text {
|
||||||
stroke: none;
|
stroke: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.activity-label {
|
&__activity {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&[s-selected] {
|
||||||
|
rect, use {
|
||||||
|
outline-style: dotted;
|
||||||
|
outline-width: 2px;
|
||||||
|
stroke: $colorGanttSelectedBorder;
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__activity-label {
|
||||||
&--outside-rect {
|
&--outside-rect {
|
||||||
fill: $colorBodyFg !important;
|
fill: $colorBodyFg !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -21,15 +21,18 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import PlanViewProvider from './PlanViewProvider';
|
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) {
|
return function install(openmct) {
|
||||||
openmct.types.addType('plan', {
|
openmct.types.addType('plan', {
|
||||||
name: 'Plan',
|
name: 'Plan',
|
||||||
key: 'plan',
|
key: 'plan',
|
||||||
description: 'A configurable timeline-like view for a compatible mission plan file.',
|
description: 'A non-configurable timeline-like view for a compatible plan file.',
|
||||||
creatable: true,
|
creatable: options.creatable ?? false,
|
||||||
cssClass: 'icon-plan',
|
cssClass: 'icon-plan',
|
||||||
form: [
|
form: [
|
||||||
{
|
{
|
||||||
@ -45,10 +48,30 @@ export default function (configuration) {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
initialize: function (domainObject) {
|
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.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));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import Properties from "../inspectorViews/properties/Properties.vue";
|
|||||||
|
|
||||||
describe('the plugin', function () {
|
describe('the plugin', function () {
|
||||||
let planDefinition;
|
let planDefinition;
|
||||||
|
let ganttDefinition;
|
||||||
let element;
|
let element;
|
||||||
let child;
|
let child;
|
||||||
let openmct;
|
let openmct;
|
||||||
@ -50,6 +51,7 @@ describe('the plugin', function () {
|
|||||||
openmct.install(new PlanPlugin());
|
openmct.install(new PlanPlugin());
|
||||||
|
|
||||||
planDefinition = openmct.types.get('plan').definition;
|
planDefinition = openmct.types.get('plan').definition;
|
||||||
|
ganttDefinition = openmct.types.get('gantt-chart').definition;
|
||||||
|
|
||||||
element = document.createElement('div');
|
element = document.createElement('div');
|
||||||
element.style.width = '640px';
|
element.style.width = '640px';
|
||||||
@ -74,16 +76,31 @@ describe('the plugin', function () {
|
|||||||
let mockPlanObject = {
|
let mockPlanObject = {
|
||||||
name: 'Plan',
|
name: 'Plan',
|
||||||
key: 'plan',
|
key: 'plan',
|
||||||
|
creatable: false
|
||||||
|
};
|
||||||
|
|
||||||
|
let mockGanttObject = {
|
||||||
|
name: 'Gantt',
|
||||||
|
key: 'gantt-chart',
|
||||||
creatable: true
|
creatable: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe('the plan type', () => {
|
||||||
it('defines a plan object type with the correct key', () => {
|
it('defines a plan object type with the correct key', () => {
|
||||||
expect(planDefinition.key).toEqual(mockPlanObject.key);
|
expect(planDefinition.key).toEqual(mockPlanObject.key);
|
||||||
});
|
});
|
||||||
|
it('is not creatable', () => {
|
||||||
it('is creatable', () => {
|
|
||||||
expect(planDefinition.creatable).toEqual(mockPlanObject.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', () => {
|
describe('the plan view', () => {
|
||||||
it('provides a plan view', () => {
|
it('provides a plan view', () => {
|
||||||
@ -107,7 +124,7 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
|
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
|
||||||
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
|
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', () => {
|
it('displays the group label', () => {
|
||||||
const labelEl = element.querySelector('.c-plan__contents .c-object-label .c-object-label__name');
|
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 = {
|
const bounds = {
|
||||||
start: 1597160002854,
|
start: 1597160002854,
|
||||||
end: 1597181232854
|
end: 1597181232854
|
||||||
@ -190,27 +207,22 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
openmct.time.bounds(bounds);
|
openmct.time.bounds(bounds);
|
||||||
|
|
||||||
Vue.nextTick(() => {
|
await Vue.nextTick();
|
||||||
const rectEls = element.querySelectorAll('.c-plan__contents rect');
|
const rectEls = element.querySelectorAll('.c-plan__contents use');
|
||||||
expect(rectEls.length).toEqual(2);
|
expect(rectEls.length).toEqual(2);
|
||||||
const textEls = element.querySelectorAll('.c-plan__contents text');
|
const textEls = element.querySelectorAll('.c-plan__contents text');
|
||||||
expect(textEls.length).toEqual(3);
|
expect(textEls.length).toEqual(3);
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('shows the status indicator when available', (done) => {
|
it ('shows the status indicator when available', async () => {
|
||||||
openmct.status.set({
|
openmct.status.set({
|
||||||
key: "test-object",
|
key: "test-object",
|
||||||
namespace: ''
|
namespace: ''
|
||||||
}, 'draft');
|
}, 'draft');
|
||||||
|
|
||||||
Vue.nextTick(() => {
|
await Vue.nextTick();
|
||||||
const statusEl = element.querySelector('.c-plan__contents .is-status--draft');
|
const statusEl = element.querySelector('.c-plan__contents .is-status--draft');
|
||||||
expect(statusEl).toBeDefined();
|
expect(statusEl).toBeDefined();
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -224,10 +236,12 @@ describe('the plugin', function () {
|
|||||||
key: 'test-plan',
|
key: 'test-plan',
|
||||||
namespace: ''
|
namespace: ''
|
||||||
},
|
},
|
||||||
|
created: 123456789,
|
||||||
|
modified: 123456790,
|
||||||
version: 'v1'
|
version: 'v1'
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
openmct.selection.select([{
|
openmct.selection.select([{
|
||||||
element: element,
|
element: element,
|
||||||
context: {
|
context: {
|
||||||
@ -241,7 +255,7 @@ describe('the plugin', function () {
|
|||||||
}
|
}
|
||||||
}], false);
|
}], false);
|
||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
await Vue.nextTick();
|
||||||
let viewContainer = document.createElement('div');
|
let viewContainer = document.createElement('div');
|
||||||
child.append(viewContainer);
|
child.append(viewContainer);
|
||||||
component = new Vue({
|
component = new Vue({
|
||||||
@ -255,7 +269,6 @@ describe('the plugin', function () {
|
|||||||
template: '<properties/>'
|
template: '<properties/>'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
@ -264,7 +277,6 @@ describe('the plugin', function () {
|
|||||||
it('provides an inspector view with the version information if available', () => {
|
it('provides an inspector view with the version information if available', () => {
|
||||||
componentObject = component.$root.$children[0];
|
componentObject = component.$root.$children[0];
|
||||||
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
|
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
|
||||||
expect(propertiesEls.length).toEqual(7);
|
|
||||||
const found = Array.from(propertiesEls).some((propertyEl) => {
|
const found = Array.from(propertiesEls).some((propertyEl) => {
|
||||||
return (propertyEl.children[0].innerHTML.trim() === 'Version'
|
return (propertyEl.children[0].innerHTML.trim() === 'Version'
|
||||||
&& propertyEl.children[1].innerHTML.trim() === 'v1');
|
&& propertyEl.children[1].innerHTML.trim() === 'v1');
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
export function getValidatedData(domainObject) {
|
export function getValidatedData(domainObject) {
|
||||||
let sourceMap = domainObject.sourceMap;
|
const sourceMap = domainObject.sourceMap;
|
||||||
let body = domainObject.selectFile?.body;
|
const body = domainObject.selectFile?.body;
|
||||||
let json = {};
|
let json = {};
|
||||||
if (typeof body === 'string') {
|
if (typeof body === 'string') {
|
||||||
try {
|
try {
|
||||||
@ -64,3 +64,27 @@ export function getValidatedData(domainObject) {
|
|||||||
return json;
|
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";
|
||||||
|
}
|
||||||
|
@ -19,10 +19,12 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
const ALLOWED_TYPES = [
|
const ALLOWED_TYPES = [
|
||||||
'telemetry.plot.overlay',
|
'telemetry.plot.overlay',
|
||||||
'telemetry.plot.stacked',
|
'telemetry.plot.stacked',
|
||||||
'plan'
|
'plan',
|
||||||
|
'gantt-chart'
|
||||||
];
|
];
|
||||||
const DISALLOWED_TYPES = [
|
const DISALLOWED_TYPES = [
|
||||||
'telemetry.plot.bar-graph',
|
'telemetry.plot.bar-graph',
|
||||||
|
@ -19,12 +19,13 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<swim-lane
|
<swim-lane
|
||||||
:icon-class="item.type.definition.cssClass"
|
:icon-class="item.type.definition.cssClass"
|
||||||
:status="status"
|
:status="status"
|
||||||
:min-height="item.height"
|
:min-height="item.height"
|
||||||
:show-ucontents="item.domainObject.type === 'plan'"
|
:show-ucontents="isPlanLikeObject(item.domainObject)"
|
||||||
:span-rows-count="item.rowCount"
|
:span-rows-count="item.rowCount"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
@ -128,6 +129,9 @@ export default {
|
|||||||
},
|
},
|
||||||
setStatus(status) {
|
setStatus(status) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
|
},
|
||||||
|
isPlanLikeObject(domainObject) {
|
||||||
|
return domainObject.type === 'plan' || domainObject.type === 'gantt-chart';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -111,6 +111,8 @@ export default {
|
|||||||
let rowCount = 0;
|
let rowCount = 0;
|
||||||
if (domainObject.type === 'plan') {
|
if (domainObject.type === 'plan') {
|
||||||
rowCount = Object.keys(getValidatedData(domainObject)).length;
|
rowCount = Object.keys(getValidatedData(domainObject)).length;
|
||||||
|
} else if (domainObject.type === 'gantt-chart') {
|
||||||
|
rowCount = Object.keys(domainObject.configuration.swimlaneVisibility).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
let height = domainObject.type === 'telemetry.plot.stacked' ? `${domainObject.composition.length * 100}px` : '100px';
|
let height = domainObject.type === 'telemetry.plot.stacked' ? `${domainObject.composition.length * 100}px` : '100px';
|
||||||
|
@ -390,6 +390,7 @@ $colorCurrentBorder: $colorBodyBg;
|
|||||||
$colorFutureBg: rgba($colorKey, 0.2);
|
$colorFutureBg: rgba($colorKey, 0.2);
|
||||||
$colorFutureFg: $colorCurrentFg;
|
$colorFutureFg: $colorCurrentFg;
|
||||||
$colorFutureBorder: $colorCurrentBorder;
|
$colorFutureBorder: $colorCurrentBorder;
|
||||||
|
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
||||||
|
|
||||||
// Tree
|
// Tree
|
||||||
$colorTreeBg: transparent;
|
$colorTreeBg: transparent;
|
||||||
|
@ -394,6 +394,7 @@ $colorCurrentBorder: #fff;
|
|||||||
$colorFutureBg: rgba($colorKey, 0.2);
|
$colorFutureBg: rgba($colorKey, 0.2);
|
||||||
$colorFutureFg: $colorCurrentFg;
|
$colorFutureFg: $colorCurrentFg;
|
||||||
$colorFutureBorder: $colorCurrentBorder;
|
$colorFutureBorder: $colorCurrentBorder;
|
||||||
|
$colorGanttSelectedBorder: #fff;
|
||||||
|
|
||||||
// Tree
|
// Tree
|
||||||
$colorTreeBg: transparent;
|
$colorTreeBg: transparent;
|
||||||
|
@ -390,6 +390,7 @@ $colorCurrentBorder: #fff;
|
|||||||
$colorFutureBg: rgba($colorKey, 0.2);
|
$colorFutureBg: rgba($colorKey, 0.2);
|
||||||
$colorFutureFg: $colorCurrentFg;
|
$colorFutureFg: $colorCurrentFg;
|
||||||
$colorFutureBorder: $colorCurrentBorder;
|
$colorFutureBorder: $colorCurrentBorder;
|
||||||
|
$colorGanttSelectedBorder: #fff;
|
||||||
|
|
||||||
// Tree
|
// Tree
|
||||||
$colorTreeBg: transparent;
|
$colorTreeBg: transparent;
|
||||||
|
@ -1,3 +1,25 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="u-contents"
|
class="u-contents"
|
||||||
@ -68,6 +90,12 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isHidden: {
|
||||||
|
type: Boolean,
|
||||||
|
default() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
hideLabel: {
|
hideLabel: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default() {
|
default() {
|
||||||
|
@ -1,3 +1,25 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
.c-swimlane {
|
.c-swimlane {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100px 100px 1fr;
|
grid-template-columns: 100px 100px 1fr;
|
||||||
@ -32,4 +54,5 @@
|
|||||||
|
|
||||||
// Yet more brittle special case selecting...
|
// Yet more brittle special case selecting...
|
||||||
.is-object-type-plan { display: contents; }
|
.is-object-type-plan { display: contents; }
|
||||||
|
.is-object-type-gantt-chart { display: contents; }
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
v-if="singleSelectNonObject"
|
v-if="singleSelectNonObject"
|
||||||
class="c-inspector__selected c-inspector__selected--non-domain-object c-object-label"
|
class="c-inspector__selected c-inspector__selected--non-domain-object c-object-label"
|
||||||
>
|
>
|
||||||
<span class="c-object-label__name">Layout Object</span>
|
<span class="c-object-label__name">{{ heading }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -74,6 +74,13 @@ export default {
|
|||||||
item() {
|
item() {
|
||||||
return this.domainObject || {};
|
return this.domainObject || {};
|
||||||
},
|
},
|
||||||
|
heading() {
|
||||||
|
if (this.activity) {
|
||||||
|
return this.activity.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Layout Item';
|
||||||
|
},
|
||||||
type() {
|
type() {
|
||||||
return this.openmct.types.get(this.item.type);
|
return this.openmct.types.get(this.item.type);
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user