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: [
'eslint:recommended',
'plugin:compat/recommended',
'plugin:vue/recommended',
'plugin:vue/vue3-recommended',
'plugin:you-dont-need-lodash-underscore/compatible',
'plugin:prettier/recommended'
],
@ -28,6 +28,8 @@ module.exports = {
}
},
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-on-child': '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} [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 {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
* @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) {
name = `${type}:${genUuid()}`;
}
@ -94,6 +98,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
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
await Promise.all([
page.waitForLoadState(),
@ -177,7 +188,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
await page.click(`li:text("Plan")`);
// 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(name);
@ -410,8 +421,18 @@ async function setEndOffset(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) {
// 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 setTimeBounds(page, startDate, endDate);
@ -419,20 +440,31 @@ async function setTimeConductorBounds(page, startDate, endDate) {
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) {
// Activate Independent Time Conductor in Fixed Time Mode
await page.getByRole('switch').click();
// Bring up the time conductor popup
await page.click('.c-conductor-holder--compact .c-compact-tc');
await expect(page.locator('.itc-popout')).toBeVisible();
await expect(page.locator('.itc-popout')).toBeInViewport();
await setTimeBounds(page, startDate, endDate);
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) {
if (startDate) {
// Fill start time
@ -549,6 +581,21 @@ async function getCanvasPixels(page, canvasSelector) {
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
module.exports = {
createDomainObjectWithDefaults,
@ -567,5 +614,6 @@ module.exports = {
setTimeConductorBounds,
setIndependentTimeConductorBounds,
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 ({
page
}) => {
await setFixedTimeMode(page);
// Create another Sine Wave Generator
const anotherSineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
@ -316,10 +317,20 @@ test.describe('Display Layout', () => {
// wait for annotations requests to be batched and requested
await page.waitForLoadState('networkidle');
// Network requests for the composite telemetry with multiple items should be:
// 1. a single batched request for annotations
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', () => {
let sineWaveObject;
let clockObject;
let treePane;
let sineWaveGeneratorTreeItem;
let clockTreeItem;
let flexibleLayout;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
@ -41,23 +45,27 @@ test.describe('Flexible Layout', () => {
clockObject = await createDomainObjectWithDefaults(page, {
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 ({
page
}) => {
const treePane = page.getByRole('tree', {
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'
});
await page.goto(flexibleLayout.url);
// Edit Flexible Layout
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();
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 ({
page
}) => {
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
await page.goto(flexibleLayout.url);
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@ -121,17 +189,7 @@ test.describe('Flexible Layout', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
const treePane = page.getByRole('tree', {
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'
});
await page.goto(flexibleLayout.url);
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@ -167,19 +225,13 @@ test.describe('Flexible Layout', () => {
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
type: 'Example Imagery'
});
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Display Layout
await page.goto(flexibleLayout.url);
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
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', {
name: new RegExp(exampleImageryObject.name)
});

View File

@ -79,25 +79,25 @@ test.describe('Example Imagery Object', () => {
// Test independent fixed time with global fixed time
// flip on independent time conductor
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('textbox', { name: 'Start date' }).fill('');
await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'Start time' }).fill('');
await page.getByRole('textbox', { name: 'Start time' }).type('01:01:00');
await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End date' }).fill('');
await page.getByRole('textbox', { name: 'End date' }).type('2021-12-30');
await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End time' }).fill('');
await page.getByRole('textbox', { name: 'End time' }).type('01:11:00');
await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');
await page.keyboard.press('Tab');
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
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
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
await setRealTimeMode(page);
await expect(
page.getByRole('switch', { name: 'Enable Independent Time Conductor' })
).toBeEnabled();
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
// 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
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
// timestamp shouldn't be in the past anymore

View File

@ -29,10 +29,11 @@ const {
createDomainObjectWithDefaults,
setRealTimeMode,
setFixedTimeMode,
waitForPlotsToRender
waitForPlotsToRender,
selectInspectorTab
} = 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.
* @param {import('@playwright/test').Page} page
@ -41,7 +42,7 @@ test.describe.fixme('Plot Tagging', () => {
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd, yEnd }) {
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
await canvas.hover({ trial: true });
//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();
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 });
// click on the tagged plot point
await canvas.click({
position: {
x: 325,
y: 377
x: 100,
y: 100
}
});
@ -146,7 +149,10 @@ test.describe.fixme('Plot Tagging', () => {
// wait for plots to load
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();
const canvas = page.locator('canvas').nth(1);
@ -171,8 +177,6 @@ test.describe.fixme('Plot Tagging', () => {
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'
@ -181,13 +185,19 @@ test.describe.fixme('Plot Tagging', () => {
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: overlayPlot.uuid
parent: overlayPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: overlayPlot.uuid
parent: overlayPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.02'
}
});
await page.goto(overlayPlot.url);
@ -200,9 +210,7 @@ test.describe.fixme('Plot Tagging', () => {
await createTags({
page,
canvas,
xEnd: 700,
yEnd: 480
canvas
});
await setFixedTimeMode(page);
@ -232,15 +240,15 @@ test.describe.fixme('Plot Tagging', () => {
test('Tags work with Plot View of telemetry items', async ({ 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);
await createTags({
page,
canvas,
xEnd: 700,
yEnd: 480
canvas
});
await basicTagsTests(page);
});
@ -253,13 +261,19 @@ test.describe.fixme('Plot Tagging', () => {
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: stackedPlot.uuid
parent: stackedPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: stackedPlot.uuid
parent: stackedPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.02'
}
});
await page.goto(stackedPlot.url);

View File

@ -59,9 +59,9 @@ test.describe('Recent Objects', () => {
await page.mouse.move(0, 100);
await page.mouse.up();
});
test.fixme(
'Navigated objects show up in recents, object renames and deletions are reflected',
async ({ page }) => {
test('Navigated objects show up in recents, object renames and deletions are reflected', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6818'
@ -95,7 +95,6 @@ test.describe('Recent Objects', () => {
).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
@ -110,8 +109,7 @@ test.describe('Recent Objects', () => {
// 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 ({
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 {
createDomainObjectWithDefaults,
openObjectTreeContextMenu
renameObjectFromContextMenu
} = require('../../appActions.js');
test.describe('Main Tree', () => {
@ -249,18 +249,3 @@ async function expandTreePaneItemByName(page, name) {
});
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",
"version": "3.0.0-SNAPSHOT",
"version": "3.0.0",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.22.5",
@ -80,7 +80,8 @@
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "npx webpack serve --config ./.webpack/webpack.dev.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",
"build:prod": "webpack --config ./.webpack/webpack.prod.js",
"build:dev": "webpack --config ./.webpack/webpack.dev.js",

View File

@ -22,9 +22,9 @@
<template>
<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 }}
</div>
</label>
<div class="c-form-row__state-indicator" :class="reqClass"></div>
<div v-if="row.control" ref="rowElement" class="c-form-row__controls"></div>
</div>

View File

@ -23,7 +23,14 @@
<template>
<span class="form-control shell">
<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>
</template>

View File

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

View File

@ -57,13 +57,22 @@ class TooltipAPI {
* @private for platform-internal use
*/
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--) {
this.activeToolTips[i].destroy();
this.activeToolTips.splice(i, 1);
}
this.activeToolTips.push(tooltip);
tooltip.show();
}
/**

View File

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

View File

@ -68,7 +68,12 @@ define([], function () {
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) {
if (!this.initialized && history.length > 0) {
this.updateRowData(history[history.length - 1]);

View File

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

View File

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

View File

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

View File

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

View File

@ -83,13 +83,19 @@ describe('The telemetry criterion', function () {
});
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.bounds.and.returnValue({
start: 0,
end: 1
});
openmct.time.getAllTimeSystems.and.returnValue([{ key: 'system' }]);
openmct.time.getContextForView.and.returnValue({});
testCriterionDefinition = {
id: 'test-criterion-id',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -638,7 +638,11 @@ export default {
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;
this.updateValue(values[length - 1]);
});

View File

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

View File

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

View File

@ -73,9 +73,43 @@ export default {
}
},
async mounted() {
this.nameChangeListeners = {};
await this.createPathBreadCrumb();
},
unmounted() {
Object.values(this.nameChangeListeners).forEach((unlisten) => {
unlisten();
});
},
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() {
if (!this.domainObject && 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.forEach((pathObject) => {
this.addNameListenerFor(pathObject.domainObject);
});
}
}
};

View File

@ -230,7 +230,22 @@ export default {
return `detail-${component}`;
},
updateSelection(selection) {
this.removeListener();
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>';
const divElement = document.querySelector('.l-shell__drawer div');
mount(
if (this.destroySnapshotContainer) {
this.destroySnapshotContainer();
}
const { destroy } = mount(
{
el: divElement,
components: {
@ -113,6 +116,7 @@ export default {
element: divElement
}
);
this.destroySnapshotContainer = destroy;
},
updateSnapshotIndicatorTitle() {
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 ExportNotebookAsTextAction(openmct));
const { vNode } = mount(
const { vNode, destroy } = mount(
{
components: {
NotebookSnapshotIndicator
@ -102,7 +102,8 @@ function installBaseNotebookFunctionality(openmct) {
const indicator = {
element: vNode.el,
key: 'notebook-snapshot-indicator',
priority: openmct.priority.DEFAULT
priority: openmct.priority.DEFAULT,
destroy: destroy
};
openmct.indicators.add(indicator);

View File

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

View File

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

View File

@ -826,40 +826,17 @@ 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) {
// we should do this by series, and then plot all the points at once instead
// of doing it one by one
if (this.annotatedPoints && this.annotatedPoints.length) {
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(
this.matchByYAxisId.bind(this, yAxisId)
);
annotatedPoints.forEach((annotatedPoint) => {
// if the annotation is outside the range, don't draw it
if (this.annotatedPointWithinRange(annotatedPoint, xRange, yRange)) {
// annotation points are all within range (checked in MctPlot with FlatBush), so we don't need to check
const canvasXValue = this.offset[yAxisId].xVal(
annotatedPoint.point,
annotatedPoint.series
@ -876,7 +853,6 @@ export default {
uniquePointsToDraw.push(pointToDraw);
this.drawAnnotatedPoint(annotatedPoint, pointToDraw);
}
}
});
}
},

View File

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

View File

@ -62,6 +62,13 @@ export default class RemoteClock extends DefaultClock {
this.openmct.objects
.get(this.identifier)
.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.timeTelemetryObject = 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.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
beforeDestroy() {
beforeUnmount() {
clearInterval(this.resizeTimer);
},
methods: {

View File

@ -58,7 +58,7 @@ export default {
}
}
},
data: function () {
data() {
const activeClock = this.getActiveClock();
return {
@ -66,11 +66,11 @@ export default {
clocks: []
};
},
mounted: function () {
mounted() {
this.loadClocks(this.configuration.menuOptions);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
destroyed: function () {
unmounted() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
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.modeChanged, this.updateMode);
},
beforeDestroy() {
beforeUnmount() {
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.timeSystemChanged, this.updateTimeSystem);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -78,6 +78,7 @@ export default {
};
},
async mounted() {
this.nameChangeListeners = {};
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
if (keyString && this.keyString !== keyString) {
@ -108,8 +109,16 @@ export default {
// remove ROOT and object itself from path
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: {
/**
* 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)}`;
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) {
this.statusUnsubscribe();
}
if (this.nameUnsubscribe) {
this.nameUnsubscribe();
}
},
methods: {
updateSelection(selection) {
if (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) {
@ -132,6 +139,11 @@ export default {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.status = this.openmct.status.get(this.keyString);
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) {
this.layoutItem = selection[0][0].context.layoutItem;
}
@ -144,6 +156,9 @@ export default {
},
updateStatus(status) {
this.status = status;
},
updateName(newName) {
this.domainObject = { ...this.domainObject, name: newName };
}
}
};

View File

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

View File

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

View File

@ -37,9 +37,13 @@ define([], function () {
openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
}
function updateDocumentTitleOnNameMutation(domainObject) {
if (typeof domainObject.name === 'string' && domainObject.name !== document.title) {
document.title = domainObject.name;
function updateDocumentTitleOnNameMutation(newName) {
if (typeof newName === 'string' && newName !== document.title) {
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);
document.title = browseObject.name; //change document title to current object in main view
// 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)) {
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;
};
}