feat: Inspector tabs (#6137)

* framework for all inspector views being provided

* move elements view to plugin

* move location view into plugin

* move styles view into plugin

* move properties view into plugin

* install inspector views in index.html

* rename filters inspector view provider for tab

* finish elements view as plugin

* finish location view as plugin

* finish properties view as plugin

* finish styles view as plugin

* point main styles to new plugins

* finish inspector tab and views components

* fix paths for styles views

* fix path issues

* rename fault management inspector view

fix unit test

* fix paths for unit tests

* rename bar graph inspector view

fix unit test

* rename plots inspector view

fix unit test

* inspector views installed in mct.js

* sort inspector views by priority

* make name required for inspector tabs

* priority changes

* only show filters tab if filters exist

* object renamed to domainObject

* remove dead code

* select first tab if selected tab becomes hidden

* bandaid fix to get e2e working

* also apply bandaid to this test

* [a11y] Basic ARIA tab role for Inspector panels

* test(e2e): better selectors for scatterPlot test

* test(e2e): fix search test selector

* pass key and glyph to views

* use key for tabs identification

* high + 1 priority for object specific views

* Closes #6118
- Significant layout and behavior refinements to Inspector tabs.
- New theme constants for tabs.
- Tabs in Tab Views updated to use theme constants.

* Closes #6118
- Refinement to look of Inspector tabs.
- Shortened names in many *InspectorViewProvider.js files.
- WIP adding glyph capability, display not yet wired up.

* Closes #6118
- Tightened H2 spacing in Inspector.

* move annotations into plugin

* register annotations view provider

* move tags inside annotations

* fix paths

* move element item group into plugin

* move PlotElementsPool view into plugin

* plots has a different element view

* fix paths for plot elements pool

* fix: `role=` instead of `aria-role=` 🤦‍♂️

* test(e2e): fix tab locators

* move location views into properties tab view

* include location.scss

* move location into properties tab

* fix html for location within properties view

* retain selected tab on new selection

* refresh view of same tab with new selection

* add browse mode inspector view for alphanumerics

* fix prop passing

* removed vestigial code

* fix inspector tab selection

* remove timeouts and unnessecary awaits

* test: assert checkbox status before checking

* add selectInspectorTab to general app actions

* use selectInspectorTabs from appActions

* need to pass page to playwright function

* select the correct tab

* fix plan unit test

* fix plots tests by clicking on correct tab

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Scott Bell <scott@traclabs.com>
This commit is contained in:
David Tsay 2023-03-06 14:11:25 -08:00 committed by GitHub
parent f388d9a548
commit 1d4cf1ff06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 1006 additions and 574 deletions

View File

@ -383,6 +383,25 @@ async function setEndOffset(page, offset) {
await setTimeConductorOffset(page, offset, endOffsetButton);
}
/**
* Selects an inspector tab based on the provided tab name
*
* @param {import('@playwright/test').Page} page
* @param {String} name the name of the tab
*/
async function selectInspectorTab(page, name) {
const inspectorTabs = page.getByRole('tablist');
const inspectorTab = inspectorTabs.getByTitle(name);
const inspectorTabClass = await inspectorTab.getAttribute('class');
const isSelectedInspectorTab = inspectorTabClass.includes('is-current');
// do not click a tab that is already selected or it will timeout your test
// do to a { pointer-events: none; } on selected tabs
if (!isSelectedInspectorTab) {
await inspectorTab.click();
}
}
// eslint-disable-next-line no-undef
module.exports = {
createDomainObjectWithDefaults,
@ -396,5 +415,6 @@ module.exports = {
setFixedTimeMode,
setRealTimeMode,
setStartOffset,
setEndOffset
setEndOffset,
selectInspectorTab
};

View File

@ -24,6 +24,7 @@
Testsuite for plot autoscale.
*/
const { selectInspectorTab } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
test.use({
viewport: {
@ -50,6 +51,7 @@ test.describe('Autoscale', () => {
// enter edit mode
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Config');
await turnOffAutoscale(page);
await setUserDefinedMinAndMax(page, '-2', '2');

View File

@ -26,6 +26,8 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { selectInspectorTab } = require('../../../../appActions');
test.describe('Log plot tests', () => {
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
@ -36,6 +38,7 @@ test.describe('Log plot tests', () => {
await makeOverlayPlot(page, myItemsFolderName);
await testRegularTicks(page);
await enableEditMode(page);
await selectInspectorTab(page, 'Config');
await enableLogMode(page);
await testLogTicks(page);
await disableLogMode(page);
@ -186,6 +189,7 @@ async function enableEditMode(page) {
*/
async function enableLogMode(page) {
// turn on log mode
await expect(page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox')).not.toBeChecked();
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').check();
// await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
}
@ -195,6 +199,7 @@ async function enableLogMode(page) {
*/
async function disableLogMode(page) {
// turn off log mode
await expect(page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox')).toBeChecked();
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck();
}

View File

@ -26,7 +26,7 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
test.describe('Overlay Plot', () => {
test.beforeEach(async ({ page }) => {
@ -45,6 +45,8 @@ test.describe('Overlay Plot', () => {
await page.goto(overlayPlot.url);
await selectInspectorTab(page, 'Config');
// navigate to plot series color palette
await page.click('.l-browse-bar__actions__edit');
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
@ -89,22 +91,7 @@ test.describe('Overlay Plot', () => {
await page.goto(overlayPlot.url);
await page.click('button[title="Edit"]');
// Expand the elements pool vertically
await page.locator('.l-pane.l-pane--vertical-handle-before', {
hasText: 'Elements'
}).locator('.l-pane__handle').hover();
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.up();
const yAxis1PropertyGroup = page.locator('[aria-label="Y Axis Properties"]');
const yAxis2PropertyGroup = page.locator('[aria-label="Y Axis 2 Properties"]');
const yAxis3PropertyGroup = page.locator('[aria-label="Y Axis 3 Properties"]');
// Assert that Y Axis 1 property group is visible only
await expect(yAxis1PropertyGroup).toBeVisible();
await expect(yAxis2PropertyGroup).toBeHidden();
await expect(yAxis3PropertyGroup).toBeHidden();
await selectInspectorTab(page, 'Elements');
// Drag swg a, c, e into Y Axis 2
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
@ -112,6 +99,12 @@ test.describe('Overlay Plot', () => {
await page.locator(`#inspector-elements-tree >> text=${swgE.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
// Assert that Y Axis 1 and Y Axis 2 property groups are visible only
await selectInspectorTab(page, 'Config');
const yAxis1PropertyGroup = page.locator('[aria-label="Y Axis Properties"]');
const yAxis2PropertyGroup = page.locator('[aria-label="Y Axis 2 Properties"]');
const yAxis3PropertyGroup = page.locator('[aria-label="Y Axis 3 Properties"]');
await expect(yAxis1PropertyGroup).toBeVisible();
await expect(yAxis2PropertyGroup).toBeVisible();
await expect(yAxis3PropertyGroup).toBeHidden();
@ -120,15 +113,21 @@ test.describe('Overlay Plot', () => {
const yAxis2Group = page.getByLabel("Y Axis 2");
const yAxis3Group = page.getByLabel("Y Axis 3");
await selectInspectorTab(page, 'Elements');
// Drag swg b into Y Axis 3
await page.locator(`#inspector-elements-tree >> text=${swgB.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]'));
// Assert that all Y Axis property groups are visible
await selectInspectorTab(page, 'Config');
await expect(yAxis1PropertyGroup).toBeVisible();
await expect(yAxis2PropertyGroup).toBeVisible();
await expect(yAxis3PropertyGroup).toBeVisible();
// Verify that the elements are in the correct buckets and in the correct order
await selectInspectorTab(page, 'Elements');
expect(yAxis1Group.getByRole('listitem', { name: swgD.name })).toBeTruthy();
expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: swgE.name })).toBeTruthy();
@ -154,8 +153,10 @@ test.describe('Overlay Plot', () => {
await page.goto(overlayPlot.url);
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Elements');
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
await page.locator('.js-overlay canvas').nth(1);
const plotPixelSize = await getCanvasPixelsWithData(page);
expect(plotPixelSize).toBeGreaterThan(0);
});

View File

@ -25,6 +25,7 @@ Tests to verify log plot functionality. Note this test suite if very much under
necessarily be used for reference when writing new tests in this area.
*/
const { selectInspectorTab } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
test.describe('Legend color in sync with plot color', () => {
@ -33,6 +34,8 @@ test.describe('Legend color in sync with plot color', () => {
// navigate to plot series color palette
await page.click('.l-browse-bar__actions__edit');
await selectInspectorTab(page, 'Config');
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
await page.locator('.c-click-swatch--menu').click();
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();

View File

@ -25,7 +25,7 @@
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Scatter Plot', () => {
@ -40,8 +40,8 @@ test.describe('Scatter Plot', () => {
});
test('Can add and remove telemetry sources', async ({ page }) => {
const editButtonLocator = page.locator('button[title="Edit"]');
const saveButtonLocator = page.locator('button[title="Save"]');
const editButton = page.locator('button[title="Edit"]');
const saveButton = page.locator('button[title="Save"]');
// Create a sine wave generator within the scatter plot
const swg1 = await createDomainObjectWithDefaults(page, {
@ -53,9 +53,10 @@ test.describe('Scatter Plot', () => {
// Navigate to the scatter plot and verify that
// the SWG appears in the elements pool
await page.goto(scatterPlot.url);
await editButtonLocator.click();
await editButton.click();
await selectInspectorTab(page, 'Elements');
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButtonLocator.click();
await saveButton.click();
await page.locator('li[title="Save and Finish Editing"]').click();
// Create another sine wave generator within the scatter plot
@ -72,10 +73,13 @@ test.describe('Scatter Plot', () => {
// Navigate to the scatter plot and verify that the new SWG
// appears in the elements pool and the old one is gone
await page.goto(scatterPlot.url);
await editButtonLocator.click();
await editButton.click();
// Click the "Elements" tab
await selectInspectorTab(page, 'Elements');
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
await saveButtonLocator.click();
await saveButton.click();
// Right click on the new SWG in the elements pool and delete it
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({

View File

@ -26,7 +26,7 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
test.describe('Stacked Plot', () => {
let stackedPlot;
@ -65,11 +65,7 @@ test.describe('Stacked Plot', () => {
await page.click('button[title="Edit"]');
// Expand the elements pool vertically
await page.locator('.l-pane__handle').nth(2).hover({ trial: true });
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.up();
await selectInspectorTab(page, 'Elements');
await swgBElementsPoolItem.click({ button: 'right' });
await page.getByRole('menuitem').filter({ hasText: /Remove/ }).click();
@ -92,11 +88,7 @@ test.describe('Stacked Plot', () => {
await page.click('button[title="Edit"]');
// Expand the elements pool vertically
await page.locator('.l-pane__handle').nth(2).hover({ trial: true });
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.up();
await selectInspectorTab(page, 'Elements');
const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0);
const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1);
@ -136,6 +128,8 @@ test.describe('Stacked Plot', () => {
test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({ page }) => {
await page.goto(stackedPlot.url);
await selectInspectorTab(page, 'Config');
// Click on the 1st plot
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click();
@ -163,6 +157,8 @@ test.describe('Stacked Plot', () => {
// Go into edit mode
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Config');
// Click on canvas for the 1st plot
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click();

View File

@ -24,7 +24,7 @@
*/
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../appActions');
const { v4: uuid } = require('uuid');
test.describe('Grand Search', () => {
@ -50,7 +50,7 @@ test.describe('Grand Search', () => {
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
// Click the Elements pool to dismiss the search menu
await page.locator('.l-pane__label:has-text("Elements")').click();
await selectInspectorTab(page, 'Elements');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();

View File

@ -143,7 +143,7 @@ define([
* @memberof module:openmct.MCT#
* @name inspectorViews
*/
['inspectorViews', () => new InspectorViewRegistry()],
['inspectorViews', () => new InspectorViewRegistry.default()],
/**
* Registry for views which should appear in Edit Properties
@ -295,6 +295,7 @@ define([
this.install(this.plugins.DeviceClassifier());
this.install(this.plugins.UserIndicator());
this.install(this.plugins.Gauge());
this.install(this.plugins.InspectorViews());
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@ -5,7 +5,7 @@ import BarGraphOptions from "./BarGraphOptions.vue";
export default function BarGraphInspectorViewProvider(openmct) {
return {
key: BAR_GRAPH_INSPECTOR_KEY,
name: 'Bar Graph Inspector View',
name: 'Bar Graph Configuration',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
@ -42,7 +42,7 @@ export default function BarGraphInspectorViewProvider(openmct) {
};
},
priority: function () {
return 1;
return openmct.priority.HIGH + 1;
}
};
}

View File

@ -579,7 +579,7 @@ describe("the plugin", function () {
child.append(viewContainer);
const applicableViews = openmct.inspectorViews.get(selection);
plotInspectorView = applicableViews[0];
plotInspectorView = applicableViews.filter(view => view.name === 'Bar Graph Configuration')[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();

View File

@ -5,7 +5,7 @@ import PlotOptions from "./PlotOptions.vue";
export default function ScatterPlotInspectorViewProvider(openmct) {
return {
key: SCATTER_PLOT_INSPECTOR_KEY,
name: 'Bar Graph Inspector View',
name: 'Config',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
@ -42,7 +42,7 @@ export default function ScatterPlotInspectorViewProvider(openmct) {
};
},
priority: function () {
return 1;
return openmct.priority.HIGH + 1;
}
};
}

View File

@ -29,9 +29,6 @@
Your selection includes one or more items that use Conditional Styling. Applying a static style below will replace any Conditional Styling with the new choice.
</div>
<template v-if="!conditionSetDomainObject">
<div class="c-inspect-styles__header">
Object Style
</div>
<FontStyleEditor
v-if="canStyleFont"
:font-style="consolidatedFontStyle"
@ -63,9 +60,6 @@
</div>
</template>
<template v-else>
<div class="c-inspect-styles__header">
Conditional Object Styles
</div>
<div class="c-inspect-styles__content c-inspect-styles__condition-set c-inspect-styles__elem">
<a
v-if="conditionSetDomainObject"
@ -156,7 +150,7 @@
<script>
import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue';
import FontStyleEditor from '../../../inspectorViews/styles/FontStyleEditor.vue';
import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";

View File

@ -22,7 +22,7 @@
import { createOpenMct, resetApplicationState } from "utils/testing";
import ConditionPlugin from "./plugin";
import stylesManager from '@/ui/inspector/styles/StylesManager';
import stylesManager from '../inspectorViews/styles/StylesManager';
import StylesView from "./components/inspector/StylesView.vue";
import Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils";

View File

@ -79,7 +79,7 @@ export default function AlphanumericFormatViewProvider(openmct, options) {
return {
key: 'alphanumeric-format',
name: 'Alphanumeric Format',
name: 'Format',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 1) {
return false;

View File

@ -22,12 +22,8 @@
<template>
<div
v-if="isEditing"
class="c-inspect-properties"
>
<div class="c-inspect-properties__header">
Alphanumeric Format
</div>
<ul class="c-inspect-properties__section">
<li class="c-inspect-properties__row">
<div
@ -40,6 +36,7 @@
<input
id="telemetryPrintfFormat"
type="text"
:disabled="!isEditing"
:value="telemetryFormat"
:placeholder="nonMixedFormat ? '' : 'Mixed'"
@change="formatTelemetry"

View File

@ -47,7 +47,7 @@
</template>
<script>
import DetailText from '@/ui/inspector/details/DetailText.vue';
import DetailText from '../inspectorViews/properties/DetailText.vue';
export default {
name: 'FaultManagementInspector',

View File

@ -30,7 +30,7 @@ export default function FaultManagementInspectorViewProvider(openmct) {
return {
openmct: openmct,
key: FAULT_MANAGEMENT_INSPECTOR,
name: 'FAULT_MANAGEMENT_TYPE',
name: 'Fault Management Configuration',
canView: (selection) => {
if (selection.length !== 1 || selection[0].length === 0) {
return false;
@ -64,8 +64,8 @@ export default function FaultManagementInspectorViewProvider(openmct) {
}
};
},
priority: () => {
return 1;
priority: function () {
return openmct.priority.HIGH + 1;
}
};
}

View File

@ -86,8 +86,9 @@ describe("The Fault Management Plugin", () => {
}
]];
const applicableInspectorViews = openmct.inspectorViews.get(faultDomainObjectSelection);
const faultManagementInspectorView = applicableInspectorViews.filter(view => view.name === 'Fault Management Configuration');
expect(applicableInspectorViews.length).toEqual(1);
expect(faultManagementInspectorView.length).toEqual(1);
});
it('creates a root object for fault management', async () => {

View File

@ -31,19 +31,17 @@ define([
function FiltersInspectorViewProvider(openmct, supportedObjectTypesArray) {
return {
key: 'filters-inspector',
name: 'Filters Inspector View',
name: 'Filters',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
const domainObject = selection?.[0]?.[0]?.context?.item;
let object = selection[0][0].context.item;
return object && supportedObjectTypesArray.some(type => object.type === type);
return domainObject && supportedObjectTypesArray.some(type => domainObject.type === type);
},
view: function (selection) {
let component;
const domainObject = selection?.[0]?.[0]?.context?.item;
return {
show: function (element) {
component = new Vue({
@ -57,6 +55,12 @@ define([
template: '<filters-view></filters-view>'
});
},
showTab: function (isEditing) {
const hasPersistedFilters = Boolean(domainObject?.configuration?.filters);
const hasGlobalFilters = Boolean(domainObject?.configuration?.globalFilters);
return hasPersistedFilters || hasGlobalFilters;
},
destroy: function () {
if (component) {
component.$destroy();
@ -66,7 +70,7 @@ define([
};
},
priority: function () {
return 1;
return openmct.priority.DEFAULT;
}
};
}

View File

@ -53,7 +53,7 @@
</template>
<script>
import TagEditor from '../../components/tags/TagEditor.vue';
import TagEditor from './tags/TagEditor.vue';
import _ from 'lodash';
export default {

View File

@ -0,0 +1,62 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 Annotations from './AnnotationsInspectorView.vue';
import Vue from 'vue';
export default function ElementsViewProvider(openmct) {
return {
key: 'annotationsView',
name: 'Annotations',
canView: function (selection) {
return selection.length;
},
view: function (selection) {
let component;
const domainObject = selection?.[0]?.[0]?.context?.item;
return {
show: function (el) {
component = new Vue({
el,
components: {
Annotations
},
provide: {
openmct,
domainObject
},
template: `<Annotations />`
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return this.openmct.priority.DEFAULT;
}
};
}

View File

@ -58,7 +58,7 @@
<script>
import AutoCompleteField from '../../../api/forms/components/controls/AutoCompleteField.vue';
import AutoCompleteField from '../../../../api/forms/components/controls/AutoCompleteField.vue';
export default {
components: {

View File

@ -42,7 +42,7 @@
></span>
<object-label
:domain-object="elementObject"
:object-path="[elementObject, parentObject]"
:object-path="[elementObject, domainObject]"
@context-click-active="setContextClickState"
/>
</div>
@ -50,13 +50,16 @@
</template>
<script>
import ObjectLabel from '../components/ObjectLabel.vue';
import ObjectLabel from '../../../ui/components/ObjectLabel.vue';
export default {
components: {
ObjectLabel
},
inject: ['openmct'],
inject: [
'openmct',
'domainObject'
],
props: {
index: {
type: Number,
@ -72,19 +75,12 @@ export default {
return {};
}
},
parentObject: {
type: Object,
required: true,
default: () => {
return {};
}
},
allowDrop: {
type: Boolean
}
},
data() {
const isAlias = this.elementObject.location !== this.openmct.objects.makeKeyString(this.parentObject.identifier);
const isAlias = this.elementObject.location !== this.openmct.objects.makeKeyString(this.domainObject.identifier);
return {
contextClickActive: false,

View File

@ -41,7 +41,6 @@
:key="element.identifier.key"
:index="index"
:element-object="element"
:parent-object="parentObject"
:allow-drop="allowDrop"
@dragstart-custom="moveFrom(index)"
@drop-custom="moveTo(index)"
@ -60,7 +59,7 @@
<script>
import _ from 'lodash';
import Search from '../components/search.vue';
import Search from '../../../ui/components/search.vue';
import ElementItem from './ElementItem.vue';
export default {
@ -68,12 +67,14 @@ export default {
Search,
ElementItem
},
inject: ['openmct'],
inject: [
'openmct',
'domainObject'
],
data() {
return {
elements: [],
isEditing: this.openmct.editor.isEditing(),
parentObject: undefined,
currentSearch: '',
selection: [],
contextClickTracker: {},
@ -111,14 +112,13 @@ export default {
this.elements = [];
this.elementsCache = {};
this.listeners = [];
this.parentObject = selection && selection[0] && selection[0][0].context.item;
if (this.compositionUnlistener) {
this.compositionUnlistener();
}
if (this.parentObject) {
this.composition = this.openmct.composition.get(this.parentObject);
if (this.domainObject) {
this.composition = this.openmct.composition.get(this.domainObject);
if (this.composition) {
this.composition.load();
@ -152,7 +152,7 @@ export default {
},
applySearch(input) {
this.currentSearch = input;
this.elements = this.parentObject.composition.map((id) =>
this.elements = this.domainObject.composition.map((id) =>
this.elementsCache[this.openmct.objects.makeKeyString(id)]
).filter((element) => {
return element !== undefined

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 ElementsPool from './ElementsPool.vue';
import Vue from 'vue';
export default function ElementsViewProvider(openmct) {
return {
key: 'elementsView',
name: 'Elements',
canView: function (selection) {
const hasValidSelection = selection?.length;
const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay';
return hasValidSelection && !isOverlayPlot;
},
view: function (selection) {
let component;
const domainObject = selection?.[0]?.[0]?.context?.item;
return {
show: function (el) {
component = new Vue({
el,
components: {
ElementsPool
},
provide: {
openmct,
domainObject
},
template: `<ElementsPool />`
});
},
showTab: function (isEditing) {
const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject));
return hasComposition && isEditing;
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return this.openmct.priority.DEFAULT;
}
};
}

View File

@ -77,10 +77,10 @@
<script>
import _ from 'lodash';
import Search from '../components/search.vue';
import Search from '../../../ui/components/search.vue';
import ElementItem from './ElementItem.vue';
import ElementItemGroup from './ElementItemGroup.vue';
import configStore from '../../plugins/plot/configuration/ConfigStore';
import configStore from '../../plot/configuration/ConfigStore';
const Y_AXIS_1 = 1;

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 PlotElementsPool from './PlotElementsPool.vue';
import Vue from 'vue';
export default function PlotElementsViewProvider(openmct) {
return {
key: 'plotElementsView',
name: 'Elements',
canView: function (selection) {
return selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay';
},
view: function (selection) {
let component;
const domainObject = selection?.[0]?.[0]?.context?.item;
return {
show: function (el) {
component = new Vue({
el,
components: {
PlotElementsPool
},
provide: {
openmct,
domainObject
},
template: `<PlotElementsPool />`
});
},
showTab: function (isEditing) {
const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject));
return hasComposition && isEditing;
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return this.openmct.priority.DEFAULT;
}
};
}

View File

@ -0,0 +1,37 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 PropertiesViewProvider from './properties/PropertiesViewProvider';
import ElementsViewProvider from './elements/ElementsViewProvider';
import PlotElementsViewProvider from './elements/PlotElementsViewProvider';
import StylesInspectorViewProvider from './styles/StylesInspectorViewProvider';
import AnnotationsViewProvider from './annotations/AnnotationsViewProvider';
export default function InspectorViewsPlugin() {
return function install(openmct) {
openmct.inspectorViews.addProvider(new PropertiesViewProvider(openmct));
openmct.inspectorViews.addProvider(new ElementsViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlotElementsViewProvider(openmct));
openmct.inspectorViews.addProvider(new StylesInspectorViewProvider(openmct));
openmct.inspectorViews.addProvider(new AnnotationsViewProvider(openmct));
};
}

View File

@ -22,7 +22,6 @@
<template>
<div
v-if="originalPath.length"
class="c-inspect-properties c-inspect-properties--location"
>
<div
@ -32,16 +31,14 @@
Original Location
</div>
<ul
v-if="!multiSelect"
class="c-inspect-properties__section"
>
<li
v-if="originalPath.length"
class="c-inspect-properties__row"
>
<ul class="c-inspect-properties__value c-location">
<li
v-for="pathObject in orderedOriginalPath"
v-for="pathObject in orderedPathBreadCrumb"
:key="pathObject.key"
class="c-location__item"
>
@ -53,53 +50,58 @@
</ul>
</li>
</ul>
<div
v-if="multiSelect"
class="c-inspect-properties__row--span-all"
>
No location to display for multiple items
</div>
</div>
</template>
<script>
import ObjectLabel from '../components/ObjectLabel.vue';
import ObjectLabel from '../../../ui/components/ObjectLabel.vue';
export default {
components: {
ObjectLabel
},
inject: ['openmct'],
inject: [
'openmct'
],
props: {
domainObject: {
type: Object,
default: undefined
},
parentDomainObject: {
type: Object,
default: undefined
}
},
data() {
return {
domainObject: {},
multiSelect: false,
originalPath: [],
keyString: ''
pathBreadCrumb: []
};
},
computed: {
orderedOriginalPath() {
return this.originalPath.slice().reverse();
orderedPathBreadCrumb() {
return this.pathBreadCrumb.slice().reverse();
}
},
mounted() {
this.openmct.selection.on('change', this.updateSelection);
this.updateSelection(this.openmct.selection.get());
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
async mounted() {
await this.createPathBreadCrumb();
},
methods: {
setOriginalPath(path, skipSlice) {
let originalPath = path;
async createPathBreadCrumb() {
if (!this.domainObject && this.parentDomainObject) {
this.setPathBreadCrumb([this.parentDomainObject]);
} else {
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
const originalPath = await this.openmct.objects.getOriginalPath(keyString);
const originalPathWithoutSelf = originalPath.slice(1, -1);
if (!skipSlice) {
originalPath = path.slice(1, -1);
this.setPathBreadCrumb(originalPathWithoutSelf);
}
this.originalPath = originalPath.map((domainObject, index, pathArray) => {
let key = this.openmct.objects.makeKeyString(domainObject.identifier);
},
setPathBreadCrumb(path) {
const pathBreadCrumb = path.map((domainObject, index, pathArray) => {
const key = this.openmct.objects.makeKeyString(domainObject.identifier);
return {
domainObject,
@ -107,46 +109,8 @@ export default {
objectPath: pathArray.slice(index)
};
});
},
clearData() {
this.domainObject = {};
this.originalPath = [];
this.keyString = '';
},
updateSelection(selection) {
if (!selection.length || !selection[0].length) {
this.clearData();
return;
}
if (selection.length > 1) {
this.multiSelect = true;
return;
} else {
this.multiSelect = false;
}
this.domainObject = selection[0][0].context.item;
let parentObject = selection[0][1];
if (!this.domainObject && parentObject && parentObject.context.item) {
this.setOriginalPath([parentObject.context.item], true);
this.keyString = '';
return;
}
let keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
if (keyString && this.keyString !== keyString) {
this.keyString = keyString;
this.originalPath = [];
this.openmct.objects.getOriginalPath(this.keyString)
.then(this.setOriginalPath);
}
this.pathBreadCrumb = pathBreadCrumb;
}
}
};

View File

@ -21,38 +21,48 @@
*****************************************************************************/
<template>
<div class="c-inspector__properties c-inspect-properties">
<div class="c-inspect-properties__header">
Details
</div>
<ul
v-if="hasDetails"
class="c-inspect-properties__section"
>
<Component
:is="getComponent(detail)"
v-for="detail in details"
:key="detail.name"
:detail="detail"
/>
<div>
<div class="c-inspector__properties c-inspect-properties">
<div class="c-inspect-properties__header">
Details
</div>
<ul
v-if="hasDetails"
class="c-inspect-properties__section"
>
<Component
:is="getComponent(detail)"
v-for="detail in details"
:key="detail.name"
:detail="detail"
/>
</ul>
<div
v-else
class="c-inspect-properties__row--span-all"
>
{{ noDetailsMessage }}
</ul>
<div
v-else
class="c-inspect-properties__row--span-all"
>
{{ noDetailsMessage }}
</div>
</div>
<Location
v-if="hasLocation"
:domain-object="domainObject"
:parent-domain-object="parentDomainObject"
/>
</div>
</template>
<script>
import Moment from 'moment';
import DetailText from './DetailText.vue';
import Location from './Location.vue';
export default {
components: {
DetailText
DetailText,
Location
},
inject: ['openmct'],
data() {
@ -62,21 +72,16 @@ export default {
},
computed: {
details() {
return this.customDetails ? this.customDetails : this.domainObjectDetails;
return this.customDetails ?? this.domainObjectDetails;
},
customDetails() {
if (this.context === undefined) {
return;
}
return this.context.details;
return this.context?.details;
},
domainObject() {
if (this.context === undefined) {
return;
}
return this.context.item;
return this.context?.item;
},
parentDomainObject() {
return this.selection?.[0]?.[1]?.context?.item;
},
type() {
if (this.domainObject === undefined) {
@ -162,20 +167,11 @@ export default {
return [...details, ...this.typeProperties];
},
context() {
if (
!this.selection
|| !this.selection.length
|| !this.selection[0].length
) {
return;
}
return this.selection[0][0].context;
return this.selection?.[0]?.[0]?.context;
},
hasDetails() {
return Boolean(
this.details
&& this.details.length
this.details?.length
&& !this.multiSelection
);
},
@ -227,6 +223,13 @@ export default {
}, this.domainObject)
};
});
},
hasLocation() {
const domainObject = this.selection?.[0]?.[0]?.context?.item;
const isRootObject = domainObject?.location === 'ROOT';
const hasSingleSelection = this.selection?.length === 1;
return hasSingleSelection && !isRootObject;
}
},
mounted() {

View File

@ -0,0 +1,60 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 Properties from './Properties.vue';
import Vue from 'vue';
export default function PropertiesViewProvider(openmct) {
return {
key: 'propertiesView',
name: 'Properties',
glyph: 'icon-info',
canView: function (selection) {
return selection.length > 0;
},
view: function (selection) {
let component;
return {
show: function (el) {
component = new Vue({
el,
components: {
Properties
},
provide: {
openmct
},
template: `<Properties />`
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return this.openmct.priority.DEFAULT;
}
};
}

View File

@ -29,7 +29,7 @@
import {
FONT_SIZES,
FONTS
} from '@/ui/inspector/styles/constants';
} from './constants';
export default {
inject: ['openmct'],

View File

@ -25,7 +25,7 @@
</template>
<script>
import SavedStylesView from '@/ui/inspector/styles/SavedStylesView.vue';
import SavedStylesView from './SavedStylesView.vue';
import Vue from 'vue';
export default {

View File

@ -38,7 +38,7 @@
</template>
<script>
import SavedStyleSelector from '@/ui/inspector/styles/SavedStyleSelector.vue';
import SavedStyleSelector from './SavedStyleSelector.vue';
export default {
name: 'SavedStylesView',

View File

@ -21,51 +21,53 @@
*****************************************************************************/
<template>
<div class="u-contents"></div>
<multipane
type="vertical"
>
<pane class="c-inspector__styles">
<div class="u-contents">
<StylesView />
</div>
</pane>
<pane
v-if="isEditing"
class="c-inspector__saved-styles"
handle="before"
label="Saved Styles"
>
<SavedStylesInspectorView />
</pane>
</multipane>
</template>
<script>
import multipane from '../../../ui/layout/multipane.vue';
import pane from '../../../ui/layout/pane.vue';
import StylesView from '@/plugins/condition/components/inspector/StylesView.vue';
import Vue from 'vue';
import SavedStylesInspectorView from './SavedStylesInspectorView.vue';
export default {
inject: ['openmct', 'stylesManager'],
components: {
multipane,
pane,
StylesView,
SavedStylesInspectorView
},
inject: ['openmct'],
data() {
return {
selection: []
isEditing: this.openmct.editor.isEditing()
};
},
mounted() {
this.openmct.selection.on('change', this.updateSelection);
this.updateSelection(this.openmct.selection.get());
this.openmct.editor.on('isEditing', this.setEditMode);
},
destroyed() {
this.openmct.selection.off('change', this.updateSelection);
beforeDestroyed() {
this.openmct.editor.off('isEditing', this.setEditMode);
},
methods: {
updateSelection(selection) {
if (selection.length > 0 && selection[0].length > 0) {
if (this.component) {
this.component.$destroy();
this.component = undefined;
this.$el.innerHTML = '';
}
let viewContainer = document.createElement('div');
this.$el.append(viewContainer);
this.component = new Vue({
el: viewContainer,
components: {
StylesView
},
provide: {
openmct: this.openmct,
selection: selection,
stylesManager: this.stylesManager
},
template: '<styles-view/>'
});
}
setEditMode(isEditing) {
this.isEditing = isEditing;
}
}
};

View File

@ -0,0 +1,89 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 stylesManager from './StylesManager';
import StylesInspectorView from './StylesInspectorView.vue';
import Vue from 'vue';
const NON_STYLABLE_TYPES = ['folder', 'webPage', 'conditionSet', 'summary-widget', 'hyperlink'];
function isLayoutObject(selection, objectType) {
//we allow conditionSets to be styled if they're part of a layout
return selection.length > 1
&& ((objectType === 'conditionSet') || (NON_STYLABLE_TYPES.indexOf(objectType) < 0));
}
function isCreatableObject(object, type) {
return (NON_STYLABLE_TYPES.indexOf(object.type) < 0) && type.definition.creatable;
}
export default function StylesInspectorViewProvider(openmct) {
return {
key: 'stylesInspectorView',
name: 'Styles',
glyph: 'icon-paint-bucket',
canView: function (selection) {
const objectSelection = selection?.[0];
const layoutItem = objectSelection?.[0]?.context?.layoutItem;
const domainObject = objectSelection?.[0]?.context?.item;
if (layoutItem) {
return true;
}
if (!domainObject) {
return false;
}
const type = openmct.types.get(domainObject.type);
return isLayoutObject(objectSelection, domainObject.type) || isCreatableObject(domainObject, type);
},
view: function (selection) {
let component;
return {
show: function (el) {
component = new Vue({
el,
components: {
StylesInspectorView
},
provide: {
openmct,
stylesManager,
selection
},
template: `<StylesInspectorView />`
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return this.openmct.priority.DEFAULT;
}
};
}

View File

@ -63,7 +63,7 @@ export default function PlanInspectorViewProvider(openmct) {
};
},
priority: function () {
return 1;
return openmct.priority.HIGH + 1;
}
};
}

View File

@ -23,7 +23,7 @@
import {createOpenMct, resetApplicationState} from "utils/testing";
import PlanPlugin from "../plan/plugin";
import Vue from 'vue';
import Properties from "@/ui/inspector/details/Properties.vue";
import Properties from "../inspectorViews/properties/Properties.vue";
describe('the plugin', function () {
let planDefinition;
@ -264,7 +264,7 @@ 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(6);
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');

View File

@ -29,7 +29,10 @@
class="c-tree"
aria-label="Plot Series Properties"
>
<h2 title="Plot series display properties in this object">Plot Series</h2>
<h2
class="--first"
title="Plot series display properties in this object"
>Plot Series</h2>
<plot-options-item
v-for="series in plotSeries"
:key="series.key"
@ -101,7 +104,10 @@
<ul
class="l-inspector-part js-legend-properties"
>
<h2 title="Legend settings for this object">Legend</h2>
<h2
class="--first"
title="Legend settings for this object"
>Legend</h2>
<li class="grid-row">
<div
class="grid-cell label"

View File

@ -29,7 +29,10 @@
class="c-tree"
aria-label="Plot Series Properties"
>
<h2 title="Display properties for this object">Plot Series</h2>
<h2
class="--first"
title="Display properties for this object"
>Plot Series</h2>
<li
v-for="series in plotSeries"
:key="series.key"
@ -52,7 +55,10 @@
v-if="isStackedPlotObject || !isStackedPlotNestedObject"
class="l-inspector-part"
>
<h2 title="Legend options">Legend</h2>
<h2
class="--first"
title="Legend options"
>Legend</h2>
<legend-form
class="grid-properties"
:legend="config.legend"

View File

@ -5,7 +5,7 @@ import Vue from 'vue';
export default function PlotsInspectorViewProvider(openmct) {
return {
key: 'plots-inspector',
name: 'Plots Inspector View',
name: 'Config',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
@ -53,7 +53,7 @@ export default function PlotsInspectorViewProvider(openmct) {
};
},
priority: function () {
return 1;
return openmct.priority.HIGH + 1;
}
};
}

View File

@ -5,7 +5,7 @@ import Vue from 'vue';
export default function StackedPlotsInspectorViewProvider(openmct) {
return {
key: 'stacked-plots-inspector',
name: 'Stacked Plots Inspector View',
name: 'Config',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
@ -51,7 +51,7 @@ export default function StackedPlotsInspectorViewProvider(openmct) {
};
},
priority: function () {
return 1;
return openmct.priority.HIGH + 1;
}
};
}

View File

@ -279,8 +279,10 @@ describe("the plugin", function () {
}
]
];
const plotInspectorView = openmct.inspectorViews.get(selection);
expect(plotInspectorView.length).toEqual(1);
const applicableInspectorViews = openmct.inspectorViews.get(selection);
const plotInspectorView = applicableInspectorViews.find(view => view.name = 'Plots Configuration');
expect(plotInspectorView).toBeDefined();
});
it("provides a stacked plot view for objects with telemetry", () => {

View File

@ -82,7 +82,8 @@ define([
'./gauge/GaugePlugin',
'./timelist/plugin',
'./faultManagement/FaultManagementPlugin',
'../../example/exampleTags/plugin'
'../../example/exampleTags/plugin',
'./inspectorViews/plugin'
], function (
_,
UTCTimeSystem,
@ -145,7 +146,8 @@ define([
GaugePlugin,
TimeList,
FaultManagementPlugin,
ExampleTags
ExampleTags,
InspectorViews
) {
const plugins = {};
@ -229,6 +231,7 @@ define([
plugins.OperatorStatus = OperatorStatus.default;
plugins.Gauge = GaugePlugin.default;
plugins.Timelist = TimeList.default;
plugins.InspectorViews = InspectorViews.default;
return plugins;
});

View File

@ -35,7 +35,7 @@ define([
function TableConfigurationViewProvider(openmct) {
return {
key: 'table-configuration',
name: 'Telemetry Table Configuration',
name: 'Configuration',
canView: function (selection) {
if (selection.length !== 1 || selection[0].length === 0) {
return false;

View File

@ -2,7 +2,7 @@
<div class="c-inspect-properties">
<template v-if="isEditing">
<div class="c-inspect-properties__header">
Table Layout
Layout
</div>
<ul class="c-inspect-properties__section">
<li class="c-inspect-properties__row">
@ -39,7 +39,7 @@
</li>
</ul>
<div class="c-inspect-properties__header">
Table Column Visibility
Columns
</div>
<ul class="c-inspect-properties__section">
<li

View File

@ -64,7 +64,7 @@ export default function TimeListInspectorViewProvider(openmct) {
};
},
priority: function () {
return 1;
return openmct.priority.HIGH + 1;
}
};
}

View File

@ -289,6 +289,13 @@ $colorInspectorPropVal: pullForward($colorInspectorFg, 15%);
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%);
$colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);
// Tabs
$colorTabBg: pullForward($colorBodyBg, 5%);
$colorTabFg: pullForward($colorBodyFg, 0%);
$colorTabCurrentBg: pullForward($colorTabBg, 10%);
$colorTabCurrentFg: pullForward($colorTabFg, 20%);
$colorTabsBaseline: $colorTabCurrentBg;
// Overlay
$colorOvrBlocker: rgba(black, 0.7);
$overlayCr: $interiorMargin;
@ -341,7 +348,7 @@ $colorItemFg: $colorBtnFg;
$colorItemFgDetails: pushBack($colorItemFg, 20%);
$shdwItemText: none;
// Tabular
// Tabular (NOT TABS!)
$colorTabBorder: pullForward($colorBodyBg, 10%);
$colorTabBodyBg: $colorBodyBg;
$colorTabBodyFg: pullForward($colorBodyFg, 20%);

View File

@ -293,6 +293,13 @@ $colorInspectorPropVal: pullForward($colorInspectorFg, 15%);
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%);
$colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);
// Tabs
$colorTabBg: pullForward($colorBodyBg, 5%);
$colorTabFg: pullForward($colorBtnFg, 10%);
$colorTabCurrentBg: pullForward($colorTabBg, 10%);
$colorTabCurrentFg: pullForward($colorTabFg, 10%);
$colorTabsBaseline: $colorTabCurrentBg;
// Overlay
$colorOvrBlocker: rgba(black, 0.7);
$overlayCr: $interiorMarginLg;

View File

@ -289,7 +289,14 @@ $colorInspectorPropVal: pullForward($colorInspectorFg, 15%);
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%);
$colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);
// Overlay
// Tabs
$colorTabBg: pullForward($colorBodyBg, 15%);
$colorTabFg: pullForward($colorTabBg, 60%);
$colorTabCurrentBg: $colorBodyFg; //pullForward($colorTabBg, 10%);
$colorTabCurrentFg: $colorBodyBg; //pullForward($colorTabFg, 10%);
$colorTabsBaseline: $colorTabCurrentBg;
// Overlay
$colorOvrBlocker: rgba(black, 0.7);
$overlayCr: $interiorMarginLg;

View File

@ -529,7 +529,7 @@ select {
display: block;
height: 1px;
width: 100%;
background: $colorBtnReverseBg;
background: $colorTabsBaseline;
position: absolute;
bottom: 0px;
z-index: 1;
@ -548,8 +548,8 @@ select {
100% 100%,
0% 100%
);
background: rgba($colorBtnBg, 0.7);
color: $colorBtnFg;
background: $colorTabBg;
color: $colorTabFg;
cursor: pointer;
display: flex;
align-items: center;
@ -569,8 +569,8 @@ select {
}
&.is-current {
background: $colorBtnReverseBg;
color: $colorBtnReverseFg;
background: $colorTabCurrentBg;
color: $colorTabCurrentFg;
pointer-events: none;
}
}

View File

@ -35,13 +35,13 @@
@import "../ui/components/progress-bar.scss";
@import "../ui/components/search.scss";
@import "../ui/components/swim-lane/swimlane.scss";
@import "../ui/components/tags/tags.scss";
@import "../plugins/inspectorViews/annotations/tags/tags.scss";
@import "../ui/components/toggle-switch.scss";
@import "../ui/components/timesystem-axis.scss";
@import "../ui/components/List/list-view.scss";
@import "../ui/inspector/elements.scss";
@import "../plugins/inspectorViews/elements/elements.scss";
@import "../ui/inspector/inspector.scss";
@import "../ui/inspector/location.scss";
@import "../plugins/inspectorViews/properties/location.scss";
@import "../ui/layout/app-logo.scss";
@import "../ui/layout/create-button.scss";
@import "../ui/layout/layout.scss";

View File

@ -23,111 +23,30 @@
<template>
<div class="c-inspector">
<object-name />
<div
v-if="showStyles"
class="c-inspector__tabs c-tabs"
>
<div
v-for="tabbedView in tabbedViews"
:key="tabbedView.key"
class="c-inspector__tab c-tab"
:class="{'is-current': isCurrent(tabbedView)}"
@click="updateCurrentTab(tabbedView)"
>
{{ tabbedView.name }}
</div>
</div>
<div class="c-inspector__content">
<multipane
v-show="currentTabbedView.key === '__properties'"
type="vertical"
>
<pane class="c-inspector__properties">
<Properties v-if="!activity" />
<div
v-if="!multiSelect"
class="c-inspect-properties c-inspect-properties--location"
>
</div>
<inspector-views />
</pane>
<pane
v-if="isEditing && hasComposition"
class="c-inspector__elements"
handle="before"
label="Elements"
>
<plot-elements-pool
v-if="isOverlayPlot"
/>
<elements-pool
v-else
/>
</pane>
</multipane>
<multipane
v-show="currentTabbedView.key === '__styles'"
type="vertical"
>
<pane class="c-inspector__styles">
<StylesInspectorView />
</pane>
<pane
v-if="isEditing"
class="c-inspector__saved-styles"
handle="before"
label="Saved Styles"
>
<SavedStylesInspectorView :is-editing="isEditing" />
</pane>
</multipane>
<multipane
v-show="currentTabbedView.key === '__annotations'"
type="vertical"
>
<pane class="c-inspector__annotations">
<AnnotationsInspectorView
@annotationCreated="updateCurrentTab(tabbedViews[2])"
/>
</pane>
</multipane>
</div>
<InspectorTabs
:selection="selection"
:is-editing="isEditing"
@select-tab="selectTab"
/>
<InspectorViews
:selection="selection"
:selected-tab="selectedTab"
/>
</div>
</template>
<script>
import multipane from '../layout/multipane.vue';
import pane from '../layout/pane.vue';
import ElementsPool from './ElementsPool.vue';
import PlotElementsPool from './PlotElementsPool.vue';
import Properties from './details/Properties.vue';
import ObjectName from './ObjectName.vue';
import InspectorTabs from './InspectorTabs.vue';
import InspectorViews from './InspectorViews.vue';
import _ from "lodash";
import stylesManager from "@/ui/inspector/styles/StylesManager";
import StylesInspectorView from "@/ui/inspector/styles/StylesInspectorView.vue";
import SavedStylesInspectorView from "@/ui/inspector/styles/SavedStylesInspectorView.vue";
import AnnotationsInspectorView from "./annotations/AnnotationsInspectorView.vue";
const OVERLAY_PLOT_TYPE = "telemetry.plot.overlay";
export default {
components: {
StylesInspectorView,
SavedStylesInspectorView,
AnnotationsInspectorView,
multipane,
pane,
ElementsPool,
PlotElementsPool,
Properties,
ObjectName,
InspectorTabs,
InspectorViews
},
provide: {
stylesManager: stylesManager
},
inject: ["openmct"],
inject: ['openmct'],
props: {
isEditing: {
type: Boolean,
@ -136,116 +55,22 @@ export default {
},
data() {
return {
hasComposition: false,
multiSelect: false,
showStyles: false,
isOverlayPlot: false,
tabbedViews: [
{
key: "__properties",
name: "Properties"
},
{
key: "__styles",
name: "Styles"
},
{
key: "__annotations",
name: "Annotations"
}
],
currentTabbedView: {},
activity: undefined
selection: this.openmct.selection.get(),
selectedTab: undefined
};
},
mounted() {
this.excludeObjectTypes = [
"folder",
"webPage",
"conditionSet",
"summary-widget",
"hyperlink"
];
this.openmct.selection.on("change", this.updateInspectorViews);
this.openmct.selection.on('change', this.setSelection);
},
destroyed() {
this.openmct.selection.off("change", this.updateInspectorViews);
this.openmct.selection.off('change', this.setSelection);
},
methods: {
updateInspectorViews(selection) {
this.refreshComposition(selection);
if (this.openmct.types.get("conditionSet")) {
this.refreshTabs(selection);
}
if (selection.length > 1) {
this.multiSelect = true;
// return;
} else {
this.multiSelect = false;
}
this.setActivity(selection);
setSelection(selection) {
this.selection = selection;
},
refreshComposition(selection) {
if (selection.length > 0 && selection[0].length > 0) {
const parentObject = selection[0][0].context.item;
this.hasComposition = Boolean(
parentObject && this.openmct.composition.get(parentObject)
);
this.isOverlayPlot = parentObject?.type === OVERLAY_PLOT_TYPE;
}
},
refreshTabs(selection) {
if (selection.length > 0 && selection[0].length > 0) {
//layout items are not domain objects but should allow conditional styles
this.showStyles = selection[0][0].context.layoutItem;
let object = selection[0][0].context.item;
if (object) {
let type = this.openmct.types.get(object.type);
this.showStyles =
this.isLayoutObject(selection[0], object.type)
|| this.isCreatableObject(object, type);
}
if (
!this.currentTabbedView.key
|| (!this.showStyles
&& this.currentTabbedView.key === this.tabbedViews[1].key)
) {
this.updateCurrentTab(this.tabbedViews[0]);
}
}
},
isLayoutObject(selection, objectType) {
//we allow conditionSets to be styled if they're part of a layout
return (
selection.length > 1
&& (objectType === "conditionSet"
|| this.excludeObjectTypes.indexOf(objectType) < 0)
);
},
isCreatableObject(object, type) {
return (
this.excludeObjectTypes.indexOf(object.type) < 0
&& type.definition.creatable
);
},
updateCurrentTab(view) {
this.currentTabbedView = view;
},
isCurrent(view) {
return _.isEqual(this.currentTabbedView, view);
},
setActivity(selection) {
this.activity =
selection
&& selection.length
&& selection[0].length
&& selection[0][0].activity;
selectTab(tab) {
this.selectedTab = tab;
}
}
};

View File

@ -36,8 +36,8 @@ import {
} from './InspectorStylesSpecMocks';
import Vue from 'vue';
import StylesView from '@/plugins/condition/components/inspector/StylesView.vue';
import SavedStylesView from '@/ui/inspector/styles/SavedStylesView.vue';
import stylesManager from '@/ui/inspector/styles/StylesManager';
import SavedStylesView from '../../plugins/inspectorViews/styles/SavedStylesView.vue';
import stylesManager from '../../plugins/inspectorViews/styles/StylesManager';
describe("the inspector", () => {
let openmct;

View File

@ -0,0 +1,115 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
class="c-inspector__tabs c-tabs"
role="tablist"
>
<div
v-for="tab in visibleTabs"
:key="tab.key"
role="tab"
class="c-inspector__tab c-tab"
:class="{'is-current': isSelected(tab)}"
:title="tab.name"
@click="selectTab(tab)"
>
<span
class="c-inspector__tab-name c-tab__name"
>{{ tab.name }}</span>
</div>
</div>
</template>
<script>
export default {
inject: ['openmct'],
props: {
selection: {
type: Array,
default: () => {
return [];
}
},
isEditing: {
type: Boolean,
required: true
}
},
selection: {
type: Array,
default: []
},
data() {
return {
tabs: [],
selectedTab: undefined
};
},
computed: {
visibleTabs() {
return this.tabs
.filter(tab => {
return tab.showTab === undefined || tab.showTab(this.isEditing);
});
}
},
watch: {
selection() {
this.updateSelection();
},
visibleTabs() {
this.selectDefaultTabIfSelectedNotVisible();
}
},
methods: {
updateSelection() {
const inspectorViews = this.openmct.inspectorViews.get(this.selection);
this.tabs = inspectorViews.map(view => {
return {
key: view.key,
name: view.name,
glyph: view.glyph ?? 'icon-object',
showTab: view.showTab
};
});
},
isSelected(tab) {
return this.selectedTab?.key === tab.key;
},
selectTab(tab) {
this.selectedTab = tab;
this.$emit('select-tab', tab);
},
selectDefaultTabIfSelectedNotVisible() {
const selectedTabIsVisible = this.visibleTabs.some(tab => this.isSelected(tab));
if (!selectedTabIsVisible) {
this.selectTab(this.visibleTabs[0]);
}
}
}
};
</script>

View File

@ -21,41 +21,65 @@
*****************************************************************************/
<template>
<div aria-label="Inspector Views"></div>
<div
class="c-inspector__content"
role="tabpanel"
aria-label="Inspector Views"
></div>
</template>
<script>
export default {
inject: ['openmct'],
data() {
return {
selection: []
};
props: {
selectedTab: {
type: Object,
default: undefined
},
selection: {
type: Array,
default: () => {
return [];
}
}
},
mounted() {
this.openmct.selection.on('change', this.updateSelection);
this.updateSelection(this.openmct.selection.get());
},
destroyed() {
this.openmct.selection.off('change', this.updateSelection);
watch: {
selection() {
this.updateSelectionViews();
},
selectedTab() {
this.clearAndShowViewsForTab();
}
},
methods: {
updateSelection(selection) {
this.selection = selection;
if (this.selectedViews) {
this.selectedViews.forEach(selectedView => {
selectedView.destroy();
updateSelectionViews(selection) {
this.clearViews();
this.selectedViews = this.openmct.inspectorViews.get(this.selection);
this.showViewsForTab();
},
clearViews() {
if (this.visibleViews) {
this.visibleViews.forEach(visibleView => {
visibleView.destroy();
});
this.visibleViews = [];
this.$el.innerHTML = '';
}
},
showViewsForTab() {
this.visibleViews = this.selectedViews
.filter(view => view.key === this.selectedTab.key);
this.selectedViews = this.openmct.inspectorViews.get(selection);
this.selectedViews.forEach(selectedView => {
this.visibleViews.forEach(visibleView => {
let viewContainer = document.createElement('div');
this.$el.append(viewContainer);
selectedView.show(viewContainer);
visibleView.show(viewContainer);
});
},
clearAndShowViewsForTab() {
this.clearViews();
this.showViewsForTab();
}
}
};

View File

@ -42,6 +42,41 @@
flex: 0 0 auto;
font-size: 0.8em;
text-transform: uppercase;
&.c-tabs {
flex-wrap: nowrap;
}
.c-tab {
background: $colorTabBg;
color: $colorTabFg;
padding: $interiorMargin;
&:not(.is-current) {
overflow: hidden;
&:after {
background-image: linear-gradient(90deg, transparent 0%, rgba($colorTabBg, 1) 70%);
content: '';
display: block;
position: absolute;
right: 0;
height: 100%;
width: 15px;
z-index: 1;
}
}
&.is-current {
background: $colorTabCurrentBg;
color: $colorTabCurrentFg;
padding-right: $interiorMargin + 3;
}
&__name {
overflow: hidden;
}
}
}
&__content {
@ -93,6 +128,11 @@
@include propertiesHeader();
font-size: 0.65rem;
grid-column: 1 / 3;
margin: $interiorMargin 0;
&.--first {
margin-top: 0;
}
}
.c-tree .grid-properties {

View File

@ -20,16 +20,17 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
const DEFAULT_VIEW_PRIORITY = 0;
/**
* A InspectorViewRegistry maintains the definitions for views
* that may occur in the inspector.
*
* @interface InspectorViewRegistry
* @memberof module:openmct
*/
function InspectorViewRegistry() {
/**
* A InspectorViewRegistry maintains the definitions for views
* that may occur in the inspector.
*
* @interface InspectorViewRegistry
* @memberof module:openmct
*/
export default class InspectorViewRegistry {
constructor() {
this.providers = {};
}
@ -40,18 +41,25 @@ define([], function () {
* which can provide views of this object
* @private for platform-internal use
*/
InspectorViewRegistry.prototype.get = function (selection) {
return this.getAllProviders().filter(function (provider) {
return provider.canView(selection);
}).map(provider => provider.view(selection));
};
get(selection) {
function byPriority(providerA, providerB) {
const priorityA = providerA.priority?.() ?? DEFAULT_VIEW_PRIORITY;
const priorityB = providerB.priority?.() ?? DEFAULT_VIEW_PRIORITY;
/**
* @private
*/
InspectorViewRegistry.prototype.getAllProviders = function () {
return Object.values(this.providers);
};
return priorityB - priorityA;
}
return this.#getAllProviders()
.filter(provider => provider.canView(selection))
.map(provider => {
const view = provider.view(selection);
view.key = provider.key;
view.name = provider.name;
view.glyph = provider.glyph;
return view;
}).sort(byPriority);
}
/**
* Registers a new type of view.
@ -60,90 +68,94 @@ define([], function () {
* @method addProvider
* @memberof module:openmct.InspectorViewRegistry#
*/
InspectorViewRegistry.prototype.addProvider = function (provider) {
addProvider(provider) {
const key = provider.key;
const name = provider.name;
if (key === undefined) {
throw "View providers must have a unique 'key' property defined";
}
if (name === undefined) {
throw "View providers must have a 'name' property defined";
}
if (this.providers[key] !== undefined) {
console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key);
console.warn(`Provider already defined for key '${key}'. Provider keys must be unique.`);
}
this.providers[key] = provider;
};
}
/**
* @private
*/
InspectorViewRegistry.prototype.getByProviderKey = function (key) {
getByProviderKey(key) {
return this.providers[key];
};
}
/**
* A View is used to provide displayable content, and to react to
* associated life cycle events.
*
* @name View
* @interface
* @memberof module:openmct
*/
#getAllProviders() {
return Object.values(this.providers);
}
}
/**
* Populate the supplied DOM element with the contents of this view.
*
* View implementations should use this method to attach any
* listeners or acquire other resources that are necessary to keep
* the contents of this view up-to-date.
*
* @param {HTMLElement} container the DOM element to populate
* @method show
* @memberof module:openmct.View#
*/
/**
* A View is used to provide displayable content, and to react to
* associated life cycle events.
*
* @name View
* @interface
* @memberof module:openmct
*/
/**
* Release any resources associated with this view.
*
* View implementations should use this method to detach any
* listeners or release other resources that are no longer necessary
* once a view is no longer used.
*
* @method destroy
* @memberof module:openmct.View#
*/
/**
* Populate the supplied DOM element with the contents of this view.
*
* View implementations should use this method to attach any
* listeners or acquire other resources that are necessary to keep
* the contents of this view up-to-date.
*
* @param {HTMLElement} container the DOM element to populate
* @method show
* @memberof module:openmct.View#
*/
/**
* Exposes types of views in inspector.
*
* @interface InspectorViewProvider
* @property {string} key a unique identifier for this view
* @property {string} name the human-readable name of this view
* @property {string} [description] a longer-form description (typically
* a single sentence or short paragraph) of this kind of view
* @property {string} [cssClass] the CSS class to apply to labels for this
* view (to add icons, for instance)
* @memberof module:openmct
*/
/**
* Release any resources associated with this view.
*
* View implementations should use this method to detach any
* listeners or release other resources that are no longer necessary
* once a view is no longer used.
*
* @method destroy
* @memberof module:openmct.View#
*/
/**
* Checks if this provider can supply views for a selection.
*
* @method canView
* @memberof module:openmct.InspectorViewProvider#
* @param {module:openmct.selection} selection
* @returns {boolean} 'true' if the view applies to the provided selection,
* otherwise 'false'.
*/
/**
* Exposes types of views in inspector.
*
* @interface InspectorViewProvider
* @property {string} key a unique identifier for this view
* @property {string} name the human-readable name of this view
* @property {string} [description] a longer-form description (typically
* a single sentence or short paragraph) of this kind of view
* @property {string} [cssClass] the CSS class to apply to labels for this
* view (to add icons, for instance)
* @memberof module:openmct
*/
/**
* Provides a view of the selection object in the inspector.
*
* @method view
* @memberof module:openmct.InspectorViewProvider#
* @param {module:openmct.selection} selection the selection object
* @returns {module:openmct.View} a view of this selection
*/
/**
* Checks if this provider can supply views for a selection.
*
* @method canView
* @memberof module:openmct.InspectorViewProvider#
* @param {module:openmct.selection} selection
* @returns {boolean} 'true' if the view applies to the provided selection,
* otherwise 'false'.
*/
return InspectorViewRegistry;
});
/**
* Provides a view of the selection object in the inspector.
*
* @method view
* @memberof module:openmct.InspectorViewProvider#
* @param {module:openmct.selection} selection the selection object
* @returns {module:openmct.View} a view of this selection
*/