Merge branch 'release/3.0.1' into fix/VIPEROMCT-388

This commit is contained in:
Khalid Adil 2023-08-23 12:09:18 -05:00
commit a8e32e2214
57 changed files with 1022 additions and 321 deletions

View File

@ -13,7 +13,7 @@ module.exports = {
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:compat/recommended', 'plugin:compat/recommended',
'plugin:vue/recommended', 'plugin:vue/vue3-recommended',
'plugin:you-dont-need-lodash-underscore/compatible', 'plugin:you-dont-need-lodash-underscore/compatible',
'plugin:prettier/recommended' 'plugin:prettier/recommended'
], ],
@ -28,6 +28,8 @@ module.exports = {
} }
}, },
rules: { rules: {
'vue/no-deprecated-dollar-listeners-api': 'warn',
'vue/no-deprecated-events-api': 'warn',
'vue/no-v-for-template-key': 'off', 'vue/no-v-for-template-key': 'off',
'vue/no-v-for-template-key-on-child': 'error', 'vue/no-v-for-template-key-on-child': 'error',
'prettier/prettier': 'error', 'prettier/prettier': 'error',

View File

@ -35,6 +35,7 @@
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator"). * @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
* @property {string} [name] the desired name of the created domain object. * @property {string} [name] the desired name of the created domain object.
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object. * @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
* @property {Object<string, string>} [customParameters] any additional parameters to be passed to the domain object's form. E.g. '[aria-label="Data Rate (hz)"]': {'0.1'}
*/ */
/** /**
@ -65,7 +66,10 @@ const { expect } = require('@playwright/test');
* @param {CreateObjectOptions} options * @param {CreateObjectOptions} options
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object. * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/ */
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) { async function createDomainObjectWithDefaults(
page,
{ type, name, parent = 'mine', customParameters = {} }
) {
if (!name) { if (!name) {
name = `${type}:${genUuid()}`; name = `${type}:${genUuid()}`;
} }
@ -94,6 +98,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
await notesInput.fill(page.testNotes); await notesInput.fill(page.testNotes);
} }
// If there are any further parameters, fill them in
for (const [key, value] of Object.entries(customParameters)) {
const input = page.locator(`form[name="mctForm"] ${key}`);
await input.fill('');
await input.fill(value);
}
// Click OK button and wait for Navigate event // Click OK button and wait for Navigate event
await Promise.all([ await Promise.all([
page.waitForLoadState(), page.waitForLoadState(),
@ -177,7 +188,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
await page.click(`li:text("Plan")`); await page.click(`li:text("Plan")`);
// Modify the name input field of the domain object to accept 'name' // Modify the name input field of the domain object to accept 'name'
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); const nameInput = page.getByLabel('Title', { exact: true });
await nameInput.fill(''); await nameInput.fill('');
await nameInput.fill(name); await nameInput.fill(name);
@ -410,8 +421,18 @@ async function setEndOffset(page, offset) {
await setTimeConductorOffset(page, offset); await setTimeConductorOffset(page, offset);
} }
/**
* Set the time conductor bounds in fixed time mode
*
* NOTE: Unless explicitly testing the Time Conductor itself, it is advised to instead
* navigate directly to the object with the desired time bounds using `navigateToObjectWithFixedTimeBounds()`.
* @param {import('@playwright/test').Page} page
* @param {string} startDate
* @param {string} endDate
*/
async function setTimeConductorBounds(page, startDate, endDate) { async function setTimeConductorBounds(page, startDate, endDate) {
// Bring up the time conductor popup // Bring up the time conductor popup
expect(await page.locator('.l-shell__time-conductor.c-compact-tc').count()).toBe(1);
await page.click('.l-shell__time-conductor.c-compact-tc'); await page.click('.l-shell__time-conductor.c-compact-tc');
await setTimeBounds(page, startDate, endDate); await setTimeBounds(page, startDate, endDate);
@ -419,20 +440,31 @@ async function setTimeConductorBounds(page, startDate, endDate) {
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
} }
/**
* Set the independent time conductor bounds in fixed time mode
* @param {import('@playwright/test').Page} page
* @param {string} startDate
* @param {string} endDate
*/
async function setIndependentTimeConductorBounds(page, startDate, endDate) { async function setIndependentTimeConductorBounds(page, startDate, endDate) {
// Activate Independent Time Conductor in Fixed Time Mode // Activate Independent Time Conductor in Fixed Time Mode
await page.getByRole('switch').click(); await page.getByRole('switch').click();
// Bring up the time conductor popup // Bring up the time conductor popup
await page.click('.c-conductor-holder--compact .c-compact-tc'); await page.click('.c-conductor-holder--compact .c-compact-tc');
await expect(page.locator('.itc-popout')).toBeInViewport();
await expect(page.locator('.itc-popout')).toBeVisible();
await setTimeBounds(page, startDate, endDate); await setTimeBounds(page, startDate, endDate);
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
} }
/**
* Set the bounds of the visible conductor in fixed time mode
* @param {import('@playwright/test').Page} page
* @param {string} startDate
* @param {string} endDate
*/
async function setTimeBounds(page, startDate, endDate) { async function setTimeBounds(page, startDate, endDate) {
if (startDate) { if (startDate) {
// Fill start time // Fill start time
@ -549,6 +581,21 @@ async function getCanvasPixels(page, canvasSelector) {
return getTelemValuePromise; return getTelemValuePromise;
} }
/**
* @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName
* @param {string} url
* @param {string} newName
*/
async function renameObjectFromContextMenu(page, url, newName) {
await openObjectTreeContextMenu(page, url);
await page.click('li:text("Edit Properties")');
const nameInput = page.getByLabel('Title', { exact: true });
await nameInput.fill('');
await nameInput.fill(newName);
await page.click('[aria-label="Save"]');
}
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
module.exports = { module.exports = {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
@ -567,5 +614,6 @@ module.exports = {
setTimeConductorBounds, setTimeConductorBounds,
setIndependentTimeConductorBounds, setIndependentTimeConductorBounds,
selectInspectorTab, selectInspectorTab,
waitForPlotsToRender waitForPlotsToRender,
renameObjectFromContextMenu
}; };

View File

@ -260,6 +260,7 @@ test.describe('Display Layout', () => {
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({ test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({
page page
}) => { }) => {
await setFixedTimeMode(page);
// Create another Sine Wave Generator // Create another Sine Wave Generator
const anotherSineWaveObject = await createDomainObjectWithDefaults(page, { const anotherSineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator' type: 'Sine Wave Generator'
@ -316,10 +317,20 @@ test.describe('Display Layout', () => {
// wait for annotations requests to be batched and requested // wait for annotations requests to be batched and requested
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// Network requests for the composite telemetry with multiple items should be: // Network requests for the composite telemetry with multiple items should be:
// 1. a single batched request for annotations // 1. a single batched request for annotations
expect(networkRequests.length).toBe(1); expect(networkRequests.length).toBe(1);
await setRealTimeMode(page);
networkRequests = [];
await page.reload();
// wait for annotations to not load (if we have any, we've got a problem)
await page.waitForLoadState('networkidle');
// In real time mode, we don't fetch annotations at all
expect(networkRequests.length).toBe(0);
}); });
}); });

View File

@ -29,6 +29,10 @@ const {
test.describe('Flexible Layout', () => { test.describe('Flexible Layout', () => {
let sineWaveObject; let sineWaveObject;
let clockObject; let clockObject;
let treePane;
let sineWaveGeneratorTreeItem;
let clockTreeItem;
let flexibleLayout;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
@ -41,23 +45,27 @@ test.describe('Flexible Layout', () => {
clockObject = await createDomainObjectWithDefaults(page, { clockObject = await createDomainObjectWithDefaults(page, {
type: 'Clock' type: 'Clock'
}); });
// Create a Flexible Layout
flexibleLayout = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Define the Sine Wave Generator and Clock tree items
treePane = page.getByRole('tree', {
name: 'Main Tree'
});
sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
clockTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(clockObject.name)
});
}); });
test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({
page page
}) => { }) => {
const treePane = page.getByRole('tree', { await page.goto(flexibleLayout.url);
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const clockTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(clockObject.name)
});
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Flexible Layout // Edit Flexible Layout
await page.locator('[title="Edit"]').click(); await page.locator('[title="Edit"]').click();
@ -78,19 +86,79 @@ test.describe('Flexible Layout', () => {
dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first(); dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'false'); await expect(dragWrapper).toHaveAttribute('draggable', 'false');
}); });
test('changing toolbar settings in edit mode is immediately reflected and persists upon save', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6942'
});
await page.goto(flexibleLayout.url);
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
// Click on the first frame to select it
await page.locator('.c-fl-container__frame').first().click();
await expect(page.locator('.c-fl-container__frame > .c-frame').first()).toHaveAttribute(
's-selected',
''
);
// Assert the toolbar is visible
await expect(page.locator('.c-toolbar')).toBeInViewport();
// Assert the layout is in columns orientation
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
// Change the layout to rows orientation
await page.getByTitle('Columns layout').click();
// Assert the layout is in rows orientation
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
// Assert the frame of the first item is visible
await expect(page.locator('.c-so-view').first()).not.toHaveClass(/c-so-view--no-frame/);
// Hide the frame of the first item
await page.getByTitle('Frame visible').click();
// Assert the frame is hidden
await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);
// Assert there are 2 containers
expect(await page.locator('.c-fl-container').count()).toEqual(2);
// Add a container
await page.getByTitle('Add Container').click();
// Assert there are 3 containers
expect(await page.locator('.c-fl-container').count()).toEqual(3);
// Save Flexible Layout
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Nav away and back
await page.goto(sineWaveObject.url);
await page.goto(flexibleLayout.url);
// Wait for the first frame to be visible so we know the layout has loaded
await expect(page.locator('.c-fl-container').nth(0)).toBeInViewport();
// Assert the settings have persisted
expect(await page.locator('.c-fl-container').count()).toEqual(3);
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);
});
test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({
page page
}) => { }) => {
const treePane = page.getByRole('tree', { await page.goto(flexibleLayout.url);
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Flexible Layout // Edit Flexible Layout
await page.locator('[title="Edit"]').click(); await page.locator('[title="Edit"]').click();
@ -121,17 +189,7 @@ test.describe('Flexible Layout', () => {
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117' description: 'https://github.com/nasa/openmct/issues/3117'
}); });
const treePane = page.getByRole('tree', { await page.goto(flexibleLayout.url);
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
// Create a Flexible Layout
const flexibleLayout = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Flexible Layout // Edit Flexible Layout
await page.locator('[title="Edit"]').click(); await page.locator('[title="Edit"]').click();
@ -167,19 +225,13 @@ test.describe('Flexible Layout', () => {
const exampleImageryObject = await createDomainObjectWithDefaults(page, { const exampleImageryObject = await createDomainObjectWithDefaults(page, {
type: 'Example Imagery' type: 'Example Imagery'
}); });
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, { await page.goto(flexibleLayout.url);
type: 'Flexible Layout' // Edit Flexible Layout
});
// Edit Display Layout
await page.locator('[title="Edit"]').click(); await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree // Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const exampleImageryTreeItem = treePane.getByRole('treeitem', { const exampleImageryTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(exampleImageryObject.name) name: new RegExp(exampleImageryObject.name)
}); });

View File

@ -79,25 +79,25 @@ test.describe('Example Imagery Object', () => {
// Test independent fixed time with global fixed time // Test independent fixed time with global fixed time
// flip on independent time conductor // flip on independent time conductor
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click(); await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
// Adding in delay to address flakiness of ITC test-- button event handlers not registering in time
await expect(page.locator('#independentTCToggle')).toBeChecked();
await expect(page.locator('.c-compact-tc').first()).toBeVisible();
await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click(); await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click();
await page.getByRole('textbox', { name: 'Start date' }).fill('');
await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30'); await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30');
await page.keyboard.press('Tab'); await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'Start time' }).fill(''); await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00');
await page.getByRole('textbox', { name: 'Start time' }).type('01:01:00');
await page.keyboard.press('Tab'); await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End date' }).fill(''); await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30');
await page.getByRole('textbox', { name: 'End date' }).type('2021-12-30');
await page.keyboard.press('Tab'); await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End time' }).fill(''); await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');
await page.getByRole('textbox', { name: 'End time' }).type('01:11:00');
await page.keyboard.press('Tab'); await page.keyboard.press('Tab');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
// expect(await page.getByRole('button', { name: 'Submit time bounds' }).isEnabled()).toBe(true);
// await page.getByRole('button', { name: 'Submit time bounds' }).click();
// check image date // check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
// flip it off // flip it off
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click(); await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
@ -106,9 +106,12 @@ test.describe('Example Imagery Object', () => {
// Test independent fixed time with global realtime // Test independent fixed time with global realtime
await setRealTimeMode(page); await setRealTimeMode(page);
await expect(
page.getByRole('switch', { name: 'Enable Independent Time Conductor' })
).toBeEnabled();
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click(); await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
// check image date to be in the past // check image date to be in the past
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
// flip it off // flip it off
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click(); await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
// timestamp shouldn't be in the past anymore // timestamp shouldn't be in the past anymore

View File

@ -29,10 +29,11 @@ const {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
setRealTimeMode, setRealTimeMode,
setFixedTimeMode, setFixedTimeMode,
waitForPlotsToRender waitForPlotsToRender,
selectInspectorTab
} = require('../../../../appActions'); } = require('../../../../appActions');
test.describe.fixme('Plot Tagging', () => { test.describe('Plot Tagging', () => {
/** /**
* Given a canvas and a set of points, tags the points on the canvas. * Given a canvas and a set of points, tags the points on the canvas.
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
@ -41,7 +42,7 @@ test.describe.fixme('Plot Tagging', () => {
* @param {Number} yEnd a telemetry item with a plot * @param {Number} yEnd a telemetry item with a plot
* @returns {Promise} * @returns {Promise}
*/ */
async function createTags({ page, canvas, xEnd, yEnd }) { async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
await canvas.hover({ trial: true }); await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag //Alt+Shift Drag Start to select some points to tag
@ -90,15 +91,17 @@ test.describe.fixme('Plot Tagging', () => {
await expect(page.getByText('No tags to display for this item')).toBeVisible(); await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1); const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stabilize.
await waitForPlotsToRender(page);
//Wait for canvas to stablize. await expect(canvas).toBeInViewport();
await canvas.hover({ trial: true }); await canvas.hover({ trial: true });
// click on the tagged plot point // click on the tagged plot point
await canvas.click({ await canvas.click({
position: { position: {
x: 325, x: 100,
y: 377 y: 100
} }
}); });
@ -146,7 +149,10 @@ test.describe.fixme('Plot Tagging', () => {
// wait for plots to load // wait for plots to load
await waitForPlotsToRender(page); await waitForPlotsToRender(page);
await page.getByText('Annotations').click(); await expect(page.getByRole('tab', { name: 'Annotations' })).not.toHaveClass(/is-current/);
await selectInspectorTab(page, 'Annotations');
await expect(page.getByRole('tab', { name: 'Annotations' })).toHaveClass(/is-current/);
await expect(page.getByText('No tags to display for this item')).toBeVisible(); await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1); const canvas = page.locator('canvas').nth(1);
@ -171,8 +177,6 @@ test.describe.fixme('Plot Tagging', () => {
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6822' description: 'https://github.com/nasa/openmct/issues/6822'
}); });
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
test.slow();
const overlayPlot = await createDomainObjectWithDefaults(page, { const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot' type: 'Overlay Plot'
@ -181,13 +185,19 @@ test.describe.fixme('Plot Tagging', () => {
const alphaSineWave = await createDomainObjectWithDefaults(page, { const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', type: 'Sine Wave Generator',
name: 'Alpha Sine Wave', name: 'Alpha Sine Wave',
parent: overlayPlot.uuid parent: overlayPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
}); });
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', type: 'Sine Wave Generator',
name: 'Beta Sine Wave', name: 'Beta Sine Wave',
parent: overlayPlot.uuid parent: overlayPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.02'
}
}); });
await page.goto(overlayPlot.url); await page.goto(overlayPlot.url);
@ -200,9 +210,7 @@ test.describe.fixme('Plot Tagging', () => {
await createTags({ await createTags({
page, page,
canvas, canvas
xEnd: 700,
yEnd: 480
}); });
await setFixedTimeMode(page); await setFixedTimeMode(page);
@ -232,15 +240,15 @@ test.describe.fixme('Plot Tagging', () => {
test('Tags work with Plot View of telemetry items', async ({ page }) => { test('Tags work with Plot View of telemetry items', async ({ page }) => {
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator' type: 'Sine Wave Generator',
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
}); });
const canvas = page.locator('canvas').nth(1); const canvas = page.locator('canvas').nth(1);
await createTags({ await createTags({
page, page,
canvas, canvas
xEnd: 700,
yEnd: 480
}); });
await basicTagsTests(page); await basicTagsTests(page);
}); });
@ -253,13 +261,19 @@ test.describe.fixme('Plot Tagging', () => {
const alphaSineWave = await createDomainObjectWithDefaults(page, { const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', type: 'Sine Wave Generator',
name: 'Alpha Sine Wave', name: 'Alpha Sine Wave',
parent: stackedPlot.uuid parent: stackedPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
}); });
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', type: 'Sine Wave Generator',
name: 'Beta Sine Wave', name: 'Beta Sine Wave',
parent: stackedPlot.uuid parent: stackedPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.02'
}
}); });
await page.goto(stackedPlot.url); await page.goto(stackedPlot.url);

View File

@ -59,59 +59,57 @@ test.describe('Recent Objects', () => {
await page.mouse.move(0, 100); await page.mouse.move(0, 100);
await page.mouse.up(); await page.mouse.up();
}); });
test.fixme( test('Navigated objects show up in recents, object renames and deletions are reflected', async ({
'Navigated objects show up in recents, object renames and deletions are reflected', page
async ({ page }) => { }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6818' description: 'https://github.com/nasa/openmct/issues/6818'
}); });
// Verify that both created objects appear in the list and are in the correct order // Verify that both created objects appear in the list and are in the correct order
await assertInitialRecentObjectsListState(); await assertInitialRecentObjectsListState();
// Navigate to the folder by clicking on the main object name in the recent objects list item // Navigate to the folder by clicking on the main object name in the recent objects list item
await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click(); await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
await page.waitForURL(`**/${folderA.uuid}?*`); await page.waitForURL(`**/${folderA.uuid}?*`);
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy(); expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();
// Rename // Rename
folderA.name = `${folderA.name}-NEW!`; folderA.name = `${folderA.name}-NEW!`;
await page.locator('.l-browse-bar__object-name').fill(''); await page.locator('.l-browse-bar__object-name').fill('');
await page.locator('.l-browse-bar__object-name').fill(folderA.name); await page.locator('.l-browse-bar__object-name').fill(folderA.name);
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
// Verify rename has been applied in recent objects list item and objects paths // Verify rename has been applied in recent objects list item and objects paths
expect( expect(
await page
.getByRole('navigation', {
name: clock.name
})
.locator('a')
.filter({
hasText: folderA.name
})
.count()
).toBeGreaterThan(0);
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
// Delete
await page.click('button[title="Show selected item in tree"]');
// Delete the folder via the left tree pane treeitem context menu
await page await page
.getByRole('treeitem', { name: new RegExp(folderA.name) }) .getByRole('navigation', {
name: clock.name
})
.locator('a') .locator('a')
.click({ .filter({
button: 'right' hasText: folderA.name
}); })
await page.getByRole('menuitem', { name: /Remove/ }).click(); .count()
await page.getByRole('button', { name: 'OK' }).click(); ).toBeGreaterThan(0);
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
// Verify that the folder and clock are no longer in the recent objects list await page.click('button[title="Show selected item in tree"]');
await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden(); // Delete the folder via the left tree pane treeitem context menu
await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden(); await page
} .getByRole('treeitem', { name: new RegExp(folderA.name) })
); .locator('a')
.click({
button: 'right'
});
await page.getByRole('menuitem', { name: /Remove/ }).click();
await page.getByRole('button', { name: 'OK' }).click();
// Verify that the folder and clock are no longer in the recent objects list
await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
});
test('Clicking on an object in the path of a recent object navigates to the object', async ({ test('Clicking on an object in the path of a recent object navigates to the object', async ({
page, page,

View File

@ -0,0 +1,78 @@
/*****************************************************************************
* 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 for renaming objects, and their global application effects.
*/
const { test, expect } = require('../../baseFixtures.js');
const {
createDomainObjectWithDefaults,
renameObjectFromContextMenu
} = require('../../appActions.js');
test.describe('Renaming objects', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
});
test('When renaming objects, the browse bar and various components all update', async ({
page
}) => {
const folder = await createDomainObjectWithDefaults(page, {
type: 'Folder'
});
// Create a new 'Clock' object with default settings
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock',
parent: folder.uuid
});
// Rename
clock.name = `${clock.name}-NEW!`;
await renameObjectFromContextMenu(page, clock.url, clock.name);
// check inspector for new name
const titleValue = await page
.getByLabel('Title inspector properties')
.getByLabel('inspector property value')
.textContent();
expect(titleValue).toBe(clock.name);
// check browse bar for new name
await expect(page.locator(`.l-browse-bar >> text=${clock.name}`)).toBeVisible();
// check tree item for new name
await expect(
page.getByRole('listitem', {
name: clock.name
})
).toBeVisible();
// check recent objects for new name
await expect(
page.getByRole('navigation', {
name: clock.name
})
).toBeVisible();
// check title for new name
const title = await page.title();
expect(title).toBe(clock.name);
});
});

View File

@ -23,7 +23,7 @@
const { test, expect } = require('../../pluginFixtures.js'); const { test, expect } = require('../../pluginFixtures.js');
const { const {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
openObjectTreeContextMenu renameObjectFromContextMenu
} = require('../../appActions.js'); } = require('../../appActions.js');
test.describe('Main Tree', () => { test.describe('Main Tree', () => {
@ -249,18 +249,3 @@ async function expandTreePaneItemByName(page, name) {
}); });
await treeItem.locator('.c-disclosure-triangle').click(); await treeItem.locator('.c-disclosure-triangle').click();
} }
/**
* @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName
* @param {string} url
* @param {string} newName
*/
async function renameObjectFromContextMenu(page, url, newName) {
await openObjectTreeContextMenu(page, url);
await page.click('li:text("Edit Properties")');
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill('');
await nameInput.fill(newName);
await page.click('[aria-label="Save"]');
}

View File

@ -0,0 +1,273 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Tests to verify plot tagging performance.
*/
const { test, expect } = require('../../pluginFixtures');
const {
createDomainObjectWithDefaults,
setRealTimeMode,
setFixedTimeMode,
waitForPlotsToRender
} = require('../../appActions');
test.describe.fixme('Plot Tagging Performance', () => {
/**
* Given a canvas and a set of points, tags the points on the canvas.
* @param {import('@playwright/test').Page} page
* @param {HTMLCanvasElement} canvas a telemetry item with a plot
* @param {Number} xEnd a telemetry item with a plot
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
await page.keyboard.down('Alt');
await page.keyboard.down('Shift');
await canvas.dragTo(canvas, {
sourcePosition: {
x: 1,
y: 1
},
targetPosition: {
x: xEnd,
y: yEnd
}
});
//Alt Drag End
await page.keyboard.up('Alt');
await page.keyboard.up('Shift');
//Wait for canvas to stablize.
await canvas.hover({ trial: true });
// add some tags
await page.getByText('Annotations').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Science').click();
}
/**
* Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot
* @returns {Promise}
*/
async function testTelemetryItem(page, telemetryItem) {
// Check that telemetry item also received the tag
await page.goto(telemetryItem.url);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stablize.
await canvas.hover({ trial: true });
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
/**
* Given a page, tests that tags are searchable, deletable, and persist across reloads.
* @param {import('@playwright/test').Page} page
* @returns {Promise}
*/
async function basicTagsTests(page) {
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Clicking elsewhere should cause annotation selection to be cleared
await expect(page.getByText('No tags to display for this item')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText(/Sine Wave/)
.first()
.click();
// Delete Driving
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText('Science');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText('Drilling');
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
await expect(page.getByText('No results found')).toBeVisible();
//Reload Page
await page.reload({ waitUntil: 'domcontentloaded' });
// wait for plots to load
await waitForPlotsToRender(page);
await page.getByText('Annotations').click();
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Tags work with Overlay Plots', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6822'
});
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
test.slow();
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
let canvas = page.locator('canvas').nth(1);
// Switch to real-time mode
// Adding tags should pause the plot
await setRealTimeMode(page);
await createTags({
page,
canvas
});
await setFixedTimeMode(page);
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);
// set to real time mode
await setRealTimeMode(page);
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText('Alpha Sine Wave')
.first()
.click();
// wait for plots to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
// expect plot to be paused
await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible();
await setFixedTimeMode(page);
});
test('Tags work with Plot View of telemetry items', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
});
const canvas = page.locator('canvas').nth(1);
await createTags({
page,
canvas
});
await basicTagsTests(page);
});
test('Tags work with Stacked Plots', async ({ page }) => {
const stackedPlot = await createDomainObjectWithDefaults(page, {
type: 'Stacked Plot'
});
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: stackedPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: stackedPlot.uuid
});
await page.goto(stackedPlot.url);
const canvas = page.locator('canvas').nth(1);
await createTags({
page,
canvas,
xEnd: 700,
yEnd: 215
});
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);
});
});

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "3.0.0-SNAPSHOT", "version": "3.0.0",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "7.22.5", "@babel/eslint-parser": "7.22.5",
@ -80,7 +80,8 @@
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint", "clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js", "start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js", "start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0", "lint": "eslint example src e2e --ext .js openmct.js --max-warnings=0 && eslint example src --ext .vue",
"lint:spelling": "cspell \"**/*.{js,md,vue}\" --show-context --gitignore",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix", "lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "webpack --config ./.webpack/webpack.prod.js", "build:prod": "webpack --config ./.webpack/webpack.prod.js",
"build:dev": "webpack --config ./.webpack/webpack.dev.js", "build:dev": "webpack --config ./.webpack/webpack.dev.js",

View File

@ -22,9 +22,9 @@
<template> <template>
<div class="form-row c-form__row" :class="[{ first: first }, cssClass]" @onChange="onChange"> <div class="form-row c-form__row" :class="[{ first: first }, cssClass]" @onChange="onChange">
<div class="c-form-row__label" :title="row.description"> <label class="c-form-row__label" :title="row.description" :for="`form-${row.key}`">
{{ row.name }} {{ row.name }}
</div> </label>
<div class="c-form-row__state-indicator" :class="reqClass"></div> <div class="c-form-row__state-indicator" :class="reqClass"></div>
<div v-if="row.control" ref="rowElement" class="c-form-row__controls"></div> <div v-if="row.control" ref="rowElement" class="c-form-row__controls"></div>
</div> </div>

View File

@ -23,7 +23,14 @@
<template> <template>
<span class="form-control shell"> <span class="form-control shell">
<span class="field control" :class="model.cssClass"> <span class="field control" :class="model.cssClass">
<input v-model="field" type="text" :size="model.size" @input="updateText()" /> <input
:id="`form-${model.key}`"
v-model="field"
:name="model.key"
type="text"
:size="model.size"
@input="updateText()"
/>
</span> </span>
</span> </span>
</template> </template>

View File

@ -554,28 +554,34 @@ export default class ObjectAPI {
*/ */
async getTelemetryPath(identifier, telemetryIdentifier) { async getTelemetryPath(identifier, telemetryIdentifier) {
const objectDetails = await this.get(identifier); const objectDetails = await this.get(identifier);
const telemetryPath = []; let telemetryPath = [];
if (objectDetails.composition && !['folder'].includes(objectDetails.type)) { if (objectDetails?.type === 'folder') {
let sourceTelemetry = objectDetails.composition[0]; return telemetryPath;
}
let sourceTelemetry = null;
if (telemetryIdentifier && utils.identifierEquals(identifier, telemetryIdentifier)) {
sourceTelemetry = identifier;
} else if (objectDetails.composition) {
sourceTelemetry = objectDetails.composition[0];
if (telemetryIdentifier) { if (telemetryIdentifier) {
sourceTelemetry = objectDetails.composition.find( sourceTelemetry = objectDetails.composition.find((telemetrySource) =>
(telemetrySource) => utils.identifierEquals(telemetrySource, telemetryIdentifier)
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);
});
} }
const compositionElement = await this.get(sourceTelemetry);
if (!['yamcs.telemetry', 'generator', 'yamcs.aggregate'].includes(compositionElement.type)) {
return telemetryPath;
}
const telemetryPathObjects = await this.getOriginalPath(compositionElement.identifier);
telemetryPath = telemetryPathObjects
.reverse()
.filter((pathObject) => pathObject.type !== 'root')
.map((pathObject) => pathObject.name);
return telemetryPath; return telemetryPath;
} }

View File

@ -57,13 +57,22 @@ class TooltipAPI {
* @private for platform-internal use * @private for platform-internal use
*/ */
showTooltip(tooltip) { showTooltip(tooltip) {
this.removeAllTooltips();
this.activeToolTips.push(tooltip);
tooltip.show();
}
/**
* API method to allow for removing all tooltips
*/
removeAllTooltips() {
if (!this.activeToolTips?.length) {
return;
}
for (let i = this.activeToolTips.length - 1; i > -1; i--) { for (let i = this.activeToolTips.length - 1; i > -1; i--) {
this.activeToolTips[i].destroy(); this.activeToolTips[i].destroy();
this.activeToolTips.splice(i, 1); this.activeToolTips.splice(i, 1);
} }
this.activeToolTips.push(tooltip);
tooltip.show();
} }
/** /**

View File

@ -3,6 +3,7 @@
height: auto; height: auto;
width: auto; width: auto;
padding: $interiorMargin; padding: $interiorMargin;
overflow-wrap: break-word;
} }
.c-tooltip { .c-tooltip {

View File

@ -68,7 +68,12 @@ define([], function () {
this.updateRowData.bind(this) this.updateRowData.bind(this)
); );
this.openmct.telemetry.request(this.domainObject, { size: 1 }).then( const options = {
size: 1,
strategy: 'latest',
timeContext: this.openmct.time.getContextForView([])
};
this.openmct.telemetry.request(this.domainObject, options).then(
function (history) { function (history) {
if (!this.initialized && history.length > 0) { if (!this.initialized && history.length > 0) {
this.updateRowData(history[history.length - 1]); this.updateRowData(history[history.length - 1]);

View File

@ -98,9 +98,11 @@ export default function () {
}; };
function getScatterPlotFormControl(openmct) { function getScatterPlotFormControl(openmct) {
let destroyComponent;
return { return {
show(element, model, onChange) { show(element, model, onChange) {
const { vNode } = mount( const { vNode, destroy } = mount(
{ {
el: element, el: element,
components: { components: {
@ -122,8 +124,12 @@ export default function () {
element element
} }
); );
destroyComponent = destroy;
return vNode; return vNode;
},
destroy() {
destroyComponent();
} }
}; };
} }

View File

@ -30,7 +30,7 @@ export default function plugin(appliesToObjects, options = { indicator: true })
return function install(openmct) { return function install(openmct) {
if (installIndicator) { if (installIndicator) {
const { vNode } = mount( const { vNode, destroy } = mount(
{ {
components: { components: {
GlobalClearIndicator GlobalClearIndicator
@ -49,7 +49,8 @@ export default function plugin(appliesToObjects, options = { indicator: true })
let indicator = { let indicator = {
element: vNode.el, element: vNode.el,
key: 'global-clear-indicator', key: 'global-clear-indicator',
priority: openmct.priority.DEFAULT priority: openmct.priority.DEFAULT,
destroy: destroy
}; };
openmct.indicators.add(indicator); openmct.indicators.add(indicator);

View File

@ -201,9 +201,11 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
} }
requestLAD(telemetryObjects, requestOptions) { requestLAD(telemetryObjects, requestOptions) {
//We pass in the global time context here
let options = { let options = {
strategy: 'latest', strategy: 'latest',
size: 1 size: 1,
timeContext: this.openmct.time.getContextForView([])
}; };
if (requestOptions !== undefined) { if (requestOptions !== undefined) {

View File

@ -189,9 +189,11 @@ export default class TelemetryCriterion extends EventEmitter {
} }
requestLAD(telemetryObjects, requestOptions) { requestLAD(telemetryObjects, requestOptions) {
//We pass in the global time context here
let options = { let options = {
strategy: 'latest', strategy: 'latest',
size: 1 size: 1,
timeContext: this.openmct.time.getContextForView([])
}; };
if (requestOptions !== undefined) { if (requestOptions !== undefined) {

View File

@ -83,13 +83,19 @@ describe('The telemetry criterion', function () {
}); });
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
openmct.time = jasmine.createSpyObj('timeAPI', ['timeSystem', 'bounds', 'getAllTimeSystems']); openmct.time = jasmine.createSpyObj('timeAPI', [
'timeSystem',
'bounds',
'getAllTimeSystems',
'getContextForView'
]);
openmct.time.timeSystem.and.returnValue({ key: 'system' }); openmct.time.timeSystem.and.returnValue({ key: 'system' });
openmct.time.bounds.and.returnValue({ openmct.time.bounds.and.returnValue({
start: 0, start: 0,
end: 1 end: 1
}); });
openmct.time.getAllTimeSystems.and.returnValue([{ key: 'system' }]); openmct.time.getAllTimeSystems.and.returnValue([{ key: 'system' }]);
openmct.time.getContextForView.and.returnValue({});
testCriterionDefinition = { testCriterionDefinition = {
id: 'test-criterion-id', id: 'test-criterion-id',

View File

@ -20,14 +20,14 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<layout-frame <LayoutFrame
:item="item" :item="item"
:grid-size="gridSize" :grid-size="gridSize"
:is-editing="isEditing" :is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)" @move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')" @endMove="() => $emit('endMove')"
> >
<object-frame <ObjectFrame
v-if="domainObject" v-if="domainObject"
ref="objectFrame" ref="objectFrame"
:domain-object="domainObject" :domain-object="domainObject"
@ -37,7 +37,7 @@
:layout-font-size="item.fontSize" :layout-font-size="item.fontSize"
:layout-font="item.font" :layout-font="item.font"
/> />
</layout-frame> </LayoutFrame>
</template> </template>
<script> <script>

View File

@ -221,6 +221,8 @@ export default class DuplicateTask {
// parse reviver to replace identifiers // parse reviver to replace identifiers
clonedParent = JSON.parse(clonedParent, (key, value) => { clonedParent = JSON.parse(clonedParent, (key, value) => {
if ( if (
value !== null &&
value !== undefined &&
Object.prototype.hasOwnProperty.call(value, 'key') && Object.prototype.hasOwnProperty.call(value, 'key') &&
Object.prototype.hasOwnProperty.call(value, 'namespace') && Object.prototype.hasOwnProperty.call(value, 'namespace') &&
value.key === oldId.key && value.key === oldId.key &&

View File

@ -37,7 +37,6 @@
<template v-for="(container, index) in containers" :key="`component-${container.id}`"> <template v-for="(container, index) in containers" :key="`component-${container.id}`">
<drop-hint <drop-hint
v-if="index === 0 && containers.length > 1" v-if="index === 0 && containers.length > 1"
:key="`hint-top-${container.id}`"
class="c-fl-frame__drop-hint" class="c-fl-frame__drop-hint"
:index="-1" :index="-1"
:allow-drop="allowContainerDrop" :allow-drop="allowContainerDrop"
@ -59,7 +58,6 @@
<resize-handle <resize-handle
v-if="index !== containers.length - 1" v-if="index !== containers.length - 1"
:key="`handle-${container.id}`"
:index="index" :index="index"
:orientation="rowsLayout ? 'vertical' : 'horizontal'" :orientation="rowsLayout ? 'vertical' : 'horizontal'"
:is-editing="isEditing" :is-editing="isEditing"
@ -70,7 +68,6 @@
<drop-hint <drop-hint
v-if="containers.length > 1" v-if="containers.length > 1"
:key="`hint-bottom-${container.id}`"
class="c-fl-frame__drop-hint" class="c-fl-frame__drop-hint"
:index="index" :index="index"
:allow-drop="allowContainerDrop" :allow-drop="allowContainerDrop"
@ -137,15 +134,16 @@ export default {
ResizeHandle, ResizeHandle,
DropHint DropHint
}, },
inject: ['openmct', 'objectPath', 'layoutObject'], inject: ['openmct', 'objectPath', 'domainObject'],
props: { props: {
isEditing: Boolean isEditing: Boolean
}, },
data() { data() {
return { return {
domainObject: this.layoutObject,
newFrameLocation: [], newFrameLocation: [],
identifierMap: {} identifierMap: {},
containers: this.domainObject.configuration.containers,
rowsLayout: this.domainObject.configuration.rowsLayout
}; };
}, },
computed: { computed: {
@ -156,22 +154,22 @@ export default {
return 'Columns'; return 'Columns';
} }
}, },
containers() {
return this.domainObject.configuration.containers;
},
rowsLayout() {
return this.domainObject.configuration.rowsLayout;
},
allContainersAreEmpty() { allContainersAreEmpty() {
return this.containers.every((container) => container.frames.length === 0); return this.containers.every((container) => container.frames.length === 0);
} }
}, },
mounted() { created() {
this.buildIdentifierMap(); this.buildIdentifierMap();
this.composition = this.openmct.composition.get(this.domainObject); this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('remove', this.removeChildObject); this.composition.on('remove', this.removeChildObject);
this.composition.on('add', this.addFrame); this.composition.on('add', this.addFrame);
this.composition.load(); this.composition.load();
this.openmct.objects.observe(this.domainObject, 'configuration.containers', (containers) => {
this.containers = containers;
});
this.openmct.objects.observe(this.domainObject, 'configuration.rowsLayout', (rowsLayout) => {
this.rowsLayout = rowsLayout;
});
}, },
beforeUnmount() { beforeUnmount() {
this.composition.off('remove', this.removeChildObject); this.composition.off('remove', this.removeChildObject);
@ -211,20 +209,16 @@ export default {
let container = this.containers.filter((c) => c.id === containerId)[0]; let container = this.containers.filter((c) => c.id === containerId)[0];
let containerIndex = this.containers.indexOf(container); let containerIndex = this.containers.indexOf(container);
/* // remove associated domainObjects from composition
remove associated domainObjects from composition
*/
container.frames.forEach((f) => { container.frames.forEach((f) => {
this.removeFromComposition(f.domainObjectIdentifier); this.removeFromComposition(f.domainObjectIdentifier);
}); });
this.containers.splice(containerIndex, 1); this.containers.splice(containerIndex, 1);
/* // add a container when there are no containers in the FL,
add a container when there are no containers in the FL, // to prevent user from not being able to add a frame via
to prevent user from not being able to add a frame via // drag and drop.
drag and drop.
*/
if (this.containers.length === 0) { if (this.containers.length === 0) {
this.containers.push(new Container(100)); this.containers.push(new Container(100));
} }

View File

@ -47,17 +47,16 @@ export default class FlexibleLayoutViewProvider {
let component = null; let component = null;
return { return {
show: function (element, isEditing) { show(element, isEditing) {
const { vNode, destroy } = mount( const { vNode, destroy } = mount(
{ {
el: element,
components: { components: {
FlexibleLayoutComponent FlexibleLayoutComponent
}, },
provide: { provide: {
openmct: openmct, openmct,
objectPath, objectPath,
layoutObject: domainObject domainObject
}, },
data() { data() {
return { return {
@ -75,7 +74,7 @@ export default class FlexibleLayoutViewProvider {
component = vNode.componentInstance; component = vNode.componentInstance;
_destroy = destroy; _destroy = destroy;
}, },
getSelectionContext: function () { getSelectionContext() {
return { return {
item: domainObject, item: domainObject,
addContainer: component.$refs.flexibleLayout.addContainer, addContainer: component.$refs.flexibleLayout.addContainer,
@ -84,10 +83,10 @@ export default class FlexibleLayoutViewProvider {
type: 'flexible-layout' type: 'flexible-layout'
}; };
}, },
onEditModeChange: function (isEditing) { onEditModeChange(isEditing) {
component.isEditing = isEditing; component.isEditing = isEditing;
}, },
destroy: function (element) { destroy() {
if (_destroy) { if (_destroy) {
_destroy(); _destroy();
component = null; component = null;

View File

@ -33,6 +33,10 @@ describe('the plugin', function () {
let mockComposition; let mockComposition;
const testViewObject = { const testViewObject = {
identifier: {
namespace: '',
key: 'test-object'
},
id: 'test-object', id: 'test-object',
type: 'flexible-layout', type: 'flexible-layout',
configuration: { configuration: {
@ -116,6 +120,10 @@ describe('the plugin', function () {
beforeEach(() => { beforeEach(() => {
flexibleLayoutItem = { flexibleLayoutItem = {
identifier: {
namespace: '',
key: 'test-object'
},
id: 'test-object', id: 'test-object',
type: 'flexible-layout', type: 'flexible-layout',
configuration: { configuration: {

View File

@ -167,9 +167,11 @@ export default function () {
}; };
function getGaugeFormController(openmct) { function getGaugeFormController(openmct) {
let destroyComponent;
return { return {
show(element, model, onChange) { show(element, model, onChange) {
const { vNode } = mount( const { vNode, destroy } = mount(
{ {
el: element, el: element,
components: { components: {
@ -191,8 +193,12 @@ export default function () {
element element
} }
); );
destroyComponent = destroy;
return vNode.componentInstance; return vNode.componentInstance;
},
destroy() {
destroyComponent();
} }
}; };
} }

View File

@ -638,7 +638,11 @@ export default {
this.valueKey = this.metadata.valuesForHints(['range'])[0].source; this.valueKey = this.metadata.valuesForHints(['range'])[0].source;
this.openmct.telemetry.request(domainObject, { strategy: 'latest' }).then((values) => { const options = {
strategy: 'latest',
timeContext: this.openmct.time.getContextForView([])
};
this.openmct.telemetry.request(domainObject, options).then((values) => {
const length = values.length; const length = values.length;
this.updateValue(values[length - 1]); this.updateValue(values[length - 1]);
}); });

View File

@ -98,6 +98,9 @@ export default {
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();
} }
if (this.destroyImageryContainer) {
this.destroyImageryContainer();
}
}, },
methods: { methods: {
setTimeContext() { setTimeContext() {
@ -237,7 +240,10 @@ export default {
imageryContainer = existingContainer; imageryContainer = existingContainer;
imageryContainer.style.maxWidth = `${containerWidth}px`; imageryContainer.style.maxWidth = `${containerWidth}px`;
} else { } else {
const { vNode } = mount( if (this.destroyImageryContainer) {
this.destroyImageryContainer();
}
const { vNode, destroy } = mount(
{ {
components: { components: {
SwimLane SwimLane
@ -257,6 +263,7 @@ export default {
} }
); );
this.destroyImageryContainer = destroy;
const component = vNode.componentInstance; const component = vNode.componentInstance;
this.$refs.imageryHolder.appendChild(component.$el); this.$refs.imageryHolder.appendChild(component.$el);

View File

@ -21,11 +21,11 @@
--> -->
<template> <template>
<li class="c-inspect-properties__row"> <li class="c-inspect-properties__row" :aria-label="`${detail.name} inspector properties`">
<div class="c-inspect-properties__label"> <div class="c-inspect-properties__label" aria-label="inspector property name">
{{ detail.name }} {{ detail.name }}
</div> </div>
<div class="c-inspect-properties__value"> <div class="c-inspect-properties__value" aria-label="inspector property value">
{{ detail.value }} {{ detail.value }}
</div> </div>
</li> </li>

View File

@ -73,9 +73,43 @@ export default {
} }
}, },
async mounted() { async mounted() {
this.nameChangeListeners = {};
await this.createPathBreadCrumb(); await this.createPathBreadCrumb();
}, },
unmounted() {
Object.values(this.nameChangeListeners).forEach((unlisten) => {
unlisten();
});
},
methods: { methods: {
updateObjectPathName(keyString, newName) {
this.pathBreadCrumb = this.pathBreadCrumb.map((pathObject) => {
if (this.openmct.objects.makeKeyString(pathObject.domainObject.identifier) === keyString) {
return {
...pathObject,
domainObject: { ...pathObject.domainObject, name: newName }
};
}
return pathObject;
});
},
removeNameListenerFor(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (this.nameChangeListeners[keyString]) {
this.nameChangeListeners[keyString]();
delete this.nameChangeListeners[keyString];
}
},
addNameListenerFor(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.nameChangeListeners[keyString]) {
this.nameChangeListeners[keyString] = this.openmct.objects.observe(
domainObject,
'name',
this.updateObjectPathName.bind(this, keyString)
);
}
},
async createPathBreadCrumb() { async createPathBreadCrumb() {
if (!this.domainObject && this.parentDomainObject) { if (!this.domainObject && this.parentDomainObject) {
this.setPathBreadCrumb([this.parentDomainObject]); this.setPathBreadCrumb([this.parentDomainObject]);
@ -98,7 +132,15 @@ export default {
}; };
}); });
this.pathBreadCrumb.forEach((pathObject) => {
this.removeNameListenerFor(pathObject.domainObject);
});
this.pathBreadCrumb = pathBreadCrumb; this.pathBreadCrumb = pathBreadCrumb;
this.pathBreadCrumb.forEach((pathObject) => {
this.addNameListenerFor(pathObject.domainObject);
});
} }
} }
}; };

View File

@ -230,7 +230,22 @@ export default {
return `detail-${component}`; return `detail-${component}`;
}, },
updateSelection(selection) { updateSelection(selection) {
this.removeListener();
this.selection.splice(0, this.selection.length, ...selection); this.selection.splice(0, this.selection.length, ...selection);
if (this.domainObject) {
this.addListener();
}
},
removeListener() {
if (this.nameListener) {
this.nameListener();
this.nameListener = null;
}
},
addListener() {
this.nameListener = this.openmct.objects.observe(this.context?.item, 'name', (newValue) => {
this.context.item = { ...this.context?.item, name: newValue };
});
} }
} }
}; };

View File

@ -90,7 +90,10 @@ export default {
drawerElement.innerHTML = '<div></div>'; drawerElement.innerHTML = '<div></div>';
const divElement = document.querySelector('.l-shell__drawer div'); const divElement = document.querySelector('.l-shell__drawer div');
mount( if (this.destroySnapshotContainer) {
this.destroySnapshotContainer();
}
const { destroy } = mount(
{ {
el: divElement, el: divElement,
components: { components: {
@ -113,6 +116,7 @@ export default {
element: divElement element: divElement
} }
); );
this.destroySnapshotContainer = destroy;
}, },
updateSnapshotIndicatorTitle() { updateSnapshotIndicatorTitle() {
const snapshotCount = this.snapshotContainer.getSnapshots().length; const snapshotCount = this.snapshotContainer.getSnapshots().length;

View File

@ -83,7 +83,7 @@ function installBaseNotebookFunctionality(openmct) {
openmct.actions.register(new CopyToNotebookAction(openmct)); openmct.actions.register(new CopyToNotebookAction(openmct));
openmct.actions.register(new ExportNotebookAsTextAction(openmct)); openmct.actions.register(new ExportNotebookAsTextAction(openmct));
const { vNode } = mount( const { vNode, destroy } = mount(
{ {
components: { components: {
NotebookSnapshotIndicator NotebookSnapshotIndicator
@ -102,7 +102,8 @@ function installBaseNotebookFunctionality(openmct) {
const indicator = { const indicator = {
element: vNode.el, element: vNode.el,
key: 'notebook-snapshot-indicator', key: 'notebook-snapshot-indicator',
priority: openmct.priority.DEFAULT priority: openmct.priority.DEFAULT,
destroy: destroy
}; };
openmct.indicators.add(indicator); openmct.indicators.add(indicator);

View File

@ -24,7 +24,7 @@ import NotificationIndicator from './components/NotificationIndicator.vue';
export default function plugin() { export default function plugin() {
return function install(openmct) { return function install(openmct) {
const { vNode } = mount( const { vNode, destroy } = mount(
{ {
components: { components: {
NotificationIndicator NotificationIndicator
@ -42,7 +42,8 @@ export default function plugin() {
let indicator = { let indicator = {
key: 'notifications-indicator', key: 'notifications-indicator',
element: vNode.el, element: vNode.el,
priority: openmct.priority.DEFAULT priority: openmct.priority.DEFAULT,
destroy: destroy
}; };
openmct.indicators.add(indicator); openmct.indicators.add(indicator);
}; };

View File

@ -248,6 +248,7 @@ export default {
highlights: [], highlights: [],
annotatedPoints: [], annotatedPoints: [],
annotationSelections: [], annotationSelections: [],
annotationsEverLoaded: false,
lockHighlightPoint: false, lockHighlightPoint: false,
yKeyOptions: [], yKeyOptions: [],
yAxisLabel: '', yAxisLabel: '',
@ -396,7 +397,11 @@ export default {
); );
this.openmct.objectViews.on('clearData', this.clearData); this.openmct.objectViews.on('clearData', this.clearData);
this.$on('loadingComplete', this.loadAnnotations); this.$on('loadingComplete', () => {
if (this.annotationViewingAndEditingAllowed) {
this.loadAnnotations();
}
});
this.openmct.selection.on('change', this.updateSelection); this.openmct.selection.on('change', this.updateSelection);
this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes]; this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes];
@ -640,6 +645,7 @@ export default {
if (rawAnnotationsForPlot) { if (rawAnnotationsForPlot) {
this.annotatedPoints = this.findAnnotationPoints(rawAnnotationsForPlot); this.annotatedPoints = this.findAnnotationPoints(rawAnnotationsForPlot);
} }
this.annotationsEverLoaded = true;
}, },
loadSeriesData(series) { loadSeriesData(series) {
//this check ensures that duplicate requests don't happen on load //this check ensures that duplicate requests don't happen on load
@ -793,6 +799,7 @@ export default {
}; };
this.config.xAxis.set('range', newRange); this.config.xAxis.set('range', newRange);
if (!isTick) { if (!isTick) {
this.annotatedPoints = [];
this.clearPanZoomHistory(); this.clearPanZoomHistory();
this.synchronizeIfBoundsMatch(); this.synchronizeIfBoundsMatch();
this.loadMoreData(newRange, true); this.loadMoreData(newRange, true);
@ -1789,6 +1796,9 @@ export default {
}); });
this.config.xAxis.set('frozen', true); this.config.xAxis.set('frozen', true);
this.setStatus(); this.setStatus();
if (!this.annotationsEverLoaded) {
this.loadAnnotations();
}
}, },
resumeRealtimeData() { resumeRealtimeData() {

View File

@ -826,56 +826,32 @@ export default {
); );
} }
}, },
annotatedPointWithinRange(annotatedPoint, xRange, yRange) {
if (!yRange) {
return false;
}
const xValue = annotatedPoint.series.getXVal(annotatedPoint.point);
const yValue = annotatedPoint.series.getYVal(annotatedPoint.point);
return (
xValue > xRange.min && xValue < xRange.max && yValue > yRange.min && yValue < yRange.max
);
},
drawAnnotatedPoints(yAxisId) { drawAnnotatedPoints(yAxisId) {
// we should do this by series, and then plot all the points at once instead // we should do this by series, and then plot all the points at once instead
// of doing it one by one // of doing it one by one
if (this.annotatedPoints && this.annotatedPoints.length) { if (this.annotatedPoints && this.annotatedPoints.length) {
const uniquePointsToDraw = []; const uniquePointsToDraw = [];
const xRange = this.config.xAxis.get('displayRange');
let yRange;
if (yAxisId === this.config.yAxis.get('id')) {
yRange = this.config.yAxis.get('displayRange');
} else if (this.config.additionalYAxes.length) {
const yAxisForId = this.config.additionalYAxes.find(
(yAxis) => yAxis.get('id') === yAxisId
);
yRange = yAxisForId.get('displayRange');
}
const annotatedPoints = this.annotatedPoints.filter( const annotatedPoints = this.annotatedPoints.filter(
this.matchByYAxisId.bind(this, yAxisId) this.matchByYAxisId.bind(this, yAxisId)
); );
annotatedPoints.forEach((annotatedPoint) => { annotatedPoints.forEach((annotatedPoint) => {
// if the annotation is outside the range, don't draw it // annotation points are all within range (checked in MctPlot with FlatBush), so we don't need to check
if (this.annotatedPointWithinRange(annotatedPoint, xRange, yRange)) { const canvasXValue = this.offset[yAxisId].xVal(
const canvasXValue = this.offset[yAxisId].xVal( annotatedPoint.point,
annotatedPoint.point, annotatedPoint.series
annotatedPoint.series );
); const canvasYValue = this.offset[yAxisId].yVal(
const canvasYValue = this.offset[yAxisId].yVal( annotatedPoint.point,
annotatedPoint.point, annotatedPoint.series
annotatedPoint.series );
); const pointToDraw = new Float32Array([canvasXValue, canvasYValue]);
const pointToDraw = new Float32Array([canvasXValue, canvasYValue]); const drawnPoint = uniquePointsToDraw.some((rawPoint) => {
const drawnPoint = uniquePointsToDraw.some((rawPoint) => { return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1];
return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1]; });
}); if (!drawnPoint) {
if (!drawnPoint) { uniquePointsToDraw.push(pointToDraw);
uniquePointsToDraw.push(pointToDraw); this.drawAnnotatedPoint(annotatedPoint, pointToDraw);
this.drawAnnotatedPoint(annotatedPoint, pointToDraw);
}
} }
}); });
} }

View File

@ -197,7 +197,7 @@ export default {
this.composition.load(); this.composition.load();
} }
const { vNode } = mount( const { vNode, destroy } = mount(
{ {
components: { components: {
Plot Plot
@ -249,6 +249,7 @@ export default {
} }
); );
this.component = vNode.componentInstance; this.component = vNode.componentInstance;
this._destroy = destroy;
if (this.isEditing) { if (this.isEditing) {
this.setSelection(); this.setSelection();

View File

@ -62,6 +62,13 @@ export default class RemoteClock extends DefaultClock {
this.openmct.objects this.openmct.objects
.get(this.identifier) .get(this.identifier)
.then((domainObject) => { .then((domainObject) => {
// The start method is called when at least one listener registers with the clock.
// When the clock is changed, listeners are unregistered from the clock and the stop method is called.
// Sometimes, the objects.get call above does not resolve before the stop method is called.
// So when we proceed with the clock subscription below, we first need to ensure that there is at least one listener for our clock.
if (this.eventNames().length === 0) {
return;
}
this.openmct.time.on('timeSystem', this._timeSystemChange); this.openmct.time.on('timeSystem', this._timeSystemChange);
this.timeTelemetryObject = domainObject; this.timeTelemetryObject = domainObject;
this.metadata = this.openmct.telemetry.getMetadata(domainObject); this.metadata = this.openmct.telemetry.getMetadata(domainObject);

View File

@ -92,7 +92,7 @@ export default {
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem); this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL); this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
}, },
beforeDestroy() { beforeUnmount() {
clearInterval(this.resizeTimer); clearInterval(this.resizeTimer);
}, },
methods: { methods: {

View File

@ -58,7 +58,7 @@ export default {
} }
} }
}, },
data: function () { data() {
const activeClock = this.getActiveClock(); const activeClock = this.getActiveClock();
return { return {
@ -66,11 +66,11 @@ export default {
clocks: [] clocks: []
}; };
}, },
mounted: function () { mounted() {
this.loadClocks(this.configuration.menuOptions); this.loadClocks(this.configuration.menuOptions);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock); this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
}, },
destroyed: function () { unmounted() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock); this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
}, },
methods: { methods: {

View File

@ -102,7 +102,7 @@ export default {
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem); this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.updateMode); this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.updateMode);
}, },
beforeDestroy() { beforeUnmount() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.addTimespan); this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.addTimespan);
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.addTimespan); this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.addTimespan);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem); this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem);

View File

@ -184,7 +184,7 @@ export default {
this.$emit('popupLoaded'); this.$emit('popupLoaded');
this.setTimeContext(); this.setTimeContext();
}, },
beforeDestroy() { beforeUnmount() {
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
}, },
methods: { methods: {

View File

@ -75,7 +75,7 @@ export default {
} }
} }
}, },
beforeDestroy() { beforeUnmount() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock); this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
}, },
mounted: function () { mounted: function () {

View File

@ -194,7 +194,7 @@ export default {
deep: true deep: true
} }
}, },
mounted() { created() {
this.initialize(); this.initialize();
}, },
beforeUnmount() { beforeUnmount() {

View File

@ -36,7 +36,7 @@ export default {
this.timeConductorOptionsHolder = this.$el; this.timeConductorOptionsHolder = this.$el;
this.timeConductorOptionsHolder.addEventListener('click', this.showPopup); this.timeConductorOptionsHolder.addEventListener('click', this.showPopup);
}, },
beforeDestroy() { beforeUnmount() {
this.clearPopup(); this.clearPopup();
}, },
methods: { methods: {

View File

@ -157,7 +157,7 @@ export default {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300); this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem()))); this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())));
}, },
beforeDestroy() { beforeUnmount() {
this.clearAllValidation(); this.clearAllValidation();
}, },
methods: { methods: {

View File

@ -173,7 +173,7 @@ export default {
this.setOffsets(); this.setOffsets();
document.addEventListener('click', this.hide); document.addEventListener('click', this.hide);
}, },
beforeDestroy() { beforeUnmount() {
document.removeEventListener('click', this.hide); document.removeEventListener('click', this.hide);
}, },
methods: { methods: {

View File

@ -43,9 +43,11 @@
<script> <script>
import raf from 'utils/raf'; import raf from 'utils/raf';
import throttle from '../../../utils/throttle';
const moment = require('moment-timezone'); const moment = require('moment-timezone');
const momentDurationFormatSetup = require('moment-duration-format'); const momentDurationFormatSetup = require('moment-duration-format');
const refreshRateSeconds = 2;
momentDurationFormatSetup(moment); momentDurationFormatSetup(moment);
@ -68,38 +70,21 @@ export default {
}; };
}, },
computed: { computed: {
relativeTimestamp() {
let relativeTimestamp;
if (this.configuration && this.configuration.timestamp) {
relativeTimestamp = moment(this.configuration.timestamp).toDate();
} else if (this.configuration && this.configuration.timestamp === undefined) {
relativeTimestamp = undefined;
}
return relativeTimestamp;
},
timeDelta() { timeDelta() {
return this.lastTimestamp - this.relativeTimestamp; if (this.configuration.pausedTime) {
return Date.parse(this.configuration.pausedTime) - this.startTimeMs;
} else {
return this.lastTimestamp - this.startTimeMs;
}
},
startTimeMs() {
return Date.parse(this.configuration.timestamp);
}, },
timeTextValue() { timeTextValue() {
if (isNaN(this.timeDelta)) {
return null;
}
const toWholeSeconds = Math.abs(Math.floor(this.timeDelta / 1000) * 1000); const toWholeSeconds = Math.abs(Math.floor(this.timeDelta / 1000) * 1000);
return moment.duration(toWholeSeconds, 'ms').format(this.format, { trim: false }); return moment.duration(toWholeSeconds, 'ms').format(this.format, { trim: false });
}, },
pausedTime() {
let pausedTime;
if (this.configuration && this.configuration.pausedTime) {
pausedTime = moment(this.configuration.pausedTime).toDate();
} else if (this.configuration && this.configuration.pausedTime === undefined) {
pausedTime = undefined;
}
return pausedTime;
},
timerState() { timerState() {
let timerState = 'started'; let timerState = 'started';
if (this.configuration && this.configuration.timerState) { if (this.configuration && this.configuration.timerState) {
@ -179,13 +164,9 @@ export default {
} }
}, },
mounted() { mounted() {
this.unobserve = this.openmct.objects.observe( this.unobserve = this.openmct.objects.observe(this.domainObject, '*', (domainObject) => {
this.domainObject, this.configuration = domainObject.configuration;
'configuration', });
(configuration) => {
this.configuration = configuration;
}
);
this.$nextTick(() => { this.$nextTick(() => {
if (!this.configuration?.timerState) { if (!this.configuration?.timerState) {
const timerAction = !this.relativeTimestamp ? 'stop' : 'start'; const timerAction = !this.relativeTimestamp ? 'stop' : 'start';
@ -193,6 +174,7 @@ export default {
} }
this.handleTick = raf(this.handleTick); this.handleTick = raf(this.handleTick);
this.refreshTimerObject = throttle(this.refreshTimerObject, refreshRateSeconds * 1000);
this.openmct.time.on('tick', this.handleTick); this.openmct.time.on('tick', this.handleTick);
this.viewActionsCollection = this.openmct.actions.getActionsCollection( this.viewActionsCollection = this.openmct.actions.getActionsCollection(
@ -210,15 +192,11 @@ export default {
}, },
methods: { methods: {
handleTick() { handleTick() {
const isTimerRunning = !['paused', 'stopped'].includes(this.timerState); this.lastTimestamp = new Date(this.openmct.time.now());
this.refreshTimerObject();
if (isTimerRunning) { },
this.lastTimestamp = new Date(this.openmct.time.now()); refreshTimerObject() {
} this.openmct.objects.refresh(this.domainObject);
if (this.timerState === 'paused' && !this.lastTimestamp) {
this.lastTimestamp = this.pausedTime;
}
}, },
restartTimer() { restartTimer() {
this.triggerAction('timer.restart'); this.triggerAction('timer.restart');

View File

@ -25,7 +25,7 @@ import UserIndicator from './components/UserIndicator.vue';
export default function UserIndicatorPlugin() { export default function UserIndicatorPlugin() {
function addIndicator(openmct) { function addIndicator(openmct) {
const { vNode } = mount( const { vNode, destroy } = mount(
{ {
components: { components: {
UserIndicator UserIndicator
@ -43,7 +43,8 @@ export default function UserIndicatorPlugin() {
openmct.indicators.add({ openmct.indicators.add({
key: 'user-indicator', key: 'user-indicator',
element: vNode.el, element: vNode.el,
priority: openmct.priority.HIGH priority: openmct.priority.HIGH,
destroy: destroy
}); });
} }

View File

@ -78,6 +78,7 @@ export default {
}; };
}, },
async mounted() { async mounted() {
this.nameChangeListeners = {};
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
if (keyString && this.keyString !== keyString) { if (keyString && this.keyString !== keyString) {
@ -108,8 +109,16 @@ export default {
// remove ROOT and object itself from path // remove ROOT and object itself from path
this.orderedPath = pathWithDomainObject.slice(1, pathWithDomainObject.length - 1).reverse(); this.orderedPath = pathWithDomainObject.slice(1, pathWithDomainObject.length - 1).reverse();
} }
this.orderedPath.forEach((pathObject) => {
this.addNameListenerFor(pathObject.domainObject);
});
} }
}, },
unmounted() {
Object.values(this.nameChangeListeners).forEach((unlisten) => {
unlisten();
});
},
methods: { methods: {
/** /**
* Generate the hash url for the given object path, removing the '/ROOT' prefix if present. * Generate the hash url for the given object path, removing the '/ROOT' prefix if present.
@ -120,6 +129,34 @@ export default {
const path = `/browse/${this.openmct.objects.getRelativePath(objectPath)}`; const path = `/browse/${this.openmct.objects.getRelativePath(objectPath)}`;
return path.replace('ROOT/', ''); return path.replace('ROOT/', '');
},
updateObjectPathName(keyString, newName) {
this.orderedPath = this.orderedPath.map((pathObject) => {
if (this.openmct.objects.makeKeyString(pathObject.domainObject.identifier) === keyString) {
return {
...pathObject,
domainObject: { ...pathObject.domainObject, name: newName }
};
}
return pathObject;
});
},
removeNameListenerFor(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (this.nameChangeListeners[keyString]) {
this.nameChangeListeners[keyString]();
delete this.nameChangeListeners[keyString];
}
},
addNameListenerFor(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.nameChangeListeners[keyString]) {
this.nameChangeListeners[keyString] = this.openmct.objects.observe(
domainObject,
'name',
this.updateObjectPathName.bind(this, keyString)
);
}
} }
} }
}; };

View File

@ -104,12 +104,19 @@ export default {
if (this.statusUnsubscribe) { if (this.statusUnsubscribe) {
this.statusUnsubscribe(); this.statusUnsubscribe();
} }
if (this.nameUnsubscribe) {
this.nameUnsubscribe();
}
}, },
methods: { methods: {
updateSelection(selection) { updateSelection(selection) {
if (this.statusUnsubscribe) { if (this.statusUnsubscribe) {
this.statusUnsubscribe(); this.statusUnsubscribe();
this.statusUnsubscribe = undefined; this.statusUnsubscribe = null;
}
if (this.nameUnsubscribe) {
this.nameUnsubscribe();
this.nameUnsubscribe = null;
} }
if (selection.length === 0 || selection[0].length === 0) { if (selection.length === 0 || selection[0].length === 0) {
@ -132,6 +139,11 @@ export default {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.status = this.openmct.status.get(this.keyString); this.status = this.openmct.status.get(this.keyString);
this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus); this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus);
this.nameUnsubscribe = this.openmct.objects.observe(
this.domainObject,
'name',
this.updateName
);
} else if (selection[0][0].context.layoutItem) { } else if (selection[0][0].context.layoutItem) {
this.layoutItem = selection[0][0].context.layoutItem; this.layoutItem = selection[0][0].context.layoutItem;
} }
@ -144,6 +156,9 @@ export default {
}, },
updateStatus(status) { updateStatus(status) {
this.status = status; this.status = status;
},
updateName(newName) {
this.domainObject = { ...this.domainObject, name: newName };
} }
} }
}; };

View File

@ -59,13 +59,47 @@ export default {
}, },
mounted() { mounted() {
this.compositionCollections = {}; this.compositionCollections = {};
this.nameChangeListeners = {};
this.openmct.router.on('change:path', this.onPathChange); this.openmct.router.on('change:path', this.onPathChange);
this.getSavedRecentItems(); this.getSavedRecentItems();
}, },
unmounted() { unmounted() {
this.openmct.router.off('change:path', this.onPathChange); this.openmct.router.off('change:path', this.onPathChange);
Object.values(this.nameChangeListeners).forEach((unlisten) => {
unlisten();
});
}, },
methods: { methods: {
addNameListenerFor(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.nameChangeListeners[keyString]) {
this.nameChangeListeners[keyString] = this.openmct.objects.observe(
domainObject,
'name',
this.updateRecentObjectName.bind(this, keyString)
);
}
},
updateRecentObjectName(keyString, newName) {
this.recents = this.recents.map((recentObject) => {
if (
this.openmct.objects.makeKeyString(recentObject.domainObject.identifier) === keyString
) {
return {
...recentObject,
domainObject: { ...recentObject.domainObject, name: newName }
};
}
return recentObject;
});
},
removeNameListenerFor(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (this.nameChangeListeners[keyString]) {
this.nameChangeListeners[keyString]();
delete this.nameChangeListeners[keyString];
}
},
/** /**
* Add a composition collection to the map and register its remove handler * Add a composition collection to the map and register its remove handler
* @param {string} navigationPath * @param {string} navigationPath
@ -112,6 +146,7 @@ export default {
// Get composition collections and add composition listeners for composable objects // Get composition collections and add composition listeners for composable objects
savedRecents.forEach((recentObject) => { savedRecents.forEach((recentObject) => {
const { domainObject, navigationPath } = recentObject; const { domainObject, navigationPath } = recentObject;
this.addNameListenerFor(domainObject);
if (this.shouldTrackCompositionFor(domainObject)) { if (this.shouldTrackCompositionFor(domainObject)) {
this.compositionCollections[navigationPath] = {}; this.compositionCollections[navigationPath] = {};
this.compositionCollections[navigationPath].collection = this.compositionCollections[navigationPath].collection =
@ -161,6 +196,8 @@ export default {
return; return;
} }
this.addNameListenerFor(domainObject);
// Move the object to the top if its already existing in the recents list // Move the object to the top if its already existing in the recents list
const existingIndex = this.recents.findIndex((recentObject) => { const existingIndex = this.recents.findIndex((recentObject) => {
return navigationPath === recentObject.navigationPath; return navigationPath === recentObject.navigationPath;
@ -179,6 +216,7 @@ export default {
while (this.recents.length > MAX_RECENT_ITEMS) { while (this.recents.length > MAX_RECENT_ITEMS) {
const poppedRecentItem = this.recents.pop(); const poppedRecentItem = this.recents.pop();
this.removeCompositionListenerFor(poppedRecentItem.navigationPath); this.removeCompositionListenerFor(poppedRecentItem.navigationPath);
this.removeNameListenerFor(poppedRecentItem.domainObject);
} }
this.setSavedRecentItems(); this.setSavedRecentItems();
@ -236,6 +274,9 @@ export default {
label: 'OK', label: 'OK',
callback: () => { callback: () => {
localStorage.removeItem(LOCAL_STORAGE_KEY__RECENT_OBJECTS); localStorage.removeItem(LOCAL_STORAGE_KEY__RECENT_OBJECTS);
Object.values(this.nameChangeListeners).forEach((unlisten) => {
unlisten();
});
this.recents = []; this.recents = [];
dialog.dismiss(); dialog.dismiss();
this.$emit('setClearButtonDisabled', true); this.$emit('setClearButtonDisabled', true);

View File

@ -83,7 +83,7 @@
<div :style="childrenHeightStyles"> <div :style="childrenHeightStyles">
<tree-item <tree-item
v-for="(treeItem, index) in visibleItems" v-for="(treeItem, index) in visibleItems"
:key="`${treeItem.navigationPath}-${index}`" :key="`${treeItem.navigationPath}-${index}-${treeItem.object.name}`"
:node="treeItem" :node="treeItem"
:is-selector-tree="isSelectorTree" :is-selector-tree="isSelectorTree"
:selected-item="selectedItem" :selected-item="selectedItem"

View File

@ -37,9 +37,13 @@ define([], function () {
openmct.layout.$refs.browseBar.viewKey = viewProvider.key; openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
} }
function updateDocumentTitleOnNameMutation(domainObject) { function updateDocumentTitleOnNameMutation(newName) {
if (typeof domainObject.name === 'string' && domainObject.name !== document.title) { if (typeof newName === 'string' && newName !== document.title) {
document.title = domainObject.name; document.title = newName;
openmct.layout.$refs.browseBar.domainObject = {
...openmct.layout.$refs.browseBar.domainObject,
name: newName
};
} }
} }
@ -80,7 +84,11 @@ define([], function () {
let currentProvider = openmct.objectViews.getByProviderKey(currentViewKey); let currentProvider = openmct.objectViews.getByProviderKey(currentViewKey);
document.title = browseObject.name; //change document title to current object in main view document.title = browseObject.name; //change document title to current object in main view
// assign listener to global for later clearing // assign listener to global for later clearing
unobserve = openmct.objects.observe(browseObject, '*', updateDocumentTitleOnNameMutation); unobserve = openmct.objects.observe(
browseObject,
'name',
updateDocumentTitleOnNameMutation
);
if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) { if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) {
viewObject(browseObject, currentProvider); viewObject(browseObject, currentProvider);

34
src/utils/throttle.js Normal file
View File

@ -0,0 +1,34 @@
/**
* Creates a throttled function that only invokes the provided function at most once every
* specified number of milliseconds. Subsequent calls within the waiting period will be ignored.
* @param {Function} func The function to throttle.
* @param {number} wait The number of milliseconds to wait between successive calls to the function.
* @return {Function} Returns the new throttled function.
*/
export default function throttle(func, wait) {
let timeout;
let result;
let previous = 0;
return function (...args) {
const now = new Date().getTime();
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func(...args);
} else if (!timeout) {
timeout = setTimeout(() => {
previous = new Date().getTime();
timeout = null;
result = func(...args);
}, remaining);
}
return result;
};
}