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.
This commit is contained in:
Jamie V 2024-03-14 09:05:23 -07:00 committed by GitHub
parent faed27c143
commit 10eb749d32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 87 additions and 146 deletions

View File

@ -24,138 +24,60 @@
Tests to verify log plot functionality when objects are missing Tests to verify log plot functionality when objects are missing
*/ */
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Handle missing object for plots', () => { test.describe('Handle missing object for plots', () => {
test('Displays empty div for missing stacked plot item @unstable', async ({ test.beforeEach(async ({ page }) => {
page, await page.goto('./', { waitUntil: 'domcontentloaded' });
browserName, });
openmctConfig test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => {
}) => {
// eslint-disable-next-line playwright/no-skipped-test // eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed'); test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');
const { myItemsFolderName } = openmctConfig; let warningReceived = false;
const errorLogs = [];
page.on('console', (message) => { page.on('console', (message) => {
if (message.type() === 'warning' && message.text().includes('Missing domain object')) { if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
errorLogs.push(message.text()); warningReceived = true;
} }
}); });
//Make stacked plot const stackedPlot = await createDomainObjectWithDefaults(page, {
await makeStackedPlot(page, myItemsFolderName); 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 //Gets local storage and deletes the last sine wave generator in the stacked plot
const localStorage = await page.evaluate(() => window.localStorage); const mct = await page.evaluate(() => window.localStorage.getItem('mct'));
const parsedData = JSON.parse(localStorage.mct); const parsedData = JSON.parse(mct);
const keys = Object.keys(parsedData); const key = Object.entries(parsedData).find(([, value]) => value.type === 'generator')?.[0];
const lastKey = keys[keys.length - 1];
delete parsedData[lastKey]; delete parsedData[key];
//Sets local storage with missing object //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 //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 //Verify Main section is there on load
await expect await expect(page.locator('.l-browse-bar__object-name')).toContainText(stackedPlot.name);
.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()
]);
//Check that there is only one stacked item plot with a plot, the missing one will be empty //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); await expect(page.getByLabel('Stacked Plot Item')).toHaveCount(1);
//Verify that console.warn is thrown //Verify that console.warn was thrown
expect(errorLogs).toHaveLength(1); 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')
]);
}

View File

@ -249,7 +249,7 @@ export default class ObjectAPI {
.get(identifier, abortSignal) .get(identifier, abortSignal)
.then((domainObject) => { .then((domainObject) => {
delete this.cache[keystring]; delete this.cache[keystring];
if (!domainObject && abortSignal.aborted) { if (!domainObject && abortSignal?.aborted) {
// we've aborted the request // we've aborted the request
return; return;
} }

View File

@ -442,9 +442,13 @@ export default class TelemetryCollection extends EventEmitter {
} else { } else {
this.timeKey = undefined; this.timeKey = undefined;
// 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._warn(TIMESYSTEM_KEY_WARNING);
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION); this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
} }
}
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);

View File

@ -46,6 +46,11 @@ export default class SeriesCollection extends Collection {
this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this); this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this);
const domainObject = this.plot.get('domainObject'); const domainObject = this.plot.get('domainObject');
if (this.openmct.objects.isMissing(domainObject)) {
return;
}
if (domainObject.telemetry) { if (domainObject.telemetry) {
this.addTelemetryObject(domainObject); this.addTelemetryObject(domainObject);
} else { } else {

View File

@ -219,6 +219,11 @@ export default {
}, },
addChild(child) { 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); const id = this.openmct.objects.makeKeyString(child.identifier);
this.tickWidthMap[id] = { this.tickWidthMap[id] = {

View File

@ -170,6 +170,10 @@ export default {
//If this object is not persistable, then package it with it's parent //If this object is not persistable, then package it with it's parent
const plotObject = this.getPlotObject(); const plotObject = this.getPlotObject();
if (plotObject === null) {
return;
}
if (this.openmct.telemetry.isTelemetryObject(plotObject)) { if (this.openmct.telemetry.isTelemetryObject(plotObject)) {
this.subscribeToStaleness(plotObject); this.subscribeToStaleness(plotObject);
} else { } else {
@ -215,10 +219,6 @@ export default {
}, },
getPlotObject() { getPlotObject() {
this.checkPlotConfiguration(); 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; return this.childObject;
}, },
checkPlotConfiguration() { checkPlotConfiguration() {

View File

@ -190,7 +190,7 @@ export default {
this.soViewResizeObserver.observe(this.$refs.soView); this.soViewResizeObserver.observe(this.$refs.soView);
} }
const viewKey = this.getViewKey(); const viewKey = this.$refs.objectView?.viewKey;
this.supportsIndependentTime = this.domainObject && SupportedViewTypes.includes(viewKey); this.supportsIndependentTime = this.domainObject && SupportedViewTypes.includes(viewKey);
}, },
beforeUnmount() { beforeUnmount() {
@ -257,9 +257,6 @@ export default {
this.widthClass = wClass.trimStart(); this.widthClass = wClass.trimStart();
}, },
getViewKey() {
return this.$refs.objectView?.viewKey;
},
async showToolTip() { async showToolTip() {
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS; const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
this.buildToolTip(await this.getObjectPath(), BELOW, 'objectName'); this.buildToolTip(await this.getObjectPath(), BELOW, 'objectName');

View File

@ -29,7 +29,7 @@
@click="goToParent" @click="goToParent"
></button> ></button>
<div class="l-browse-bar__object-name--w c-object-label" :class="[statusClass]"> <div class="l-browse-bar__object-name--w c-object-label" :class="[statusClass]">
<div class="c-object-label__type-icon" :class="type.cssClass"> <div class="c-object-label__type-icon" :class="cssClass">
<span class="is-status__indicator" :title="`This item is ${status}`"></span> <span class="is-status__indicator" :title="`This item is ${status}`"></span>
</div> </div>
<span <span
@ -43,7 +43,7 @@
@mouseover.ctrl="showToolTip" @mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip" @mouseleave="hideToolTip"
> >
{{ domainObject.name }} {{ domainObjectName }}
</span> </span>
</div> </div>
</div> </div>
@ -150,8 +150,6 @@ import tooltipHelpers from '../../api/tooltips/tooltipMixins.js';
import { SupportedViewTypes } from '../../utils/constants.js'; import { SupportedViewTypes } from '../../utils/constants.js';
import ViewSwitcher from './ViewSwitcher.vue'; import ViewSwitcher from './ViewSwitcher.vue';
const PLACEHOLDER_OBJECT = {};
export default { export default {
components: { components: {
IndependentTimeConductor, IndependentTimeConductor,
@ -168,12 +166,12 @@ export default {
} }
} }
}, },
data: function () { data() {
return { return {
notebookTypes: [], notebookTypes: [],
showViewMenu: false, showViewMenu: false,
showSaveMenu: false, showSaveMenu: false,
domainObject: PLACEHOLDER_OBJECT, domainObject: undefined,
viewKey: undefined, viewKey: undefined,
isEditing: this.openmct.editor.isEditing(), isEditing: this.openmct.editor.isEditing(),
notebookEnabled: this.openmct.types.get('notebook'), notebookEnabled: this.openmct.types.get('notebook'),
@ -185,11 +183,22 @@ export default {
statusClass() { statusClass() {
return this.status ? `is-status--${this.status}` : ''; return this.status ? `is-status--${this.status}` : '';
}, },
supportsIndependentTime() {
return (
this.domainObject?.identifier &&
!this.openmct.objects.isMissing(this.domainObject) &&
SupportedViewTypes.includes(this.viewKey)
);
},
currentView() { currentView() {
return this.views.filter((v) => v.key === this.viewKey)[0] || {}; return this.views.filter((v) => v.key === this.viewKey)[0] || {};
}, },
views() { views() {
if (this.domainObject && this.openmct.router.started !== true) { if (this.domainObject && this.openmct.router.started === false) {
return [];
}
if (!this.domainObject) {
return []; return [];
} }
@ -203,25 +212,29 @@ export default {
}); });
}, },
hasParent() { hasParent() {
return toRaw(this.domainObject) !== PLACEHOLDER_OBJECT && this.parentUrl !== '/browse'; return toRaw(this.domainObject) && this.parentUrl !== '/browse';
}, },
parentUrl() { 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; const hash = this.openmct.router.getCurrentLocation().path;
return hash.slice(0, hash.lastIndexOf('/' + objectKeyString)); return hash.slice(0, hash.lastIndexOf('/' + objectKeyString));
}, },
type() { cssClass() {
const objectType = this.openmct.types.get(this.domainObject.type); if (!this.domainObject) {
if (!objectType) { return '';
return {};
} }
return objectType.definition; const objectType = this.openmct.types.get(this.domainObject.type);
if (!objectType) {
return '';
}
return objectType?.definition?.cssClass ?? '';
}, },
isPersistable() { isPersistable() {
let persistable = const persistable =
this.domainObject.identifier && this.domainObject?.identifier &&
this.openmct.objects.isPersistable(this.domainObject.identifier); this.openmct.objects.isPersistable(this.domainObject.identifier);
return persistable; return persistable;
@ -246,10 +259,8 @@ export default {
return 'Unlocked for editing - click to lock.'; return 'Unlocked for editing - click to lock.';
} }
}, },
supportsIndependentTime() { domainObjectName() {
const viewKey = this.getViewKey(); return this.domainObject?.name ?? '';
return this.domainObject && SupportedViewTypes.includes(viewKey);
} }
}, },
watch: { watch: {
@ -273,7 +284,7 @@ export default {
this.updateActionItems(this.actionCollection.getActionsObject()); this.updateActionItems(this.actionCollection.getActionsObject());
} }
}, },
mounted: function () { mounted() {
document.addEventListener('click', this.closeViewAndSaveMenu); document.addEventListener('click', this.closeViewAndSaveMenu);
this.promptUserbeforeNavigatingAway = this.promptUserbeforeNavigatingAway.bind(this); this.promptUserbeforeNavigatingAway = this.promptUserbeforeNavigatingAway.bind(this);
window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway); window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway);
@ -282,7 +293,7 @@ export default {
this.isEditing = isEditing; this.isEditing = isEditing;
}); });
}, },
beforeUnmount: function () { beforeUnmount() {
if (this.mutationObserver) { if (this.mutationObserver) {
this.mutationObserver(); this.mutationObserver();
} }
@ -323,9 +334,6 @@ export default {
edit() { edit() {
this.openmct.editor.edit(); this.openmct.editor.edit();
}, },
getViewKey() {
return this.viewKey;
},
promptUserandCancelEditing() { promptUserandCancelEditing() {
let dialog = this.openmct.overlays.dialog({ let dialog = this.openmct.overlays.dialog({
iconClass: 'alert', iconClass: 'alert',