mirror of
https://github.com/nasa/openmct.git
synced 2024-12-20 05:37:53 +00:00
Merge branch 'release/3.0.1' into fix/VIPEROMCT-388
This commit is contained in:
commit
a8e32e2214
@ -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',
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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)
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
78
e2e/tests/functional/renaming.e2e.spec.js
Normal file
78
e2e/tests/functional/renaming.e2e.spec.js
Normal 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);
|
||||
});
|
||||
});
|
@ -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"]');
|
||||
}
|
||||
|
273
e2e/tests/performance/tagging.perf.spec.js
Normal file
273
e2e/tests/performance/tagging.perf.spec.js
Normal 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);
|
||||
});
|
||||
});
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,6 +3,7 @@
|
||||
height: auto;
|
||||
width: auto;
|
||||
padding: $interiorMargin;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.c-tooltip {
|
||||
|
@ -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]);
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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 &&
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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: {
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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]);
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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 };
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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: {
|
||||
|
@ -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: {
|
||||
|
@ -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);
|
||||
|
@ -184,7 +184,7 @@ export default {
|
||||
this.$emit('popupLoaded');
|
||||
this.setTimeContext();
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.stopFollowingTimeContext();
|
||||
},
|
||||
methods: {
|
||||
|
@ -75,7 +75,7 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
|
||||
},
|
||||
mounted: function () {
|
||||
|
@ -194,7 +194,7 @@ export default {
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
created() {
|
||||
this.initialize();
|
||||
},
|
||||
beforeUnmount() {
|
||||
|
@ -36,7 +36,7 @@ export default {
|
||||
this.timeConductorOptionsHolder = this.$el;
|
||||
this.timeConductorOptionsHolder.addEventListener('click', this.showPopup);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.clearPopup();
|
||||
},
|
||||
methods: {
|
||||
|
@ -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: {
|
||||
|
@ -173,7 +173,7 @@ export default {
|
||||
this.setOffsets();
|
||||
document.addEventListener('click', this.hide);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('click', this.hide);
|
||||
},
|
||||
methods: {
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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 };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
|
@ -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
34
src/utils/throttle.js
Normal 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;
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user