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
*/
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')
]);
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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 {

View File

@ -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] = {

View File

@ -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() {

View File

@ -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');

View File

@ -29,7 +29,7 @@
@click="goToParent"
></button>
<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>
</div>
<span
@ -43,7 +43,7 @@
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
{{ domainObject.name }}
{{ domainObjectName }}
</span>
</div>
</div>
@ -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',