Compare commits
	
		
			18 Commits
		
	
	
		
			v4.0.0-rc1
			...
			v3.0.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 0b949d16f0 | ||
|   | 1423c23297 | ||
|   | b043f26e49 | ||
|   | 856d88597e | ||
|   | c3ac07ebaf | ||
|   | 2002396d0e | ||
|   | 7980abcb38 | ||
|   | dedfd3b6f7 | ||
|   | 5b7b722ae8 | ||
|   | f5433c0d3b | ||
|   | 5619994e83 | ||
|   | 339640e0d6 | ||
|   | 32b68cf0df | ||
|   | 60e5bbc590 | ||
|   | 492e8055e5 | ||
|   | 796616fe3f | ||
|   | 0f5d3afc4a | ||
|   | 44415b3769 | 
| @@ -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,59 +59,57 @@ 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.info().annotations.push({ | ||||
|         type: 'issue', | ||||
|         description: 'https://github.com/nasa/openmct/issues/6818' | ||||
|       }); | ||||
|   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' | ||||
|     }); | ||||
|  | ||||
|       // Verify that both created objects appear in the list and are in the correct order | ||||
|       await assertInitialRecentObjectsListState(); | ||||
|     // Verify that both created objects appear in the list and are in the correct order | ||||
|     await assertInitialRecentObjectsListState(); | ||||
|  | ||||
|       // Navigate to the folder by clicking on the main object name in the recent objects list item | ||||
|       await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click(); | ||||
|       await page.waitForURL(`**/${folderA.uuid}?*`); | ||||
|       expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy(); | ||||
|     // Navigate to the folder by clicking on the main object name in the recent objects list item | ||||
|     await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click(); | ||||
|     await page.waitForURL(`**/${folderA.uuid}?*`); | ||||
|     expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy(); | ||||
|  | ||||
|       // Rename | ||||
|       folderA.name = `${folderA.name}-NEW!`; | ||||
|       await page.locator('.l-browse-bar__object-name').fill(''); | ||||
|       await page.locator('.l-browse-bar__object-name').fill(folderA.name); | ||||
|       await page.keyboard.press('Enter'); | ||||
|     // Rename | ||||
|     folderA.name = `${folderA.name}-NEW!`; | ||||
|     await page.locator('.l-browse-bar__object-name').fill(''); | ||||
|     await page.locator('.l-browse-bar__object-name').fill(folderA.name); | ||||
|     await page.keyboard.press('Enter'); | ||||
|  | ||||
|       // Verify rename has been applied in recent objects list item and objects paths | ||||
|       expect( | ||||
|         await page | ||||
|           .getByRole('navigation', { | ||||
|             name: clock.name | ||||
|           }) | ||||
|           .locator('a') | ||||
|           .filter({ | ||||
|             hasText: folderA.name | ||||
|           }) | ||||
|           .count() | ||||
|       ).toBeGreaterThan(0); | ||||
|       expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); | ||||
|  | ||||
|       // Delete | ||||
|       await page.click('button[title="Show selected item in tree"]'); | ||||
|       // Delete the folder via the left tree pane treeitem context menu | ||||
|     // Verify rename has been applied in recent objects list item and objects paths | ||||
|     expect( | ||||
|       await page | ||||
|         .getByRole('treeitem', { name: new RegExp(folderA.name) }) | ||||
|         .getByRole('navigation', { | ||||
|           name: clock.name | ||||
|         }) | ||||
|         .locator('a') | ||||
|         .click({ | ||||
|           button: 'right' | ||||
|         }); | ||||
|       await page.getByRole('menuitem', { name: /Remove/ }).click(); | ||||
|       await page.getByRole('button', { name: 'OK' }).click(); | ||||
|         .filter({ | ||||
|           hasText: folderA.name | ||||
|         }) | ||||
|         .count() | ||||
|     ).toBeGreaterThan(0); | ||||
|     expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); | ||||
|  | ||||
|       // Verify that the folder and clock are no longer in the recent objects list | ||||
|       await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden(); | ||||
|       await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden(); | ||||
|     } | ||||
|   ); | ||||
|     await page.click('button[title="Show selected item in tree"]'); | ||||
|     // Delete the folder via the left tree pane treeitem context menu | ||||
|     await page | ||||
|       .getByRole('treeitem', { name: new RegExp(folderA.name) }) | ||||
|       .locator('a') | ||||
|       .click({ | ||||
|         button: 'right' | ||||
|       }); | ||||
|     await page.getByRole('menuitem', { name: /Remove/ }).click(); | ||||
|     await page.getByRole('button', { name: 'OK' }).click(); | ||||
|  | ||||
|     // Verify that the folder and clock are no longer in the recent objects list | ||||
|     await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden(); | ||||
|     await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden(); | ||||
|   }); | ||||
|  | ||||
|   test('Clicking on an object in the path of a recent object navigates to the object', async ({ | ||||
|     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.1", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "devDependencies": { | ||||
|     "@babel/eslint-parser": "7.22.5", | ||||
| @@ -81,7 +81,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]; | ||||
|     let telemetryPath = []; | ||||
|     if (objectDetails?.type === 'folder') { | ||||
|       return telemetryPath; | ||||
|     } | ||||
|  | ||||
|     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) => | ||||
|             this.makeKeyString(telemetrySource) === this.makeKeyString(telemetryIdentifier) | ||||
|         sourceTelemetry = objectDetails.composition.find((telemetrySource) => | ||||
|           utils.identifierEquals(telemetrySource, telemetryIdentifier) | ||||
|         ); | ||||
|       } | ||||
|       const compositionElement = await this.get(sourceTelemetry); | ||||
|       if (!['yamcs.telemetry', 'generator'].includes(compositionElement.type)) { | ||||
|         return telemetryPath; | ||||
|       } | ||||
|       const telemetryKey = compositionElement.identifier.key; | ||||
|       const telemetryPathObjects = await this.getOriginalPath(telemetryKey); | ||||
|       telemetryPathObjects.forEach((pathObject) => { | ||||
|         if (pathObject.type === 'root') { | ||||
|           return; | ||||
|         } | ||||
|         telemetryPath.unshift(pathObject.name); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const compositionElement = await this.get(sourceTelemetry); | ||||
|     if (!['yamcs.telemetry', 'generator', 'yamcs.aggregate'].includes(compositionElement.type)) { | ||||
|       return telemetryPath; | ||||
|     } | ||||
|  | ||||
|     const telemetryPathObjects = await this.getOriginalPath(compositionElement.identifier); | ||||
|     telemetryPath = telemetryPathObjects | ||||
|       .reverse() | ||||
|       .filter((pathObject) => pathObject.type !== 'root') | ||||
|       .map((pathObject) => pathObject.name); | ||||
|  | ||||
|     return telemetryPath; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -42,6 +42,7 @@ class TimeContext extends EventEmitter { | ||||
|     this.activeClock = undefined; | ||||
|     this.offsets = undefined; | ||||
|     this.mode = undefined; | ||||
|     this.warnCounts = {}; | ||||
|  | ||||
|     this.tick = this.tick.bind(this); | ||||
|   } | ||||
| @@ -648,6 +649,17 @@ class TimeContext extends EventEmitter { | ||||
|   } | ||||
|  | ||||
|   #warnMethodDeprecated(method, newMethod) { | ||||
|     const MAX_CALLS = 1; // Only warn once per unique method and newMethod combination | ||||
|  | ||||
|     const key = `${method}.${newMethod}`; | ||||
|     const currentWarnCount = this.warnCounts[key] || 0; | ||||
|  | ||||
|     if (currentWarnCount >= MAX_CALLS) { | ||||
|       return; // Don't warn if already warned once | ||||
|     } | ||||
|  | ||||
|     this.warnCounts[key] = currentWarnCount + 1; | ||||
|  | ||||
|     let message = `[DEPRECATION WARNING]: The ${method} API method is deprecated and will be removed in a future version of Open MCT.`; | ||||
|  | ||||
|     if (newMethod) { | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|   | ||||
| @@ -864,6 +864,7 @@ export default { | ||||
|       if (this.domainObject.configuration) { | ||||
|         const persistedLayers = this.domainObject.configuration.layers; | ||||
|         if (!persistedLayers) { | ||||
|           this.layers.forEach((layer) => (layer.visible = false)); | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -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: '', | ||||
| @@ -394,7 +395,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]; | ||||
|  | ||||
| @@ -636,6 +641,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 | ||||
| @@ -789,6 +795,7 @@ export default { | ||||
|       }; | ||||
|       this.config.xAxis.set('range', newRange); | ||||
|       if (!isTick) { | ||||
|         this.annotatedPoints = []; | ||||
|         this.clearPanZoomHistory(); | ||||
|         this.synchronizeIfBoundsMatch(); | ||||
|         this.loadMoreData(newRange, true); | ||||
| @@ -1785,6 +1792,9 @@ export default { | ||||
|       }); | ||||
|       this.config.xAxis.set('frozen', true); | ||||
|       this.setStatus(); | ||||
|       if (!this.annotationsEverLoaded) { | ||||
|         this.loadAnnotations(); | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     resumeRealtimeData() { | ||||
|   | ||||
| @@ -42,7 +42,8 @@ import configStore from '../configuration/ConfigStore'; | ||||
| import PlotConfigurationModel from '../configuration/PlotConfigurationModel'; | ||||
| import LimitLine from './LimitLine.vue'; | ||||
| import LimitLabel from './LimitLabel.vue'; | ||||
| import Vue from 'vue'; | ||||
| import mount from 'utils/mount'; | ||||
| import { toRaw } from 'vue'; | ||||
|  | ||||
| const MARKER_SIZE = 6.0; | ||||
| const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0; | ||||
| @@ -315,7 +316,7 @@ export default { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const elements = this.seriesElements.get(series); | ||||
|       const elements = this.seriesElements.get(toRaw(series)); | ||||
|       elements.lines.forEach(function (line) { | ||||
|         this.lines.splice(this.lines.indexOf(line), 1); | ||||
|         line.destroy(); | ||||
| @@ -333,7 +334,7 @@ export default { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const elements = this.seriesElements.get(series); | ||||
|       const elements = this.seriesElements.get(toRaw(series)); | ||||
|       if (elements.alarmSet) { | ||||
|         elements.alarmSet.destroy(); | ||||
|         this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1); | ||||
| @@ -349,7 +350,7 @@ export default { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const elements = this.seriesElements.get(series); | ||||
|       const elements = this.seriesElements.get(toRaw(series)); | ||||
|       elements.pointSets.forEach(function (pointSet) { | ||||
|         this.pointSets.splice(this.pointSets.indexOf(pointSet), 1); | ||||
|         pointSet.destroy(); | ||||
| @@ -473,7 +474,7 @@ export default { | ||||
|       this.$emit('plotReinitializeCanvas'); | ||||
|     }, | ||||
|     removeChartElement(series) { | ||||
|       const elements = this.seriesElements.get(series); | ||||
|       const elements = this.seriesElements.get(toRaw(series)); | ||||
|  | ||||
|       elements.lines.forEach(function (line) { | ||||
|         this.lines.splice(this.lines.indexOf(line), 1); | ||||
| @@ -488,7 +489,7 @@ export default { | ||||
|         this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1); | ||||
|       } | ||||
|  | ||||
|       this.seriesElements.delete(series); | ||||
|       this.seriesElements.delete(toRaw(series)); | ||||
|  | ||||
|       this.clearLimitLines(series); | ||||
|     }, | ||||
| @@ -554,7 +555,7 @@ export default { | ||||
|         this.alarmSets.push(elements.alarmSet); | ||||
|       } | ||||
|  | ||||
|       this.seriesElements.set(series, elements); | ||||
|       this.seriesElements.set(toRaw(series), elements); | ||||
|     }, | ||||
|     makeLimitLines(series) { | ||||
|       this.clearLimitLines(series); | ||||
| @@ -573,10 +574,10 @@ export default { | ||||
|         this.limitLines.push(limitLine); | ||||
|       } | ||||
|  | ||||
|       this.seriesLimits.set(series, limitElements); | ||||
|       this.seriesLimits.set(toRaw(series), limitElements); | ||||
|     }, | ||||
|     clearLimitLines(series) { | ||||
|       const seriesLimits = this.seriesLimits.get(series); | ||||
|       const seriesLimits = this.seriesLimits.get(toRaw(series)); | ||||
|  | ||||
|       if (seriesLimits) { | ||||
|         seriesLimits.limitLines.forEach(function (line) { | ||||
| @@ -584,7 +585,7 @@ export default { | ||||
|           line.destroy(); | ||||
|         }, this); | ||||
|  | ||||
|         this.seriesLimits.delete(series); | ||||
|         this.seriesLimits.delete(toRaw(series)); | ||||
|       } | ||||
|     }, | ||||
|     canDraw(yAxisId) { | ||||
| @@ -747,16 +748,14 @@ export default { | ||||
|         left: 0, | ||||
|         top: this.drawAPI.y(limit.point.y) | ||||
|       }; | ||||
|       let LimitLineClass = Vue.extend(LimitLine); | ||||
|       const component = new LimitLineClass({ | ||||
|         propsData: { | ||||
|       const { vNode } = mount(LimitLine, { | ||||
|         props: { | ||||
|           point, | ||||
|           limit | ||||
|         } | ||||
|       }); | ||||
|       component.$mount(); | ||||
|  | ||||
|       return component.$el; | ||||
|       return vNode.el; | ||||
|     }, | ||||
|     getLimitOverlap(limit, overlapMap) { | ||||
|       //calculate if limit lines are too close to each other | ||||
| @@ -792,16 +791,14 @@ export default { | ||||
|         left: 0, | ||||
|         top: this.drawAPI.y(limit.point.y) | ||||
|       }; | ||||
|       let LimitLabelClass = Vue.extend(LimitLabel); | ||||
|       const component = new LimitLabelClass({ | ||||
|         propsData: { | ||||
|       const { vNode } = mount(LimitLabel, { | ||||
|         props: { | ||||
|           limit: Object.assign({}, overlap, limit), | ||||
|           point | ||||
|         } | ||||
|       }); | ||||
|       component.$mount(); | ||||
|  | ||||
|       return component.$el; | ||||
|       return vNode.el; | ||||
|     }, | ||||
|     drawAlarmPoints(alarmSet) { | ||||
|       this.drawAPI.drawLimitPoints( | ||||
| @@ -829,56 +826,32 @@ export default { | ||||
|         ); | ||||
|       } | ||||
|     }, | ||||
|     annotatedPointWithinRange(annotatedPoint, xRange, yRange) { | ||||
|       if (!yRange) { | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       const xValue = annotatedPoint.series.getXVal(annotatedPoint.point); | ||||
|       const yValue = annotatedPoint.series.getYVal(annotatedPoint.point); | ||||
|  | ||||
|       return ( | ||||
|         xValue > xRange.min && xValue < xRange.max && yValue > yRange.min && yValue < yRange.max | ||||
|       ); | ||||
|     }, | ||||
|     drawAnnotatedPoints(yAxisId) { | ||||
|       // 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)) { | ||||
|             const canvasXValue = this.offset[yAxisId].xVal( | ||||
|               annotatedPoint.point, | ||||
|               annotatedPoint.series | ||||
|             ); | ||||
|             const canvasYValue = this.offset[yAxisId].yVal( | ||||
|               annotatedPoint.point, | ||||
|               annotatedPoint.series | ||||
|             ); | ||||
|             const pointToDraw = new Float32Array([canvasXValue, canvasYValue]); | ||||
|             const drawnPoint = uniquePointsToDraw.some((rawPoint) => { | ||||
|               return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1]; | ||||
|             }); | ||||
|             if (!drawnPoint) { | ||||
|               uniquePointsToDraw.push(pointToDraw); | ||||
|               this.drawAnnotatedPoint(annotatedPoint, pointToDraw); | ||||
|             } | ||||
|           // 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 | ||||
|           ); | ||||
|           const canvasYValue = this.offset[yAxisId].yVal( | ||||
|             annotatedPoint.point, | ||||
|             annotatedPoint.series | ||||
|           ); | ||||
|           const pointToDraw = new Float32Array([canvasXValue, canvasYValue]); | ||||
|           const drawnPoint = uniquePointsToDraw.some((rawPoint) => { | ||||
|             return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1]; | ||||
|           }); | ||||
|           if (!drawnPoint) { | ||||
|             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.lastTimestamp = new Date(this.openmct.time.now()); | ||||
|       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; | ||||
|   }; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user