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
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); 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 // eslint-disable-next-line no-undef
module.exports = { module.exports = {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
@ -396,5 +415,6 @@ module.exports = {
setFixedTimeMode, setFixedTimeMode,
setRealTimeMode, setRealTimeMode,
setStartOffset, setStartOffset,
setEndOffset setEndOffset,
selectInspectorTab
}; };

View File

@ -24,6 +24,7 @@
Testsuite for plot autoscale. Testsuite for plot autoscale.
*/ */
const { selectInspectorTab } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
test.use({ test.use({
viewport: { viewport: {
@ -50,6 +51,7 @@ test.describe('Autoscale', () => {
// enter edit mode // enter edit mode
await page.click('button[title="Edit"]'); await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Config');
await turnOffAutoscale(page); await turnOffAutoscale(page);
await setUserDefinedMinAndMax(page, '-2', '2'); 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 { test, expect } = require('../../../../pluginFixtures');
const { selectInspectorTab } = require('../../../../appActions');
test.describe('Log plot tests', () => { test.describe('Log plot tests', () => {
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page, openmctConfig }) => { test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig; const { myItemsFolderName } = openmctConfig;
@ -36,6 +38,7 @@ test.describe('Log plot tests', () => {
await makeOverlayPlot(page, myItemsFolderName); await makeOverlayPlot(page, myItemsFolderName);
await testRegularTicks(page); await testRegularTicks(page);
await enableEditMode(page); await enableEditMode(page);
await selectInspectorTab(page, 'Config');
await enableLogMode(page); await enableLogMode(page);
await testLogTicks(page); await testLogTicks(page);
await disableLogMode(page); await disableLogMode(page);
@ -186,6 +189,7 @@ async function enableEditMode(page) {
*/ */
async function enableLogMode(page) { async function enableLogMode(page) {
// turn on log mode // 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.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(); // 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) { async function disableLogMode(page) {
// turn off log mode // 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(); 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 { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions'); const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
test.describe('Overlay Plot', () => { test.describe('Overlay Plot', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@ -45,6 +45,8 @@ test.describe('Overlay Plot', () => {
await page.goto(overlayPlot.url); await page.goto(overlayPlot.url);
await selectInspectorTab(page, 'Config');
// navigate to plot series color palette // navigate to plot series color palette
await page.click('.l-browse-bar__actions__edit'); await page.click('.l-browse-bar__actions__edit');
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click(); 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.goto(overlayPlot.url);
await page.click('button[title="Edit"]'); await page.click('button[title="Edit"]');
// Expand the elements pool vertically await selectInspectorTab(page, 'Elements');
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();
// Drag swg a, c, e into Y Axis 2 // 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"]')); 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"]')); 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 // 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(yAxis1PropertyGroup).toBeVisible();
await expect(yAxis2PropertyGroup).toBeVisible(); await expect(yAxis2PropertyGroup).toBeVisible();
await expect(yAxis3PropertyGroup).toBeHidden(); await expect(yAxis3PropertyGroup).toBeHidden();
@ -120,15 +113,21 @@ test.describe('Overlay Plot', () => {
const yAxis2Group = page.getByLabel("Y Axis 2"); const yAxis2Group = page.getByLabel("Y Axis 2");
const yAxis3Group = page.getByLabel("Y Axis 3"); const yAxis3Group = page.getByLabel("Y Axis 3");
await selectInspectorTab(page, 'Elements');
// Drag swg b into Y Axis 3 // 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"]')); 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 // Assert that all Y Axis property groups are visible
await selectInspectorTab(page, 'Config');
await expect(yAxis1PropertyGroup).toBeVisible(); await expect(yAxis1PropertyGroup).toBeVisible();
await expect(yAxis2PropertyGroup).toBeVisible(); await expect(yAxis2PropertyGroup).toBeVisible();
await expect(yAxis3PropertyGroup).toBeVisible(); await expect(yAxis3PropertyGroup).toBeVisible();
// Verify that the elements are in the correct buckets and in the correct order // 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', { name: swgD.name })).toBeTruthy();
expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy(); expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: swgE.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.goto(overlayPlot.url);
await page.click('button[title="Edit"]'); await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Elements');
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click(); await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
await page.locator('.js-overlay canvas').nth(1);
const plotPixelSize = await getCanvasPixelsWithData(page); const plotPixelSize = await getCanvasPixelsWithData(page);
expect(plotPixelSize).toBeGreaterThan(0); 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. necessarily be used for reference when writing new tests in this area.
*/ */
const { selectInspectorTab } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
test.describe('Legend color in sync with plot color', () => { 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 // navigate to plot series color palette
await page.click('.l-browse-bar__actions__edit'); 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('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
await page.locator('.c-click-swatch--menu').click(); await page.locator('.c-click-swatch--menu').click();
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').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 { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions'); const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
const uuid = require('uuid').v4; const uuid = require('uuid').v4;
test.describe('Scatter Plot', () => { test.describe('Scatter Plot', () => {
@ -40,8 +40,8 @@ test.describe('Scatter Plot', () => {
}); });
test('Can add and remove telemetry sources', async ({ page }) => { test('Can add and remove telemetry sources', async ({ page }) => {
const editButtonLocator = page.locator('button[title="Edit"]'); const editButton = page.locator('button[title="Edit"]');
const saveButtonLocator = page.locator('button[title="Save"]'); const saveButton = page.locator('button[title="Save"]');
// Create a sine wave generator within the scatter plot // Create a sine wave generator within the scatter plot
const swg1 = await createDomainObjectWithDefaults(page, { const swg1 = await createDomainObjectWithDefaults(page, {
@ -53,9 +53,10 @@ test.describe('Scatter Plot', () => {
// Navigate to the scatter plot and verify that // Navigate to the scatter plot and verify that
// the SWG appears in the elements pool // the SWG appears in the elements pool
await page.goto(scatterPlot.url); 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 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(); await page.locator('li[title="Save and Finish Editing"]').click();
// Create another sine wave generator within the scatter plot // 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 // Navigate to the scatter plot and verify that the new SWG
// appears in the elements pool and the old one is gone // appears in the elements pool and the old one is gone
await page.goto(scatterPlot.url); 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=${swg1.name}`)).toBeHidden();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); 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 // Right click on the new SWG in the elements pool and delete it
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ 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 { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions'); const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
test.describe('Stacked Plot', () => { test.describe('Stacked Plot', () => {
let stackedPlot; let stackedPlot;
@ -65,11 +65,7 @@ test.describe('Stacked Plot', () => {
await page.click('button[title="Edit"]'); await page.click('button[title="Edit"]');
// Expand the elements pool vertically await selectInspectorTab(page, 'Elements');
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 swgBElementsPoolItem.click({ button: 'right' }); await swgBElementsPoolItem.click({ button: 'right' });
await page.getByRole('menuitem').filter({ hasText: /Remove/ }).click(); await page.getByRole('menuitem').filter({ hasText: /Remove/ }).click();
@ -92,11 +88,7 @@ test.describe('Stacked Plot', () => {
await page.click('button[title="Edit"]'); await page.click('button[title="Edit"]');
// Expand the elements pool vertically await selectInspectorTab(page, 'Elements');
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();
const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0); const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0);
const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1); 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 }) => { 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 page.goto(stackedPlot.url);
await selectInspectorTab(page, 'Config');
// Click on the 1st plot // Click on the 1st plot
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click(); 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 // Go into edit mode
await page.click('button[title="Edit"]'); await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Config');
// Click on canvas for the 1st plot // Click on canvas for the 1st plot
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click(); await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click();

View File

@ -24,7 +24,7 @@
*/ */
const { test, expect } = require('../../pluginFixtures'); const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions'); const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../appActions');
const { v4: uuid } = require('uuid'); const { v4: uuid } = require('uuid');
test.describe('Grand Search', () => { 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=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`); 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 // 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 expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import PlotOptions from "./PlotOptions.vue";
export default function ScatterPlotInspectorViewProvider(openmct) { export default function ScatterPlotInspectorViewProvider(openmct) {
return { return {
key: SCATTER_PLOT_INSPECTOR_KEY, key: SCATTER_PLOT_INSPECTOR_KEY,
name: 'Bar Graph Inspector View', name: 'Config',
canView: function (selection) { canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) { if (selection.length === 0 || selection[0].length === 0) {
return false; return false;
@ -42,7 +42,7 @@ export default function ScatterPlotInspectorViewProvider(openmct) {
}; };
}, },
priority: function () { 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. 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> </div>
<template v-if="!conditionSetDomainObject"> <template v-if="!conditionSetDomainObject">
<div class="c-inspect-styles__header">
Object Style
</div>
<FontStyleEditor <FontStyleEditor
v-if="canStyleFont" v-if="canStyleFont"
:font-style="consolidatedFontStyle" :font-style="consolidatedFontStyle"
@ -63,9 +60,6 @@
</div> </div>
</template> </template>
<template v-else> <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"> <div class="c-inspect-styles__content c-inspect-styles__condition-set c-inspect-styles__elem">
<a <a
v-if="conditionSetDomainObject" v-if="conditionSetDomainObject"
@ -156,7 +150,7 @@
<script> <script>
import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue'; import FontStyleEditor from '../../../inspectorViews/styles/FontStyleEditor.vue';
import StyleEditor from "./StyleEditor.vue"; import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js"; import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils"; import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";

View File

@ -22,7 +22,7 @@
import { createOpenMct, resetApplicationState } from "utils/testing"; import { createOpenMct, resetApplicationState } from "utils/testing";
import ConditionPlugin from "./plugin"; 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 StylesView from "./components/inspector/StylesView.vue";
import Vue from 'vue'; import Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils"; import {getApplicableStylesForItem} from "./utils/styleUtils";

View File

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

View File

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

View File

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

View File

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

View File

@ -86,8 +86,9 @@ describe("The Fault Management Plugin", () => {
} }
]]; ]];
const applicableInspectorViews = openmct.inspectorViews.get(faultDomainObjectSelection); 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 () => { it('creates a root object for fault management', async () => {

View File

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

View File

@ -53,7 +53,7 @@
</template> </template>
<script> <script>
import TagEditor from '../../components/tags/TagEditor.vue'; import TagEditor from './tags/TagEditor.vue';
import _ from 'lodash'; import _ from 'lodash';
export default { 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> <script>
import AutoCompleteField from '../../../api/forms/components/controls/AutoCompleteField.vue'; import AutoCompleteField from '../../../../api/forms/components/controls/AutoCompleteField.vue';
export default { export default {
components: { components: {

View File

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

View File

@ -41,7 +41,6 @@
:key="element.identifier.key" :key="element.identifier.key"
:index="index" :index="index"
:element-object="element" :element-object="element"
:parent-object="parentObject"
:allow-drop="allowDrop" :allow-drop="allowDrop"
@dragstart-custom="moveFrom(index)" @dragstart-custom="moveFrom(index)"
@drop-custom="moveTo(index)" @drop-custom="moveTo(index)"
@ -60,7 +59,7 @@
<script> <script>
import _ from 'lodash'; import _ from 'lodash';
import Search from '../components/search.vue'; import Search from '../../../ui/components/search.vue';
import ElementItem from './ElementItem.vue'; import ElementItem from './ElementItem.vue';
export default { export default {
@ -68,12 +67,14 @@ export default {
Search, Search,
ElementItem ElementItem
}, },
inject: ['openmct'], inject: [
'openmct',
'domainObject'
],
data() { data() {
return { return {
elements: [], elements: [],
isEditing: this.openmct.editor.isEditing(), isEditing: this.openmct.editor.isEditing(),
parentObject: undefined,
currentSearch: '', currentSearch: '',
selection: [], selection: [],
contextClickTracker: {}, contextClickTracker: {},
@ -111,14 +112,13 @@ export default {
this.elements = []; this.elements = [];
this.elementsCache = {}; this.elementsCache = {};
this.listeners = []; this.listeners = [];
this.parentObject = selection && selection[0] && selection[0][0].context.item;
if (this.compositionUnlistener) { if (this.compositionUnlistener) {
this.compositionUnlistener(); this.compositionUnlistener();
} }
if (this.parentObject) { if (this.domainObject) {
this.composition = this.openmct.composition.get(this.parentObject); this.composition = this.openmct.composition.get(this.domainObject);
if (this.composition) { if (this.composition) {
this.composition.load(); this.composition.load();
@ -152,7 +152,7 @@ export default {
}, },
applySearch(input) { applySearch(input) {
this.currentSearch = 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)] this.elementsCache[this.openmct.objects.makeKeyString(id)]
).filter((element) => { ).filter((element) => {
return element !== undefined 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> <script>
import _ from 'lodash'; import _ from 'lodash';
import Search from '../components/search.vue'; import Search from '../../../ui/components/search.vue';
import ElementItem from './ElementItem.vue'; import ElementItem from './ElementItem.vue';
import ElementItemGroup from './ElementItemGroup.vue'; import ElementItemGroup from './ElementItemGroup.vue';
import configStore from '../../plugins/plot/configuration/ConfigStore'; import configStore from '../../plot/configuration/ConfigStore';
const Y_AXIS_1 = 1; 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> <template>
<div <div
v-if="originalPath.length"
class="c-inspect-properties c-inspect-properties--location" class="c-inspect-properties c-inspect-properties--location"
> >
<div <div
@ -32,16 +31,14 @@
Original Location Original Location
</div> </div>
<ul <ul
v-if="!multiSelect"
class="c-inspect-properties__section" class="c-inspect-properties__section"
> >
<li <li
v-if="originalPath.length"
class="c-inspect-properties__row" class="c-inspect-properties__row"
> >
<ul class="c-inspect-properties__value c-location"> <ul class="c-inspect-properties__value c-location">
<li <li
v-for="pathObject in orderedOriginalPath" v-for="pathObject in orderedPathBreadCrumb"
:key="pathObject.key" :key="pathObject.key"
class="c-location__item" class="c-location__item"
> >
@ -53,53 +50,58 @@
</ul> </ul>
</li> </li>
</ul> </ul>
<div
v-if="multiSelect"
class="c-inspect-properties__row--span-all"
>
No location to display for multiple items
</div>
</div> </div>
</template> </template>
<script> <script>
import ObjectLabel from '../components/ObjectLabel.vue'; import ObjectLabel from '../../../ui/components/ObjectLabel.vue';
export default { export default {
components: { components: {
ObjectLabel ObjectLabel
}, },
inject: ['openmct'], inject: [
'openmct'
],
props: {
domainObject: {
type: Object,
default: undefined
},
parentDomainObject: {
type: Object,
default: undefined
}
},
data() { data() {
return { return {
domainObject: {}, pathBreadCrumb: []
multiSelect: false,
originalPath: [],
keyString: ''
}; };
}, },
computed: { computed: {
orderedOriginalPath() { orderedPathBreadCrumb() {
return this.originalPath.slice().reverse(); return this.pathBreadCrumb.slice().reverse();
} }
}, },
mounted() { async mounted() {
this.openmct.selection.on('change', this.updateSelection); await this.createPathBreadCrumb();
this.updateSelection(this.openmct.selection.get());
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
}, },
methods: { methods: {
setOriginalPath(path, skipSlice) { async createPathBreadCrumb() {
let originalPath = path; 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) { this.setPathBreadCrumb(originalPathWithoutSelf);
originalPath = path.slice(1, -1);
} }
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 { return {
domainObject, domainObject,
@ -107,46 +109,8 @@ export default {
objectPath: pathArray.slice(index) objectPath: pathArray.slice(index)
}; };
}); });
},
clearData() {
this.domainObject = {};
this.originalPath = [];
this.keyString = '';
},
updateSelection(selection) {
if (!selection.length || !selection[0].length) {
this.clearData();
return; this.pathBreadCrumb = pathBreadCrumb;
}
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);
}
} }
} }
}; };

View File

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

View File

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

View File

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

View File

@ -21,51 +21,53 @@
*****************************************************************************/ *****************************************************************************/
<template> <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> </template>
<script> <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 StylesView from '@/plugins/condition/components/inspector/StylesView.vue';
import Vue from 'vue'; import SavedStylesInspectorView from './SavedStylesInspectorView.vue';
export default { export default {
inject: ['openmct', 'stylesManager'], components: {
multipane,
pane,
StylesView,
SavedStylesInspectorView
},
inject: ['openmct'],
data() { data() {
return { return {
selection: [] isEditing: this.openmct.editor.isEditing()
}; };
}, },
mounted() { mounted() {
this.openmct.selection.on('change', this.updateSelection); this.openmct.editor.on('isEditing', this.setEditMode);
this.updateSelection(this.openmct.selection.get());
}, },
destroyed() { beforeDestroyed() {
this.openmct.selection.off('change', this.updateSelection); this.openmct.editor.off('isEditing', this.setEditMode);
}, },
methods: { methods: {
updateSelection(selection) { setEditMode(isEditing) {
if (selection.length > 0 && selection[0].length > 0) { this.isEditing = isEditing;
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/>'
});
}
} }
} }
}; };

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 () { priority: function () {
return 1; return openmct.priority.HIGH + 1;
} }
}; };
} }

View File

@ -23,7 +23,7 @@
import {createOpenMct, resetApplicationState} from "utils/testing"; import {createOpenMct, resetApplicationState} from "utils/testing";
import PlanPlugin from "../plan/plugin"; import PlanPlugin from "../plan/plugin";
import Vue from 'vue'; import Vue from 'vue';
import Properties from "@/ui/inspector/details/Properties.vue"; import Properties from "../inspectorViews/properties/Properties.vue";
describe('the plugin', function () { describe('the plugin', function () {
let planDefinition; let planDefinition;
@ -264,7 +264,7 @@ describe('the plugin', function () {
it('provides an inspector view with the version information if available', () => { it('provides an inspector view with the version information if available', () => {
componentObject = component.$root.$children[0]; componentObject = component.$root.$children[0];
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row'); const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
expect(propertiesEls.length).toEqual(6); expect(propertiesEls.length).toEqual(7);
const found = Array.from(propertiesEls).some((propertyEl) => { const found = Array.from(propertiesEls).some((propertyEl) => {
return (propertyEl.children[0].innerHTML.trim() === 'Version' return (propertyEl.children[0].innerHTML.trim() === 'Version'
&& propertyEl.children[1].innerHTML.trim() === 'v1'); && propertyEl.children[1].innerHTML.trim() === 'v1');

View File

@ -29,7 +29,10 @@
class="c-tree" class="c-tree"
aria-label="Plot Series Properties" 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 <plot-options-item
v-for="series in plotSeries" v-for="series in plotSeries"
:key="series.key" :key="series.key"
@ -101,7 +104,10 @@
<ul <ul
class="l-inspector-part js-legend-properties" 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"> <li class="grid-row">
<div <div
class="grid-cell label" class="grid-cell label"

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import Vue from 'vue';
export default function StackedPlotsInspectorViewProvider(openmct) { export default function StackedPlotsInspectorViewProvider(openmct) {
return { return {
key: 'stacked-plots-inspector', key: 'stacked-plots-inspector',
name: 'Stacked Plots Inspector View', name: 'Config',
canView: function (selection) { canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) { if (selection.length === 0 || selection[0].length === 0) {
return false; return false;
@ -51,7 +51,7 @@ export default function StackedPlotsInspectorViewProvider(openmct) {
}; };
}, },
priority: function () { 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); const applicableInspectorViews = openmct.inspectorViews.get(selection);
expect(plotInspectorView.length).toEqual(1); const plotInspectorView = applicableInspectorViews.find(view => view.name = 'Plots Configuration');
expect(plotInspectorView).toBeDefined();
}); });
it("provides a stacked plot view for objects with telemetry", () => { it("provides a stacked plot view for objects with telemetry", () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -289,7 +289,14 @@ $colorInspectorPropVal: pullForward($colorInspectorFg, 15%);
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%); $colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%);
$colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%); $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); $colorOvrBlocker: rgba(black, 0.7);
$overlayCr: $interiorMarginLg; $overlayCr: $interiorMarginLg;

View File

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

View File

@ -35,13 +35,13 @@
@import "../ui/components/progress-bar.scss"; @import "../ui/components/progress-bar.scss";
@import "../ui/components/search.scss"; @import "../ui/components/search.scss";
@import "../ui/components/swim-lane/swimlane.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/toggle-switch.scss";
@import "../ui/components/timesystem-axis.scss"; @import "../ui/components/timesystem-axis.scss";
@import "../ui/components/List/list-view.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/inspector.scss";
@import "../ui/inspector/location.scss"; @import "../plugins/inspectorViews/properties/location.scss";
@import "../ui/layout/app-logo.scss"; @import "../ui/layout/app-logo.scss";
@import "../ui/layout/create-button.scss"; @import "../ui/layout/create-button.scss";
@import "../ui/layout/layout.scss"; @import "../ui/layout/layout.scss";

View File

@ -23,111 +23,30 @@
<template> <template>
<div class="c-inspector"> <div class="c-inspector">
<object-name /> <object-name />
<div <InspectorTabs
v-if="showStyles" :selection="selection"
class="c-inspector__tabs c-tabs" :is-editing="isEditing"
> @select-tab="selectTab"
<div />
v-for="tabbedView in tabbedViews" <InspectorViews
:key="tabbedView.key" :selection="selection"
class="c-inspector__tab c-tab" :selected-tab="selectedTab"
: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>
</div> </div>
</template> </template>
<script> <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 ObjectName from './ObjectName.vue';
import InspectorTabs from './InspectorTabs.vue';
import InspectorViews from './InspectorViews.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 { export default {
components: { components: {
StylesInspectorView,
SavedStylesInspectorView,
AnnotationsInspectorView,
multipane,
pane,
ElementsPool,
PlotElementsPool,
Properties,
ObjectName, ObjectName,
InspectorTabs,
InspectorViews InspectorViews
}, },
provide: { inject: ['openmct'],
stylesManager: stylesManager
},
inject: ["openmct"],
props: { props: {
isEditing: { isEditing: {
type: Boolean, type: Boolean,
@ -136,116 +55,22 @@ export default {
}, },
data() { data() {
return { return {
hasComposition: false, selection: this.openmct.selection.get(),
multiSelect: false, selectedTab: undefined
showStyles: false,
isOverlayPlot: false,
tabbedViews: [
{
key: "__properties",
name: "Properties"
},
{
key: "__styles",
name: "Styles"
},
{
key: "__annotations",
name: "Annotations"
}
],
currentTabbedView: {},
activity: undefined
}; };
}, },
mounted() { mounted() {
this.excludeObjectTypes = [ this.openmct.selection.on('change', this.setSelection);
"folder",
"webPage",
"conditionSet",
"summary-widget",
"hyperlink"
];
this.openmct.selection.on("change", this.updateInspectorViews);
}, },
destroyed() { destroyed() {
this.openmct.selection.off("change", this.updateInspectorViews); this.openmct.selection.off('change', this.setSelection);
}, },
methods: { methods: {
updateInspectorViews(selection) { setSelection(selection) {
this.refreshComposition(selection); this.selection = 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);
}, },
refreshComposition(selection) { selectTab(tab) {
if (selection.length > 0 && selection[0].length > 0) { this.selectedTab = tab;
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;
} }
} }
}; };

View File

@ -36,8 +36,8 @@ import {
} from './InspectorStylesSpecMocks'; } from './InspectorStylesSpecMocks';
import Vue from 'vue'; import Vue from 'vue';
import StylesView from '@/plugins/condition/components/inspector/StylesView.vue'; import StylesView from '@/plugins/condition/components/inspector/StylesView.vue';
import SavedStylesView from '@/ui/inspector/styles/SavedStylesView.vue'; import SavedStylesView from '../../plugins/inspectorViews/styles/SavedStylesView.vue';
import stylesManager from '@/ui/inspector/styles/StylesManager'; import stylesManager from '../../plugins/inspectorViews/styles/StylesManager';
describe("the inspector", () => { describe("the inspector", () => {
let openmct; 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> <template>
<div aria-label="Inspector Views"></div> <div
class="c-inspector__content"
role="tabpanel"
aria-label="Inspector Views"
></div>
</template> </template>
<script> <script>
export default { export default {
inject: ['openmct'], inject: ['openmct'],
data() { props: {
return { selectedTab: {
selection: [] type: Object,
}; default: undefined
},
selection: {
type: Array,
default: () => {
return [];
}
}
}, },
mounted() { watch: {
this.openmct.selection.on('change', this.updateSelection); selection() {
this.updateSelection(this.openmct.selection.get()); this.updateSelectionViews();
}, },
destroyed() { selectedTab() {
this.openmct.selection.off('change', this.updateSelection); this.clearAndShowViewsForTab();
}
}, },
methods: { methods: {
updateSelection(selection) { updateSelectionViews(selection) {
this.selection = selection; this.clearViews();
this.selectedViews = this.openmct.inspectorViews.get(this.selection);
if (this.selectedViews) { this.showViewsForTab();
this.selectedViews.forEach(selectedView => { },
selectedView.destroy(); clearViews() {
if (this.visibleViews) {
this.visibleViews.forEach(visibleView => {
visibleView.destroy();
}); });
this.visibleViews = [];
this.$el.innerHTML = ''; this.$el.innerHTML = '';
} }
},
showViewsForTab() {
this.visibleViews = this.selectedViews
.filter(view => view.key === this.selectedTab.key);
this.selectedViews = this.openmct.inspectorViews.get(selection); this.visibleViews.forEach(visibleView => {
this.selectedViews.forEach(selectedView => {
let viewContainer = document.createElement('div'); let viewContainer = document.createElement('div');
this.$el.append(viewContainer); 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; flex: 0 0 auto;
font-size: 0.8em; font-size: 0.8em;
text-transform: uppercase; 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 { &__content {
@ -93,6 +128,11 @@
@include propertiesHeader(); @include propertiesHeader();
font-size: 0.65rem; font-size: 0.65rem;
grid-column: 1 / 3; grid-column: 1 / 3;
margin: $interiorMargin 0;
&.--first {
margin-top: 0;
}
} }
.c-tree .grid-properties { .c-tree .grid-properties {

View File

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