mirror of
https://github.com/nasa/openmct.git
synced 2024-12-18 20:57:53 +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:
parent
0b3e0e7efd
commit
ff3a20e446
@ -159,24 +159,26 @@ async function expandTreePaneItemByName(page, name) {
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||
*/
|
||||
async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
||||
if (!name) {
|
||||
name = `Plan:${genUuid()}`;
|
||||
}
|
||||
|
||||
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}?hideTree=true`);
|
||||
|
||||
//Click the Create button
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click 'Plan' menu option
|
||||
await page.click(`li:text("Plan")`);
|
||||
|
||||
// Modify the name input field of the domain object to accept 'name'
|
||||
if (name) {
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
await nameInput.fill("");
|
||||
await nameInput.fill(name);
|
||||
}
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
await nameInput.fill("");
|
||||
await nameInput.fill(name);
|
||||
|
||||
// Upload buffer from memory
|
||||
await page.locator('input#fileElem').setInputFiles({
|
||||
@ -194,7 +196,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
||||
]);
|
||||
|
||||
// Wait until the URL is updated
|
||||
await page.waitForURL(`**/mine/*`);
|
||||
await page.waitForURL(`**/${parent}/*`);
|
||||
const uuid = await getFocusedObjectUuid(page);
|
||||
const objectUrl = await getHashUrlToDomainObject(page, uuid);
|
||||
|
||||
|
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
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
const { test, expect } = require('../../../pluginFixtures');
|
||||
const { test } = require('../../../pluginFixtures');
|
||||
const { createPlanFromJSON } = require('../../../appActions');
|
||||
|
||||
const testPlan = {
|
||||
"TEST_GROUP": [
|
||||
{
|
||||
"name": "Past event 1",
|
||||
"start": 1660320408000,
|
||||
"end": 1660343797000,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 2",
|
||||
"start": 1660406808000,
|
||||
"end": 1660429160000,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 3",
|
||||
"start": 1660493208000,
|
||||
"end": 1660503981000,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 4",
|
||||
"start": 1660579608000,
|
||||
"end": 1660624108000,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 5",
|
||||
"start": 1660666008000,
|
||||
"end": 1660681529000,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
}
|
||||
]
|
||||
};
|
||||
const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json');
|
||||
const { assertPlanActivities } = require('../../../helper/planningUtils');
|
||||
|
||||
test.describe("Plan", () => {
|
||||
test("Create a Plan and display all plan events @unstable", async ({ page }) => {
|
||||
let plan;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan
|
||||
plan = await createPlanFromJSON(page, {
|
||||
json: testPlan1
|
||||
});
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
});
|
||||
|
||||
// Switch to fixed time mode with all plan events within the bounds
|
||||
await page.goto(`${plan.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`);
|
||||
const eventCount = await page.locator('.activity-bounds').count();
|
||||
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
||||
test("Displays all plan events", async ({ page }) => {
|
||||
await assertPlanActivities(page, testPlan1, plan.url);
|
||||
});
|
||||
});
|
||||
|
||||
|
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.MyItems());
|
||||
openmct.install(openmct.plugins.PlanLayout());
|
||||
openmct.install(openmct.plugins.PlanLayout({
|
||||
creatable: true
|
||||
}));
|
||||
openmct.install(openmct.plugins.Timeline());
|
||||
openmct.install(openmct.plugins.Hyperlink());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
|
@ -46,7 +46,7 @@ export default class Editor extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the application is in edit mode, false otherwise.
|
||||
* @returns {boolean} true if the application is in edit mode, false otherwise.
|
||||
*/
|
||||
isEditing() {
|
||||
return this.editing;
|
||||
|
@ -71,7 +71,7 @@ function (
|
||||
StatusAPI: StatusAPI.default,
|
||||
TelemetryAPI: TelemetryAPI,
|
||||
TimeAPI: TimeAPI.default,
|
||||
TypeRegistry: TypeRegistry,
|
||||
TypeRegistry: TypeRegistry.default,
|
||||
UserAPI: UserAPI.default,
|
||||
AnnotationAPI: AnnotationAPI.default
|
||||
};
|
||||
|
@ -62,6 +62,8 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
||||
* @property {Identifier[]} [composition] if
|
||||
* present, this will be used by the default composition provider
|
||||
* to load domain objects
|
||||
* @property {Object.<string, any>} [configuration] A key-value map containing configuration
|
||||
* settings for this domain object.
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
*/
|
||||
|
||||
|
@ -20,63 +20,25 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(function () {
|
||||
|
||||
/**
|
||||
* A Type describes a kind of domain object that may appear or be
|
||||
* created within Open MCT.
|
||||
*
|
||||
* @param {module:opemct.TypeRegistry~TypeDefinition} definition
|
||||
* @class Type
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
function Type(definition) {
|
||||
/**
|
||||
* A Type describes a kind of domain object that may appear or be
|
||||
* created within Open MCT.
|
||||
*
|
||||
* @param {module:opemct.TypeRegistry~TypeDefinition} definition
|
||||
* @class Type
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
export default class Type {
|
||||
constructor(definition) {
|
||||
this.definition = definition;
|
||||
if (definition.key) {
|
||||
this.key = definition.key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a domain object is an instance of this type.
|
||||
* @param domainObject
|
||||
* @returns {boolean} true if the domain object is of this type
|
||||
* @memberof module:openmct.Type#
|
||||
* @method check
|
||||
*/
|
||||
Type.prototype.check = function (domainObject) {
|
||||
// Depends on assignment from MCT.
|
||||
return domainObject.type === this.key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a definition for this type that can be registered using the
|
||||
* legacy bundle format.
|
||||
* @private
|
||||
*/
|
||||
Type.prototype.toLegacyDefinition = function () {
|
||||
const def = {};
|
||||
def.name = this.definition.name;
|
||||
def.cssClass = this.definition.cssClass;
|
||||
def.description = this.definition.description;
|
||||
def.properties = this.definition.form;
|
||||
|
||||
if (this.definition.initialize) {
|
||||
def.model = {};
|
||||
this.definition.initialize(def.model);
|
||||
}
|
||||
|
||||
if (this.definition.creatable) {
|
||||
def.features = ['creation'];
|
||||
}
|
||||
|
||||
return def;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a type definition from a legacy definition.
|
||||
*/
|
||||
Type.definitionFromLegacyDefinition = function (legacyDefinition) {
|
||||
static definitionFromLegacyDefinition(legacyDefinition) {
|
||||
let definition = {};
|
||||
definition.name = legacyDefinition.name;
|
||||
definition.cssClass = legacyDefinition.cssClass;
|
||||
@ -121,7 +83,39 @@ define(function () {
|
||||
}
|
||||
|
||||
return definition;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Check if a domain object is an instance of this type.
|
||||
* @param domainObject
|
||||
* @returns {boolean} true if the domain object is of this type
|
||||
* @memberof module:openmct.Type#
|
||||
* @method check
|
||||
*/
|
||||
check(domainObject) {
|
||||
// Depends on assignment from MCT.
|
||||
return domainObject.type === this.key;
|
||||
}
|
||||
/**
|
||||
* Get a definition for this type that can be registered using the
|
||||
* legacy bundle format.
|
||||
* @private
|
||||
*/
|
||||
toLegacyDefinition() {
|
||||
const def = {};
|
||||
def.name = this.definition.name;
|
||||
def.cssClass = this.definition.cssClass;
|
||||
def.description = this.definition.description;
|
||||
def.properties = this.definition.form;
|
||||
|
||||
return Type;
|
||||
});
|
||||
if (this.definition.initialize) {
|
||||
def.model = {};
|
||||
this.definition.initialize(def.model);
|
||||
}
|
||||
|
||||
if (this.definition.creatable) {
|
||||
def.features = ['creation'];
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
@ -19,35 +19,36 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
define(['./Type'], function (Type) {
|
||||
const UNKNOWN_TYPE = new Type({
|
||||
key: "unknown",
|
||||
name: "Unknown Type",
|
||||
cssClass: "icon-object-unknown"
|
||||
});
|
||||
import Type from './Type';
|
||||
|
||||
/**
|
||||
* @typedef TypeDefinition
|
||||
* @memberof module:openmct.TypeRegistry~
|
||||
* @property {string} label the name for this type of object
|
||||
* @property {string} description a longer-form description of this type
|
||||
* @property {function (object)} [initialize] a function which initializes
|
||||
* the model for new domain objects of this type
|
||||
* @property {boolean} [creatable] true if users should be allowed to
|
||||
* create this type (default: false)
|
||||
* @property {string} [cssClass] the CSS class to apply for icons
|
||||
*/
|
||||
const UNKNOWN_TYPE = new Type({
|
||||
key: "unknown",
|
||||
name: "Unknown Type",
|
||||
cssClass: "icon-object-unknown"
|
||||
});
|
||||
|
||||
/**
|
||||
* A TypeRegistry maintains the definitions for different types
|
||||
* that domain objects may have.
|
||||
* @interface TypeRegistry
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
function TypeRegistry() {
|
||||
/**
|
||||
* @typedef TypeDefinition
|
||||
* @memberof module:openmct.TypeRegistry~
|
||||
* @property {string} label the name for this type of object
|
||||
* @property {string} description a longer-form description of this type
|
||||
* @property {function (object)} [initialize] a function which initializes
|
||||
* the model for new domain objects of this type
|
||||
* @property {boolean} [creatable] true if users should be allowed to
|
||||
* create this type (default: false)
|
||||
* @property {string} [cssClass] the CSS class to apply for icons
|
||||
*/
|
||||
|
||||
/**
|
||||
* A TypeRegistry maintains the definitions for different types
|
||||
* that domain objects may have.
|
||||
* @interface TypeRegistry
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
export default class TypeRegistry {
|
||||
constructor() {
|
||||
this.types = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new object type.
|
||||
*
|
||||
@ -56,17 +57,16 @@ define(['./Type'], function (Type) {
|
||||
* @method addType
|
||||
* @memberof module:openmct.TypeRegistry#
|
||||
*/
|
||||
TypeRegistry.prototype.addType = function (typeKey, typeDef) {
|
||||
addType(typeKey, typeDef) {
|
||||
this.standardizeType(typeDef);
|
||||
this.types[typeKey] = new Type(typeDef);
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Takes a typeDef, standardizes it, and logs warnings about unsupported
|
||||
* usage.
|
||||
* @private
|
||||
*/
|
||||
TypeRegistry.prototype.standardizeType = function (typeDef) {
|
||||
standardizeType(typeDef) {
|
||||
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
|
||||
if (!typeDef.name) {
|
||||
typeDef.name = typeDef.label;
|
||||
@ -74,18 +74,16 @@ define(['./Type'], function (Type) {
|
||||
|
||||
delete typeDef.label;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* List keys for all registered types.
|
||||
* @method listKeys
|
||||
* @memberof module:openmct.TypeRegistry#
|
||||
* @returns {string[]} all registered type keys
|
||||
*/
|
||||
TypeRegistry.prototype.listKeys = function () {
|
||||
listKeys() {
|
||||
return Object.keys(this.types);
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Retrieve a registered type by its key.
|
||||
* @method get
|
||||
@ -93,18 +91,15 @@ define(['./Type'], function (Type) {
|
||||
* @memberof module:openmct.TypeRegistry#
|
||||
* @returns {module:openmct.Type} the registered type
|
||||
*/
|
||||
TypeRegistry.prototype.get = function (typeKey) {
|
||||
get(typeKey) {
|
||||
return this.types[typeKey] || UNKNOWN_TYPE;
|
||||
};
|
||||
|
||||
TypeRegistry.prototype.importLegacyTypes = function (types) {
|
||||
}
|
||||
importLegacyTypes(types) {
|
||||
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
|
||||
.forEach((type) => {
|
||||
let def = Type.definitionFromLegacyDefinition(type);
|
||||
this.addType(type.key, def);
|
||||
});
|
||||
};
|
||||
|
||||
return TypeRegistry;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,36 +20,36 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['./TypeRegistry', './Type'], function (TypeRegistry, Type) {
|
||||
describe('The Type API', function () {
|
||||
let typeRegistryInstance;
|
||||
import TypeRegistry from './TypeRegistry';
|
||||
|
||||
beforeEach(function () {
|
||||
typeRegistryInstance = new TypeRegistry ();
|
||||
typeRegistryInstance.addType('testType', {
|
||||
name: 'Test Type',
|
||||
description: 'This is a test type.',
|
||||
creatable: true
|
||||
});
|
||||
});
|
||||
describe('The Type API', function () {
|
||||
let typeRegistryInstance;
|
||||
|
||||
it('types can be standardized', function () {
|
||||
typeRegistryInstance.addType('standardizationTestType', {
|
||||
label: 'Test Type',
|
||||
description: 'This is a test type.',
|
||||
creatable: true
|
||||
});
|
||||
typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType);
|
||||
expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined();
|
||||
expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type');
|
||||
});
|
||||
|
||||
it('new types are registered successfully and can be retrieved', function () {
|
||||
expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type');
|
||||
});
|
||||
|
||||
it('type registry contains new keys', function () {
|
||||
expect(typeRegistryInstance.listKeys ()).toContain('testType');
|
||||
beforeEach(function () {
|
||||
typeRegistryInstance = new TypeRegistry ();
|
||||
typeRegistryInstance.addType('testType', {
|
||||
name: 'Test Type',
|
||||
description: 'This is a test type.',
|
||||
creatable: true
|
||||
});
|
||||
});
|
||||
|
||||
it('types can be standardized', function () {
|
||||
typeRegistryInstance.addType('standardizationTestType', {
|
||||
label: 'Test Type',
|
||||
description: 'This is a test type.',
|
||||
creatable: true
|
||||
});
|
||||
typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType);
|
||||
expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined();
|
||||
expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type');
|
||||
});
|
||||
|
||||
it('new types are registered successfully and can be retrieved', function () {
|
||||
expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type');
|
||||
});
|
||||
|
||||
it('type registry contains new keys', function () {
|
||||
expect(typeRegistryInstance.listKeys ()).toContain('testType');
|
||||
});
|
||||
});
|
||||
|
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.
|
||||
*****************************************************************************/
|
||||
|
||||
import Plan from './Plan.vue';
|
||||
import Plan from './components/Plan.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function PlanViewProvider(openmct) {
|
||||
@ -35,11 +35,11 @@ export default function PlanViewProvider(openmct) {
|
||||
name: 'Plan',
|
||||
cssClass: 'icon-plan',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'plan';
|
||||
return domainObject.type === 'plan' || domainObject.type === 'gantt-chart';
|
||||
},
|
||||
|
||||
canEdit(domainObject) {
|
||||
return false;
|
||||
return domainObject.type === 'gantt-chart';
|
||||
},
|
||||
|
||||
view: function (domainObject, objectPath) {
|
||||
|
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.
|
||||
*****************************************************************************/
|
||||
|
||||
import PlanActivitiesView from "./PlanActivitiesView.vue";
|
||||
import PlanActivitiesView from "./components/PlanActivitiesView.vue";
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function PlanInspectorViewProvider(openmct) {
|
||||
export default function ActivityInspectorViewProvider(openmct) {
|
||||
return {
|
||||
key: 'plan-inspector',
|
||||
name: 'Plan Inspector View',
|
||||
key: 'activity-inspector',
|
||||
name: 'Activity',
|
||||
canView: function (selection) {
|
||||
if (selection.length === 0 || selection[0].length === 0) {
|
||||
return false;
|
||||
@ -44,6 +44,7 @@ export default function PlanInspectorViewProvider(openmct) {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
name: "PlanActivitiesView",
|
||||
components: {
|
||||
PlanActivitiesView: PlanActivitiesView
|
||||
},
|
@ -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';
|
||||
|
||||
const propertyLabels = {
|
||||
'start': 'Start DateTime',
|
||||
'end': 'End DateTime',
|
||||
'duration': 'Duration',
|
||||
'earliestStart': 'Earliest Start',
|
||||
'latestEnd': 'Latest End',
|
||||
'gap': 'Gap',
|
||||
'overlap': 'Overlap',
|
||||
'totalTime': 'Total Time'
|
||||
start: 'Start DateTime',
|
||||
end: 'End DateTime',
|
||||
duration: 'Duration',
|
||||
earliestStart: 'Earliest Start',
|
||||
latestEnd: 'Latest End',
|
||||
gap: 'Gap',
|
||||
overlap: 'Overlap',
|
||||
totalTime: 'Total Time'
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PlanActivityView
|
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>
|
@ -21,21 +21,34 @@
|
||||
*****************************************************************************/
|
||||
|
||||
.c-plan {
|
||||
svg {
|
||||
text-rendering: geometricPrecision;
|
||||
svg {
|
||||
text-rendering: geometricPrecision;
|
||||
|
||||
text {
|
||||
stroke: none;
|
||||
text {
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-label {
|
||||
&--outside-rect {
|
||||
fill: $colorBodyFg !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__activity {
|
||||
cursor: pointer;
|
||||
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
&[s-selected] {
|
||||
rect, use {
|
||||
outline-style: dotted;
|
||||
outline-width: 2px;
|
||||
stroke: $colorGanttSelectedBorder;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__activity-label {
|
||||
&--outside-rect {
|
||||
fill: $colorBodyFg !important;
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -21,15 +21,18 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import PlanViewProvider from './PlanViewProvider';
|
||||
import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider";
|
||||
import ActivityInspectorViewProvider from "./inspector/ActivityInspectorViewProvider";
|
||||
import GanttChartInspectorViewProvider from "./inspector/GanttChartInspectorViewProvider";
|
||||
import ganttChartCompositionPolicy from './GanttChartCompositionPolicy';
|
||||
import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration';
|
||||
|
||||
export default function (configuration) {
|
||||
export default function (options = {}) {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('plan', {
|
||||
name: 'Plan',
|
||||
key: 'plan',
|
||||
description: 'A configurable timeline-like view for a compatible mission plan file.',
|
||||
creatable: true,
|
||||
description: 'A non-configurable timeline-like view for a compatible plan file.',
|
||||
creatable: options.creatable ?? false,
|
||||
cssClass: 'icon-plan',
|
||||
form: [
|
||||
{
|
||||
@ -45,10 +48,30 @@ export default function (configuration) {
|
||||
}
|
||||
],
|
||||
initialize: function (domainObject) {
|
||||
domainObject.configuration = {
|
||||
clipActivityNames: DEFAULT_CONFIGURATION.clipActivityNames
|
||||
};
|
||||
}
|
||||
});
|
||||
// Name TBD and subject to change
|
||||
openmct.types.addType('gantt-chart', {
|
||||
name: 'Gantt Chart',
|
||||
key: 'gantt-chart',
|
||||
description: 'A configurable timeline-like view for a compatible plan file.',
|
||||
creatable: true,
|
||||
cssClass: 'icon-plan',
|
||||
form: [],
|
||||
initialize(domainObject) {
|
||||
domainObject.configuration = {
|
||||
clipActivityNames: true
|
||||
};
|
||||
domainObject.composition = [];
|
||||
}
|
||||
});
|
||||
openmct.objectViews.addProvider(new PlanViewProvider(openmct));
|
||||
openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct));
|
||||
openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct));
|
||||
openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct));
|
||||
openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ import Properties from "../inspectorViews/properties/Properties.vue";
|
||||
|
||||
describe('the plugin', function () {
|
||||
let planDefinition;
|
||||
let ganttDefinition;
|
||||
let element;
|
||||
let child;
|
||||
let openmct;
|
||||
@ -50,6 +51,7 @@ describe('the plugin', function () {
|
||||
openmct.install(new PlanPlugin());
|
||||
|
||||
planDefinition = openmct.types.get('plan').definition;
|
||||
ganttDefinition = openmct.types.get('gantt-chart').definition;
|
||||
|
||||
element = document.createElement('div');
|
||||
element.style.width = '640px';
|
||||
@ -74,15 +76,30 @@ describe('the plugin', function () {
|
||||
let mockPlanObject = {
|
||||
name: 'Plan',
|
||||
key: 'plan',
|
||||
creatable: false
|
||||
};
|
||||
|
||||
let mockGanttObject = {
|
||||
name: 'Gantt',
|
||||
key: 'gantt-chart',
|
||||
creatable: true
|
||||
};
|
||||
|
||||
it('defines a plan object type with the correct key', () => {
|
||||
expect(planDefinition.key).toEqual(mockPlanObject.key);
|
||||
describe('the plan type', () => {
|
||||
it('defines a plan object type with the correct key', () => {
|
||||
expect(planDefinition.key).toEqual(mockPlanObject.key);
|
||||
});
|
||||
it('is not creatable', () => {
|
||||
expect(planDefinition.creatable).toEqual(mockPlanObject.creatable);
|
||||
});
|
||||
});
|
||||
|
||||
it('is creatable', () => {
|
||||
expect(planDefinition.creatable).toEqual(mockPlanObject.creatable);
|
||||
describe('the gantt-chart type', () => {
|
||||
it('defines a gantt-chart object type with the correct key', () => {
|
||||
expect(ganttDefinition.key).toEqual(mockGanttObject.key);
|
||||
});
|
||||
it('is creatable', () => {
|
||||
expect(ganttDefinition.creatable).toEqual(mockGanttObject.creatable);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the plan view', () => {
|
||||
@ -107,7 +124,7 @@ describe('the plugin', function () {
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
|
||||
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
|
||||
expect(planView.canEdit()).toBeFalse();
|
||||
expect(planView.canEdit(testViewObject)).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
@ -179,10 +196,10 @@ describe('the plugin', function () {
|
||||
|
||||
it('displays the group label', () => {
|
||||
const labelEl = element.querySelector('.c-plan__contents .c-object-label .c-object-label__name');
|
||||
expect(labelEl.innerHTML).toEqual('TEST-GROUP');
|
||||
expect(labelEl.innerHTML).toMatch(/TEST-GROUP/);
|
||||
});
|
||||
|
||||
it('displays the activities and their labels', (done) => {
|
||||
it('displays the activities and their labels', async () => {
|
||||
const bounds = {
|
||||
start: 1597160002854,
|
||||
end: 1597181232854
|
||||
@ -190,27 +207,22 @@ describe('the plugin', function () {
|
||||
|
||||
openmct.time.bounds(bounds);
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const rectEls = element.querySelectorAll('.c-plan__contents rect');
|
||||
expect(rectEls.length).toEqual(2);
|
||||
const textEls = element.querySelectorAll('.c-plan__contents text');
|
||||
expect(textEls.length).toEqual(3);
|
||||
|
||||
done();
|
||||
});
|
||||
await Vue.nextTick();
|
||||
const rectEls = element.querySelectorAll('.c-plan__contents use');
|
||||
expect(rectEls.length).toEqual(2);
|
||||
const textEls = element.querySelectorAll('.c-plan__contents text');
|
||||
expect(textEls.length).toEqual(3);
|
||||
});
|
||||
|
||||
it ('shows the status indicator when available', (done) => {
|
||||
it ('shows the status indicator when available', async () => {
|
||||
openmct.status.set({
|
||||
key: "test-object",
|
||||
namespace: ''
|
||||
}, 'draft');
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const statusEl = element.querySelector('.c-plan__contents .is-status--draft');
|
||||
expect(statusEl).toBeDefined();
|
||||
done();
|
||||
});
|
||||
await Vue.nextTick();
|
||||
const statusEl = element.querySelector('.c-plan__contents .is-status--draft');
|
||||
expect(statusEl).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -224,10 +236,12 @@ describe('the plugin', function () {
|
||||
key: 'test-plan',
|
||||
namespace: ''
|
||||
},
|
||||
created: 123456789,
|
||||
modified: 123456790,
|
||||
version: 'v1'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
openmct.selection.select([{
|
||||
element: element,
|
||||
context: {
|
||||
@ -241,19 +255,18 @@ describe('the plugin', function () {
|
||||
}
|
||||
}], false);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
let viewContainer = document.createElement('div');
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
el: viewContainer,
|
||||
components: {
|
||||
Properties
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct
|
||||
},
|
||||
template: '<properties/>'
|
||||
});
|
||||
await Vue.nextTick();
|
||||
let viewContainer = document.createElement('div');
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
el: viewContainer,
|
||||
components: {
|
||||
Properties
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct
|
||||
},
|
||||
template: '<properties/>'
|
||||
});
|
||||
});
|
||||
|
||||
@ -264,7 +277,6 @@ describe('the plugin', function () {
|
||||
it('provides an inspector view with the version information if available', () => {
|
||||
componentObject = component.$root.$children[0];
|
||||
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
|
||||
expect(propertiesEls.length).toEqual(7);
|
||||
const found = Array.from(propertiesEls).some((propertyEl) => {
|
||||
return (propertyEl.children[0].innerHTML.trim() === 'Version'
|
||||
&& propertyEl.children[1].innerHTML.trim() === 'v1');
|
||||
|
@ -21,8 +21,8 @@
|
||||
*****************************************************************************/
|
||||
|
||||
export function getValidatedData(domainObject) {
|
||||
let sourceMap = domainObject.sourceMap;
|
||||
let body = domainObject.selectFile?.body;
|
||||
const sourceMap = domainObject.sourceMap;
|
||||
const body = domainObject.selectFile?.body;
|
||||
let json = {};
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
@ -64,3 +64,27 @@ export function getValidatedData(domainObject) {
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
export function getContrastingColor(hexColor) {
|
||||
function cutHex(h, start, end) {
|
||||
const hStr = (h.charAt(0) === '#') ? h.substring(1, 7) : h;
|
||||
|
||||
return parseInt(hStr.substring(start, end), 16);
|
||||
}
|
||||
|
||||
// https://codepen.io/davidhalford/pen/ywEva/
|
||||
const cThreshold = 130;
|
||||
|
||||
if (hexColor.indexOf('#') === -1) {
|
||||
// We weren't given a hex color
|
||||
return "#ff0000";
|
||||
}
|
||||
|
||||
const hR = cutHex(hexColor, 0, 2);
|
||||
const hG = cutHex(hexColor, 2, 4);
|
||||
const hB = cutHex(hexColor, 4, 6);
|
||||
|
||||
const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000;
|
||||
|
||||
return cBrightness > cThreshold ? "#000000" : "#ffffff";
|
||||
}
|
||||
|
@ -19,10 +19,12 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const ALLOWED_TYPES = [
|
||||
'telemetry.plot.overlay',
|
||||
'telemetry.plot.stacked',
|
||||
'plan'
|
||||
'plan',
|
||||
'gantt-chart'
|
||||
];
|
||||
const DISALLOWED_TYPES = [
|
||||
'telemetry.plot.bar-graph',
|
||||
|
@ -19,12 +19,13 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<swim-lane
|
||||
:icon-class="item.type.definition.cssClass"
|
||||
:status="status"
|
||||
:min-height="item.height"
|
||||
:show-ucontents="item.domainObject.type === 'plan'"
|
||||
:show-ucontents="isPlanLikeObject(item.domainObject)"
|
||||
:span-rows-count="item.rowCount"
|
||||
>
|
||||
<template #label>
|
||||
@ -128,6 +129,9 @@ export default {
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
},
|
||||
isPlanLikeObject(domainObject) {
|
||||
return domainObject.type === 'plan' || domainObject.type === 'gantt-chart';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -111,6 +111,8 @@ export default {
|
||||
let rowCount = 0;
|
||||
if (domainObject.type === 'plan') {
|
||||
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';
|
||||
|
@ -390,6 +390,7 @@ $colorCurrentBorder: $colorBodyBg;
|
||||
$colorFutureBg: rgba($colorKey, 0.2);
|
||||
$colorFutureFg: $colorCurrentFg;
|
||||
$colorFutureBorder: $colorCurrentBorder;
|
||||
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
||||
|
||||
// Tree
|
||||
$colorTreeBg: transparent;
|
||||
|
@ -394,6 +394,7 @@ $colorCurrentBorder: #fff;
|
||||
$colorFutureBg: rgba($colorKey, 0.2);
|
||||
$colorFutureFg: $colorCurrentFg;
|
||||
$colorFutureBorder: $colorCurrentBorder;
|
||||
$colorGanttSelectedBorder: #fff;
|
||||
|
||||
// Tree
|
||||
$colorTreeBg: transparent;
|
||||
|
@ -390,6 +390,7 @@ $colorCurrentBorder: #fff;
|
||||
$colorFutureBg: rgba($colorKey, 0.2);
|
||||
$colorFutureFg: $colorCurrentFg;
|
||||
$colorFutureBorder: $colorCurrentBorder;
|
||||
$colorGanttSelectedBorder: #fff;
|
||||
|
||||
// Tree
|
||||
$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>
|
||||
<div
|
||||
class="u-contents"
|
||||
@ -68,6 +90,12 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isHidden: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
hideLabel: {
|
||||
type: Boolean,
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 100px 1fr;
|
||||
@ -32,4 +54,5 @@
|
||||
|
||||
// Yet more brittle special case selecting...
|
||||
.is-object-type-plan { display: contents; }
|
||||
.is-object-type-gantt-chart { display: contents; }
|
||||
}
|
||||
|
@ -44,7 +44,7 @@
|
||||
v-if="singleSelectNonObject"
|
||||
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
|
||||
@ -74,6 +74,13 @@ export default {
|
||||
item() {
|
||||
return this.domainObject || {};
|
||||
},
|
||||
heading() {
|
||||
if (this.activity) {
|
||||
return this.activity.name;
|
||||
}
|
||||
|
||||
return 'Layout Item';
|
||||
},
|
||||
type() {
|
||||
return this.openmct.types.get(this.item.type);
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user