From 10eb749d329ff4e6b19e3ec7a78601e86d2649ff Mon Sep 17 00:00:00 2001 From: Jamie V Date: Thu, 14 Mar 2024 09:05:23 -0700 Subject: [PATCH] Prevent Metadata Time System Error for Missing Objects (#7565) https://github.com/nasa/openmct/pull/7565 Modified Stacked Plots to not show Missing Objects. Added a check in Telemetry Collections for missing objects before displaying telemetry metadata time system error. --- .../plugins/plot/missingPlotObj.e2e.spec.js | 142 ++++-------------- src/api/objects/ObjectAPI.js | 2 +- src/api/telemetry/TelemetryCollection.js | 8 +- .../plot/configuration/SeriesCollection.js | 5 + src/plugins/plot/stackedPlot/StackedPlot.vue | 5 + .../plot/stackedPlot/StackedPlotItem.vue | 8 +- src/ui/components/ObjectFrame.vue | 5 +- src/ui/layout/BrowseBar.vue | 58 ++++--- 8 files changed, 87 insertions(+), 146 deletions(-) diff --git a/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js b/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js index bee8c83a2d..8e51d6c9eb 100644 --- a/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js @@ -24,138 +24,60 @@ Tests to verify log plot functionality when objects are missing */ +import { createDomainObjectWithDefaults } from '../../../../appActions.js'; import { expect, test } from '../../../../pluginFixtures.js'; test.describe('Handle missing object for plots', () => { - test('Displays empty div for missing stacked plot item @unstable', async ({ - page, - browserName, - openmctConfig - }) => { + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); + test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => { // eslint-disable-next-line playwright/no-skipped-test test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed'); - const { myItemsFolderName } = openmctConfig; - const errorLogs = []; + let warningReceived = false; page.on('console', (message) => { if (message.type() === 'warning' && message.text().includes('Missing domain object')) { - errorLogs.push(message.text()); + warningReceived = true; } }); - //Make stacked plot - await makeStackedPlot(page, myItemsFolderName); + const stackedPlot = await createDomainObjectWithDefaults(page, { + type: 'Stacked Plot' + }); + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: stackedPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: stackedPlot.uuid + }); //Gets local storage and deletes the last sine wave generator in the stacked plot - const localStorage = await page.evaluate(() => window.localStorage); - const parsedData = JSON.parse(localStorage.mct); - const keys = Object.keys(parsedData); - const lastKey = keys[keys.length - 1]; + const mct = await page.evaluate(() => window.localStorage.getItem('mct')); + const parsedData = JSON.parse(mct); + const key = Object.entries(parsedData).find(([, value]) => value.type === 'generator')?.[0]; - delete parsedData[lastKey]; + delete parsedData[key]; //Sets local storage with missing object - await page.evaluate(`window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`); + const jsonData = JSON.stringify(parsedData); + await page.evaluate((data) => { + window.localStorage.setItem('mct', data); + }, jsonData); //Reloads page and clicks on stacked plot - await Promise.all([page.reload(), page.waitForLoadState('networkidle')]); + await page.reload({ waitUntil: 'domcontentloaded' }); + await page.goto(stackedPlot.url); //Verify Main section is there on load - await expect - .soft(page.locator('.l-browse-bar__object-name')) - .toContainText('Unnamed Stacked Plot'); - - await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); - await Promise.all([ - page.waitForNavigation(), - page.locator('text=Unnamed Stacked Plot').first().click() - ]); + await expect(page.locator('.l-browse-bar__object-name')).toContainText(stackedPlot.name); //Check that there is only one stacked item plot with a plot, the missing one will be empty - await expect(page.locator('.c-plot--stacked-container:has(.gl-plot)')).toHaveCount(1); - //Verify that console.warn is thrown - expect(errorLogs).toHaveLength(1); + await expect(page.getByLabel('Stacked Plot Item')).toHaveCount(1); + //Verify that console.warn was thrown + expect(warningReceived).toBe(true); }); }); - -/** - * This is used the create a stacked plot object - * @private - */ -async function makeStackedPlot(page, myItemsFolderName) { - // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z - await page.goto('./', { waitUntil: 'domcontentloaded' }); - - // create stacked plot - await page.locator('button.c-create-button').click(); - await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click(); - - await Promise.all([ - page.waitForNavigation({ waitUntil: 'networkidle' }), - page.locator('button:has-text("OK")').click(), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); - - // save the stacked plot - await saveStackedPlot(page); - - // create a sinewave generator - await createSineWaveGenerator(page); - - // click on stacked plot - await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); - await Promise.all([ - page.waitForNavigation(), - page.locator('text=Unnamed Stacked Plot').first().click() - ]); - - // create a second sinewave generator - await createSineWaveGenerator(page); - - // click on stacked plot - await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); - await Promise.all([ - page.waitForNavigation(), - page.locator('text=Unnamed Stacked Plot').first().click() - ]); -} - -/** - * This is used to save a stacked plot object - * @private - */ -async function saveStackedPlot(page) { - // save stacked plot - await page - .locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button') - .nth(1) - .click(); - - await Promise.all([ - page.locator('text=Save and Finish Editing').click(), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); - //Wait until Save Banner is gone - await page.locator('.c-message-banner__close-button').click(); - await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); -} - -/** - * This is used to create a sine wave generator object - * @private - */ -async function createSineWaveGenerator(page) { - //Create sine wave generator - await page.locator('button.c-create-button').click(); - await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); - - await Promise.all([ - page.waitForNavigation({ waitUntil: 'networkidle' }), - page.locator('button:has-text("OK")').click(), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); -} diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index 3615f3c94c..55b46d2850 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -249,7 +249,7 @@ export default class ObjectAPI { .get(identifier, abortSignal) .then((domainObject) => { delete this.cache[keystring]; - if (!domainObject && abortSignal.aborted) { + if (!domainObject && abortSignal?.aborted) { // we've aborted the request return; } diff --git a/src/api/telemetry/TelemetryCollection.js b/src/api/telemetry/TelemetryCollection.js index 4791a04ae0..eb2462aa36 100644 --- a/src/api/telemetry/TelemetryCollection.js +++ b/src/api/telemetry/TelemetryCollection.js @@ -442,8 +442,12 @@ export default class TelemetryCollection extends EventEmitter { } else { this.timeKey = undefined; - this._warn(TIMESYSTEM_KEY_WARNING); - this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION); + // missing objects will never have a domain, if one happens to get through + // to this point this warning/notification does not apply + if (!this.openmct.objects.isMissing(this.domainObject)) { + this._warn(TIMESYSTEM_KEY_WARNING); + this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION); + } } let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); diff --git a/src/plugins/plot/configuration/SeriesCollection.js b/src/plugins/plot/configuration/SeriesCollection.js index 001d9358df..cd3401b416 100644 --- a/src/plugins/plot/configuration/SeriesCollection.js +++ b/src/plugins/plot/configuration/SeriesCollection.js @@ -46,6 +46,11 @@ export default class SeriesCollection extends Collection { this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this); const domainObject = this.plot.get('domainObject'); + + if (this.openmct.objects.isMissing(domainObject)) { + return; + } + if (domainObject.telemetry) { this.addTelemetryObject(domainObject); } else { diff --git a/src/plugins/plot/stackedPlot/StackedPlot.vue b/src/plugins/plot/stackedPlot/StackedPlot.vue index 89e76a6058..897dca433d 100644 --- a/src/plugins/plot/stackedPlot/StackedPlot.vue +++ b/src/plugins/plot/stackedPlot/StackedPlot.vue @@ -219,6 +219,11 @@ export default { }, addChild(child) { + if (this.openmct.objects.isMissing(child)) { + console.warn('Missing domain object for stacked plot: ', child); + return; + } + const id = this.openmct.objects.makeKeyString(child.identifier); this.tickWidthMap[id] = { diff --git a/src/plugins/plot/stackedPlot/StackedPlotItem.vue b/src/plugins/plot/stackedPlot/StackedPlotItem.vue index 15388e48a6..a53b13ee71 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotItem.vue +++ b/src/plugins/plot/stackedPlot/StackedPlotItem.vue @@ -170,6 +170,10 @@ export default { //If this object is not persistable, then package it with it's parent const plotObject = this.getPlotObject(); + if (plotObject === null) { + return; + } + if (this.openmct.telemetry.isTelemetryObject(plotObject)) { this.subscribeToStaleness(plotObject); } else { @@ -215,10 +219,6 @@ export default { }, getPlotObject() { this.checkPlotConfiguration(); - // If object is missing, warn - if (this.openmct.objects.isMissing(this.childObject)) { - console.warn('Missing domain object for stacked plot', this.childObject); - } return this.childObject; }, checkPlotConfiguration() { diff --git a/src/ui/components/ObjectFrame.vue b/src/ui/components/ObjectFrame.vue index 1b3f7ec98c..177f9d5d1f 100644 --- a/src/ui/components/ObjectFrame.vue +++ b/src/ui/components/ObjectFrame.vue @@ -190,7 +190,7 @@ export default { this.soViewResizeObserver.observe(this.$refs.soView); } - const viewKey = this.getViewKey(); + const viewKey = this.$refs.objectView?.viewKey; this.supportsIndependentTime = this.domainObject && SupportedViewTypes.includes(viewKey); }, beforeUnmount() { @@ -257,9 +257,6 @@ export default { this.widthClass = wClass.trimStart(); }, - getViewKey() { - return this.$refs.objectView?.viewKey; - }, async showToolTip() { const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS; this.buildToolTip(await this.getObjectPath(), BELOW, 'objectName'); diff --git a/src/ui/layout/BrowseBar.vue b/src/ui/layout/BrowseBar.vue index b71e601a43..315122b93d 100644 --- a/src/ui/layout/BrowseBar.vue +++ b/src/ui/layout/BrowseBar.vue @@ -29,7 +29,7 @@ @click="goToParent" >
-
+
- {{ domainObject.name }} + {{ domainObjectName }}
@@ -150,8 +150,6 @@ import tooltipHelpers from '../../api/tooltips/tooltipMixins.js'; import { SupportedViewTypes } from '../../utils/constants.js'; import ViewSwitcher from './ViewSwitcher.vue'; -const PLACEHOLDER_OBJECT = {}; - export default { components: { IndependentTimeConductor, @@ -168,12 +166,12 @@ export default { } } }, - data: function () { + data() { return { notebookTypes: [], showViewMenu: false, showSaveMenu: false, - domainObject: PLACEHOLDER_OBJECT, + domainObject: undefined, viewKey: undefined, isEditing: this.openmct.editor.isEditing(), notebookEnabled: this.openmct.types.get('notebook'), @@ -185,11 +183,22 @@ export default { statusClass() { return this.status ? `is-status--${this.status}` : ''; }, + supportsIndependentTime() { + return ( + this.domainObject?.identifier && + !this.openmct.objects.isMissing(this.domainObject) && + SupportedViewTypes.includes(this.viewKey) + ); + }, currentView() { return this.views.filter((v) => v.key === this.viewKey)[0] || {}; }, views() { - if (this.domainObject && this.openmct.router.started !== true) { + if (this.domainObject && this.openmct.router.started === false) { + return []; + } + + if (!this.domainObject) { return []; } @@ -203,25 +212,29 @@ export default { }); }, hasParent() { - return toRaw(this.domainObject) !== PLACEHOLDER_OBJECT && this.parentUrl !== '/browse'; + return toRaw(this.domainObject) && this.parentUrl !== '/browse'; }, parentUrl() { - const objectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); + const objectKeyString = this.openmct.objects.makeKeyString(this.domainObject?.identifier); const hash = this.openmct.router.getCurrentLocation().path; return hash.slice(0, hash.lastIndexOf('/' + objectKeyString)); }, - type() { - const objectType = this.openmct.types.get(this.domainObject.type); - if (!objectType) { - return {}; + cssClass() { + if (!this.domainObject) { + return ''; } - return objectType.definition; + const objectType = this.openmct.types.get(this.domainObject.type); + if (!objectType) { + return ''; + } + + return objectType?.definition?.cssClass ?? ''; }, isPersistable() { - let persistable = - this.domainObject.identifier && + const persistable = + this.domainObject?.identifier && this.openmct.objects.isPersistable(this.domainObject.identifier); return persistable; @@ -246,10 +259,8 @@ export default { return 'Unlocked for editing - click to lock.'; } }, - supportsIndependentTime() { - const viewKey = this.getViewKey(); - - return this.domainObject && SupportedViewTypes.includes(viewKey); + domainObjectName() { + return this.domainObject?.name ?? ''; } }, watch: { @@ -273,7 +284,7 @@ export default { this.updateActionItems(this.actionCollection.getActionsObject()); } }, - mounted: function () { + mounted() { document.addEventListener('click', this.closeViewAndSaveMenu); this.promptUserbeforeNavigatingAway = this.promptUserbeforeNavigatingAway.bind(this); window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway); @@ -282,7 +293,7 @@ export default { this.isEditing = isEditing; }); }, - beforeUnmount: function () { + beforeUnmount() { if (this.mutationObserver) { this.mutationObserver(); } @@ -323,9 +334,6 @@ export default { edit() { this.openmct.editor.edit(); }, - getViewKey() { - return this.viewKey; - }, promptUserandCancelEditing() { let dialog = this.openmct.overlays.dialog({ iconClass: 'alert',