diff --git a/e2e/appActions.js b/e2e/appActions.js index c50566a149..bf63790343 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -401,14 +401,7 @@ async function setEndOffset(page, offset) { 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(); - } + await inspectorTab.click(); } /** diff --git a/e2e/tests/functional/tooltips.e2e.spec.js b/e2e/tests/functional/tooltips.e2e.spec.js new file mode 100644 index 0000000000..ae50c9b79c --- /dev/null +++ b/e2e/tests/functional/tooltips.e2e.spec.js @@ -0,0 +1,398 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to tests which can quickly verify that any openmct installation is +operable and that any type of testing can proceed. + +Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them +more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly +as they cover a very "thin surface" of functionality. + +When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel +comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects. +Make no assumptions about the order that elements appear in the DOM. +*/ + +const { test, expect } = require('../../pluginFixtures'); +const { createDomainObjectWithDefaults, expandEntireTree } = require('../../appActions'); + +test.describe('Verify tooltips', () => { + let folder1; + let folder2; + let folder3; + let sineWaveObject1; + let sineWaveObject2; + let sineWaveObject3; + + const swg1Path = 'My Items / Folder Foo / SWG 1'; + const swg2Path = 'My Items / Folder Foo / Folder Bar / SWG 2'; + const swg3Path = 'My Items / Folder Foo / Folder Bar / Folder Baz / SWG 3'; + + test.beforeEach(async ({ page, openmctConfig }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + folder1 = await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: 'Folder Foo' + }); + folder2 = await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: 'Folder Bar', + parent: folder1.uuid + }); + folder3 = await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: 'Folder Baz', + parent: folder2.uuid + }); + // Create Sine Wave Generator + sineWaveObject1 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'SWG 1', + parent: folder1.uuid + }); + sineWaveObject1.path = swg1Path; + sineWaveObject2 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'SWG 2', + parent: folder2.uuid + }); + sineWaveObject2.path = swg2Path; + sineWaveObject3 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'SWG 3', + parent: folder3.uuid + }); + sineWaveObject3.path = swg3Path; + + // Expand all folders + await expandEntireTree(page); + }); + + // LAD Tables - DONE + // Expanded collapsed plot legend - DONE + // Object Labels - DONE + // Display Layout headers - DONE + // Flexible Layout headers - DONE + // Tab View layout headers - DONE + // Search - DONE + // Gauge - + // Notebook Embed - DONE + // Telemetry Table - + // Timeline Objects + // Tree - DONE + // Recent Objects + + test('display correct paths for LAD tables', async ({ page, openmctConfig }) => { + // Create LAD table + await createDomainObjectWithDefaults(page, { + type: 'LAD Table', + name: 'Test LAD Table' + }); + // Edit LAD table + await page.locator('[title="Edit"]').click(); + + // Add the Sine Wave Generator to the LAD table and save changes + await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-lad-table-wrapper'); + await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-lad-table-wrapper'); + await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-lad-table-wrapper'); + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + await page.keyboard.down('Control'); + + async function getToolTip(object) { + await page.locator('.c-create-button').hover(); + await page.getByRole('cell', { name: object.name }).hover(); + let tooltipText = await page.locator('.c-tooltip').textContent(); + return tooltipText.replace('\n', '').trim(); + } + + expect(await getToolTip(sineWaveObject1)).toBe(sineWaveObject1.path); + expect(await getToolTip(sineWaveObject2)).toBe(sineWaveObject2.path); + expect(await getToolTip(sineWaveObject3)).toBe(sineWaveObject3.path); + }); + + test('display correct paths for expanded and collapsed plot legend items', async ({ page }) => { + // Create Overlay Plot + await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot', + name: 'Test Overlay Plots' + }); + // Edit Overlay Plot + await page.locator('[title="Edit"]').click(); + + // Add the Sine Wave Generator to the LAD table and save changes + await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot'); + await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.gl-plot'); + await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.gl-plot'); + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + await page.keyboard.down('Control'); + + async function getCollapsedLegendToolTip(object) { + await page.locator('.c-create-button').hover(); + await page + .locator('.plot-series-name', { has: page.locator(`text="${object.name} Hz"`) }) + .hover(); + let tooltipText = await page.locator('.c-tooltip').textContent(); + return tooltipText.replace('\n', '').trim(); + } + + async function getExpandedLegendToolTip(object) { + await page.locator('.c-create-button').hover(); + await page + .locator('.plot-series-name', { has: page.locator(`text="${object.name}"`) }) + .hover(); + let tooltipText = await page.locator('.c-tooltip').textContent(); + return tooltipText.replace('\n', '').trim(); + } + + expect(await getCollapsedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path); + expect(await getCollapsedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path); + expect(await getCollapsedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path); + + await page.keyboard.up('Control'); + await page.locator('.gl-plot-legend__view-control.c-disclosure-triangle').click(); + await page.keyboard.down('Control'); + + expect(await getExpandedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path); + expect(await getExpandedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path); + expect(await getExpandedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path); + }); + + test('display correct paths when hovering over object labels', async ({ page }) => { + async function getObjectLabelTooltip(object) { + await page + .locator('.c-tree__item__name.c-object-label__name', { + has: page.locator(`text="${object.name}"`) + }) + .click(); + await page.keyboard.down('Control'); + await page + .locator('.l-browse-bar__object-name.c-object-label__name', { + has: page.locator(`text="${object.name}"`) + }) + .hover(); + const tooltipText = await page.locator('.c-tooltip').textContent(); + await page.keyboard.up('Control'); + return tooltipText.replace('\n', '').trim(); + } + + expect(await getObjectLabelTooltip(sineWaveObject1)).toBe(sineWaveObject1.path); + expect(await getObjectLabelTooltip(sineWaveObject3)).toBe(sineWaveObject3.path); + }); + + test('display correct paths when hovering over display layout pane headers', async ({ page }) => { + // Create Overlay Plot + await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot', + name: 'Test Overlay Plot' + }); + // Edit Overlay Plot + await page.locator('[title="Edit"]').click(); + await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot'); + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + // Create Stacked Plot + await createDomainObjectWithDefaults(page, { + type: 'Stacked Plot', + name: 'Test Stacked Plot' + }); + // Edit Stacked Plot + await page.locator('[title="Edit"]').click(); + await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-plot--stacked.holder'); + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + // Create Display Layout + await createDomainObjectWithDefaults(page, { + type: 'Display Layout', + name: 'Test Display Layout' + }); + // Edit Display Layout + await page.locator('[title="Edit"]').click(); + + await page.dragAndDrop("text='Test Overlay Plot'", '.l-layout__grid-holder', { + targetPosition: { x: 0, y: 0 } + }); + await page.dragAndDrop("text='Test Stacked Plot'", '.l-layout__grid-holder', { + targetPosition: { x: 0, y: 250 } + }); + await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.l-layout__grid-holder', { + targetPosition: { x: 500, y: 200 } + }); + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + await page.keyboard.down('Control'); + + await page.getByText('Test Overlay Plot').nth(2).hover(); + let tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(tooltipText).toBe('My Items / Test Overlay Plot'); + + // await page.keyboard.up('Control'); + // await page.locator('.c-plot-legend__view-control >> nth=0').click(); + // await page.keyboard.down('Control'); + // await page.locator('.plot-wrapper-expanded-legend .plot-series-name').first().hover(); + // tooltipText = await page.locator('.c-tooltip').textContent(); + // tooltipText = tooltipText.replace('\n', '').trim(); + // expect(tooltipText).toBe(sineWaveObject1.path); + + await page.getByText('Test Stacked Plot').nth(2).hover(); + tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(tooltipText).toBe('My Items / Test Stacked Plot'); + + await page.getByText('SWG 3').nth(2).hover(); + tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(sineWaveObject3.path).toBe(tooltipText); + }); + + test('display correct paths when hovering over flexible object labels', async ({ page }) => { + await createDomainObjectWithDefaults(page, { + type: 'Flexible Layout', + name: 'Test Flexible Layout' + }); + + await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl__container >> nth=0'); + await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl__container >> nth=1'); + + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + await page.keyboard.down('Control'); + await page.getByText('SWG 1').nth(2).hover(); + let tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(tooltipText).toBe(sineWaveObject1.path); + + await page.getByText('SWG 3').nth(2).hover(); + tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(tooltipText).toBe(sineWaveObject3.path); + }); + + test('display correct paths when hovering over tab view labels', async ({ page }) => { + await createDomainObjectWithDefaults(page, { + type: 'Tabs View', + name: 'Test Tabs View' + }); + + await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-tabs-view__tabs-holder'); + await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-tabs-view__tabs-holder'); + + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + await page.keyboard.down('Control'); + await page.getByText('SWG 1').nth(2).hover(); + let tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(tooltipText).toBe(sineWaveObject1.path); + + await page.getByText('SWG 3').nth(2).hover(); + tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(tooltipText).toBe(sineWaveObject3.path); + }); + + test('display correct paths when hovering tree items', async ({ page }) => { + await page.keyboard.down('Control'); + await page.getByText('SWG 1').nth(0).hover(); + let tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(tooltipText).toBe(sineWaveObject1.path); + + await page.getByText('SWG 3').nth(0).hover(); + tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(tooltipText).toBe(sineWaveObject3.path); + }); + + test('display correct paths when hovering search items', async ({ page }) => { + await page.getByRole('searchbox', { name: 'Search Input' }).click(); + await page.fill('.c-search__input', 'SWG 3'); + + await page.keyboard.down('Control'); + await page.locator('.c-gsearch-result__title').hover(); + let tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(tooltipText).toBe(sineWaveObject3.path); + }); + + test('display path for source telemetry when hovering over gauge', ({ page }) => { + expect(true).toBe(true); + // await createDomainObjectWithDefaults(page, { + // type: 'Gauge', + // name: 'Test Gauge' + // }); + // await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-gauge__wrapper'); + // await page.keyboard.down('Control'); + // await page.locator('.c-gauge__current-value-text-wrapper').hover(); + // let tooltipText = await page.locator('.c-tooltip').textContent(); + // tooltipText = tooltipText.replace('\n', '').trim(); + // expect(tooltipText).toBe(sineWaveObject3.path); + }); + + test('display tooltip path for notebook embeds', async ({ page }) => { + await createDomainObjectWithDefaults(page, { + type: 'Notebook', + name: 'Test Notebook' + }); + + await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-notebook__drag-area'); + await page.keyboard.down('Control'); + await page.locator('.c-ne__embed').hover(); + let tooltipText = await page.locator('.c-tooltip').textContent(); + tooltipText = tooltipText.replace('\n', '').trim(); + expect(tooltipText).toBe(sineWaveObject3.path); + }); + + // test('display tooltip path for telemetry table names', async ({ page }) => { + // await setEndOffset(page, { secs: '10' }); + // await createDomainObjectWithDefaults(page, { + // type: 'Telemetry Table', + // name: 'Test Telemetry Table' + // }); + + // await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table'); + // await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table'); + + // await page.locator('button[title="Save"]').click(); + // await page.locator('text=Save and Finish Editing').click(); + + // // .c-telemetry-table__body + + // await page.keyboard.down('Control'); + + // await page.locator('.noselect > [title="SWG 3"]').first().hover(); + // let tooltipText = await page.locator('.c-tooltip').textContent(); + // tooltipText = tooltipText.replace('\n', '').trim(); + // expect(tooltipText).toBe(sineWaveObject3.path); + // }); +}); diff --git a/openmct.js b/openmct.js index 96c6cdec2d..9c7a8e0503 100644 --- a/openmct.js +++ b/openmct.js @@ -56,6 +56,7 @@ if (document.currentScript) { * @property {import('./src/api/notifications/NotificationAPI').default} notifications * @property {import('./src/api/Editor').default} editor * @property {import('./src/api/overlays/OverlayAPI')} overlays + * @property {import('./src/api/tooltips/ToolTipAPI')} tooltips * @property {import('./src/api/menu/MenuAPI').default} menus * @property {import('./src/api/actions/ActionsAPI').default} actions * @property {import('./src/api/status/StatusAPI').default} status diff --git a/src/MCT.js b/src/MCT.js index d7d5703e8d..a4c43c6701 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -24,6 +24,7 @@ define([ 'EventEmitter', './api/api', './api/overlays/OverlayAPI', + './api/tooltips/ToolTipAPI', './selection/Selection', './plugins/plugins', './ui/registries/ViewRegistry', @@ -48,6 +49,7 @@ define([ EventEmitter, api, OverlayAPI, + ToolTipAPI, Selection, plugins, ViewRegistry, @@ -220,6 +222,8 @@ define([ ['overlays', () => new OverlayAPI.default()], + ['tooltips', () => new ToolTipAPI.default()], + ['menus', () => new api.MenuAPI(this)], ['actions', () => new api.ActionsAPI(this)], diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index cc3171b573..6b5c8fe22b 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -540,6 +540,40 @@ export default class ObjectAPI { .join('/'); } + /** + * Return path of telemetry objects in the object composition + * @param {object} identifier the identifier for the domain object to query for + * @param {object} [telemetryIdentifier] the specific identifier for the telemetry + * to look for in the composition, uses first object in composition otherwise + * @returns {Array} path of telemetry object in object composition + */ + async getTelemetryPath(identifier, telemetryIdentifier) { + const objectDetails = await this.get(identifier); + const telemetryPath = []; + if (objectDetails.composition && !['folder'].includes(objectDetails.type)) { + let sourceTelemetry = objectDetails.composition[0]; + if (telemetryIdentifier) { + sourceTelemetry = objectDetails.composition.find( + (telemetrySource) => + this.makeKeyString(telemetrySource) === this.makeKeyString(telemetryIdentifier) + ); + } + const compositionElement = await this.get(sourceTelemetry); + if (!['yamcs.telemetry', 'generator'].includes(compositionElement.type)) { + return telemetryPath; + } + const telemetryKey = compositionElement.identifier.key; + const telemetryPathObjects = await this.getOriginalPath(telemetryKey); + telemetryPathObjects.forEach((pathObject) => { + if (pathObject.type === 'root') { + return; + } + telemetryPath.unshift(pathObject.name); + }); + } + return telemetryPath; + } + /** * Modify a domain object. Internal to ObjectAPI, won't call save after. * @private diff --git a/src/api/tooltips/ToolTip.js b/src/api/tooltips/ToolTip.js new file mode 100644 index 0000000000..8e41829856 --- /dev/null +++ b/src/api/tooltips/ToolTip.js @@ -0,0 +1,73 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import TooltipComponent from './components/TooltipComponent.vue'; +import EventEmitter from 'EventEmitter'; +import Vue from 'vue'; + +class Tooltip extends EventEmitter { + constructor( + { toolTipText, toolTipLocation, parentElement } = { + tooltipText: '', + toolTipLocation: 'below', + parentElement: null + } + ) { + super(); + + this.container = document.createElement('div'); + + this.component = new Vue({ + components: { + TooltipComponent: TooltipComponent + }, + provide: { + toolTipText, + toolTipLocation, + parentElement + }, + template: '' + }); + + this.isActive = null; + } + + destroy() { + if (!this.isActive) { + return; + } + document.body.removeChild(this.container); + this.component.$destroy(); + this.isActive = false; + } + + /** + * @private + **/ + show() { + document.body.appendChild(this.container); + this.container.appendChild(this.component.$mount().$el); + this.isActive = true; + } +} + +export default Tooltip; diff --git a/src/api/tooltips/ToolTipAPI.js b/src/api/tooltips/ToolTipAPI.js new file mode 100644 index 0000000000..425538e23e --- /dev/null +++ b/src/api/tooltips/ToolTipAPI.js @@ -0,0 +1,90 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import Tooltip from './ToolTip'; + +/** + * @readonly + * @enum {String} TooltipLocation + * @property {String} ABOVE The string for locating tooltips above an element + * @property {String} BELOW The string for locating tooltips below an element + * @property {String} RIGHT The pixel-spatial annotation type + * @property {String} LEFT The temporal annotation type + * @property {String} CENTER The plot-spatial annotation type + */ +const TOOLTIP_LOCATIONS = Object.freeze({ + ABOVE: 'above', + BELOW: 'below', + RIGHT: 'right', + LEFT: 'left', + CENTER: 'center' +}); + +/** + * The TooltipAPI is responsible for adding custom tooltips to + * the desired elements on the screen + * + * @memberof api/tooltips + * @constructor + */ + +class TooltipAPI { + constructor() { + this.activeToolTips = []; + this.TOOLTIP_LOCATIONS = TOOLTIP_LOCATIONS; + } + + /** + * @private for platform-internal use + */ + showTooltip(tooltip) { + for (let i = this.activeToolTips.length - 1; i > -1; i--) { + this.activeToolTips[i].destroy(); + this.activeToolTips.splice(i, 1); + } + this.activeToolTips.push(tooltip); + + tooltip.show(); + } + + /** + * A description of option properties that can be passed into the tooltip + * @typedef {Object} TooltipOptions + * @property {string} tooltipText text to show in the tooltip + * @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement + * @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to + */ + + /** + * Tooltips take an options object that consists of the string, tooltipLocation, and parentElement + * @param {TooltipOptions} options + */ + tooltip(options) { + let tooltip = new Tooltip(options); + + this.showTooltip(tooltip); + + return tooltip; + } +} + +export default TooltipAPI; diff --git a/src/api/tooltips/components/TooltipComponent.vue b/src/api/tooltips/components/TooltipComponent.vue new file mode 100644 index 0000000000..c7127e7c6d --- /dev/null +++ b/src/api/tooltips/components/TooltipComponent.vue @@ -0,0 +1,61 @@ + + + + diff --git a/src/api/tooltips/components/tooltip-component.scss b/src/api/tooltips/components/tooltip-component.scss new file mode 100644 index 0000000000..1b93f71afa --- /dev/null +++ b/src/api/tooltips/components/tooltip-component.scss @@ -0,0 +1,8 @@ +.c-tooltip-wrapper { + max-width: 200px; + padding: $interiorMargin; +} + +.c-tooltip { + font-style: italic; +} diff --git a/src/api/tooltips/tooltipMixins.js b/src/api/tooltips/tooltipMixins.js new file mode 100644 index 0000000000..e716a9c896 --- /dev/null +++ b/src/api/tooltips/tooltipMixins.js @@ -0,0 +1,72 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +const tooltipHelpers = { + methods: { + async getTelemetryPathString(telemetryIdentifier) { + let telemetryPathString = ''; + if (!this.domainObject?.identifier) { + return; + } + const telemetryPath = await this.openmct.objects.getTelemetryPath( + this.domainObject.identifier, + telemetryIdentifier + ); + if (telemetryPath.length) { + telemetryPathString = telemetryPath.join(' / '); + } + return telemetryPathString; + }, + async getObjectPath(objectIdentifier) { + if (!objectIdentifier && !this.domainObject) { + return; + } + const domainObjectIdentifier = objectIdentifier || this.domainObject.identifier; + const objectPathList = await this.openmct.objects.getOriginalPath(domainObjectIdentifier); + objectPathList.pop(); + return objectPathList + .map((pathItem) => pathItem.name) + .reverse() + .join(' / '); + }, + buildToolTip(tooltipText, tooltipLocation, elementRef) { + if (!tooltipText || tooltipText.length < 1) { + return; + } + let parentElement = this.$refs[elementRef]; + if (Array.isArray(parentElement)) { + parentElement = parentElement[0]; + } + this.tooltip = this.openmct.tooltips.tooltip({ + toolTipText: tooltipText, + toolTipLocation: tooltipLocation, + parentElement: parentElement + }); + }, + hideToolTip() { + this.tooltip?.destroy(); + this.tooltip = null; + } + } +}; + +export default tooltipHelpers; diff --git a/src/plugins/LADTable/components/LADRow.vue b/src/plugins/LADTable/components/LADRow.vue index 5f292d0f4c..30a6c52838 100644 --- a/src/plugins/LADTable/components/LADRow.vue +++ b/src/plugins/LADTable/components/LADRow.vue @@ -26,7 +26,14 @@ @click="clickedRow" @contextmenu.prevent="showContextMenu" > - {{ domainObject.name }} + + {{ domainObject.name }} + {{ formattedTimestamp }} {{ value }} @@ -42,8 +49,10 @@ const BLANK_VALUE = '---'; import identifierToString from '/src/tools/url'; import PreviewAction from '@/ui/preview/PreviewAction.js'; +import tooltipHelpers from '../../../api/tooltips/tooltipMixins'; export default { + mixins: [tooltipHelpers], inject: ['openmct', 'currentView'], props: { domainObject: { @@ -259,6 +268,10 @@ export default { return metadata .values() .find((metadatum) => metadatum.hints.domain === undefined && metadatum.key !== 'name'); + }, + async showToolTip() { + const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS; + this.buildToolTip(await this.getObjectPath(), BELOW, 'tableCell'); } } }; diff --git a/src/plugins/LADTable/pluginSpec.js b/src/plugins/LADTable/pluginSpec.js index a317d8b9cd..3d8e674cbe 100644 --- a/src/plugins/LADTable/pluginSpec.js +++ b/src/plugins/LADTable/pluginSpec.js @@ -264,7 +264,7 @@ describe('The LAD Table', () => { }); it('should show the name provided for the the telemetry producing object', () => { - const rowName = parent.querySelector(TABLE_BODY_FIRST_ROW_FIRST_DATA).innerText; + const rowName = parent.querySelector(TABLE_BODY_FIRST_ROW_FIRST_DATA).innerText.trim(); const expectedName = mockObj.telemetry.name; expect(rowName).toBe(expectedName); diff --git a/src/plugins/conditionWidget/components/ConditionWidget.vue b/src/plugins/conditionWidget/components/ConditionWidget.vue index 86d157baa4..7a1283c9b6 100644 --- a/src/plugins/conditionWidget/components/ConditionWidget.vue +++ b/src/plugins/conditionWidget/components/ConditionWidget.vue @@ -21,7 +21,12 @@ --> diff --git a/src/ui/layout/BrowseBar.vue b/src/ui/layout/BrowseBar.vue index 267e8dbf43..ba358a126c 100644 --- a/src/ui/layout/BrowseBar.vue +++ b/src/ui/layout/BrowseBar.vue @@ -33,12 +33,15 @@ {{ domainObject.name }} @@ -127,6 +130,7 @@