Compare commits
	
		
			27 Commits
		
	
	
		
			omm-releas
			...
			bugfix/iss
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					70870e3c29 | ||
| 
						 | 
					731187020b | ||
| 
						 | 
					ccdaa7d2cc | ||
| 
						 | 
					492289ad82 | ||
| 
						 | 
					c2957acea5 | ||
| 
						 | 
					2d502b7ac2 | ||
| 
						 | 
					981b1afb71 | ||
| 
						 | 
					f37d3aadb6 | ||
| 
						 | 
					66cbc32dd8 | ||
| 
						 | 
					4bec2c459c | ||
| 
						 | 
					97781c216e | ||
| 
						 | 
					9656783fbd | ||
| 
						 | 
					4f37daafb5 | ||
| 
						 | 
					80e16ae254 | ||
| 
						 | 
					cbba210ee7 | ||
| 
						 | 
					060ee35dbe | ||
| 
						 | 
					dda6800858 | ||
| 
						 | 
					7b2ad060ac | ||
| 
						 | 
					7917f0977d | ||
| 
						 | 
					2e5f8e7a47 | ||
| 
						 | 
					1c79d2b5cf | ||
| 
						 | 
					2b7129fe0b | ||
| 
						 | 
					decec8deef | ||
| 
						 | 
					50592fdc0e | ||
| 
						 | 
					8bc698cfed | ||
| 
						 | 
					430428f689 | ||
| 
						 | 
					c6987cd866 | 
@@ -144,7 +144,9 @@ async function createNotification(page, createNotificationOptions) {
 | 
			
		||||
 * @param {string} name
 | 
			
		||||
 */
 | 
			
		||||
async function expandTreePaneItemByName(page, name) {
 | 
			
		||||
    const treePane = page.locator('#tree-pane');
 | 
			
		||||
    const treePane = page.getByRole('tree', {
 | 
			
		||||
        name: 'Main Tree'
 | 
			
		||||
    });
 | 
			
		||||
    const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
 | 
			
		||||
    const expandTriangle = treeItem.locator('.c-disclosure-triangle');
 | 
			
		||||
    await expandTriangle.click();
 | 
			
		||||
@@ -218,6 +220,30 @@ async function openObjectTreeContextMenu(page, url) {
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Expands the entire object tree (every expandable tree item).
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {"Main Tree" | "Create Modal Tree"} [treeName="Main Tree"]
 | 
			
		||||
 */
 | 
			
		||||
async function expandEntireTree(page, treeName = "Main Tree") {
 | 
			
		||||
    const treeLocator = page.getByRole('tree', {
 | 
			
		||||
        name: treeName
 | 
			
		||||
    });
 | 
			
		||||
    const collapsedTreeItems = treeLocator.getByRole('treeitem', {
 | 
			
		||||
        expanded: false
 | 
			
		||||
    }).locator('span.c-disclosure-triangle.is-enabled');
 | 
			
		||||
 | 
			
		||||
    while (await collapsedTreeItems.count() > 0) {
 | 
			
		||||
        await collapsedTreeItems.nth(0).click();
 | 
			
		||||
 | 
			
		||||
        // FIXME: Replace hard wait with something event-driven.
 | 
			
		||||
        // Without the wait, this fails periodically due to a race condition
 | 
			
		||||
        // with Vue rendering (loop exits prematurely).
 | 
			
		||||
        // eslint-disable-next-line playwright/no-wait-for-timeout
 | 
			
		||||
        await page.waitForTimeout(200);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets the UUID of the currently focused object by parsing the current URL
 | 
			
		||||
 * and returning the last UUID in the path.
 | 
			
		||||
@@ -362,6 +388,7 @@ module.exports = {
 | 
			
		||||
    createDomainObjectWithDefaults,
 | 
			
		||||
    createNotification,
 | 
			
		||||
    expandTreePaneItemByName,
 | 
			
		||||
    expandEntireTree,
 | 
			
		||||
    createPlanFromJSON,
 | 
			
		||||
    openObjectTreeContextMenu,
 | 
			
		||||
    getHashUrlToDomainObject,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								e2e/helper/addInitNotebookWithUrls.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								e2e/helper/addInitNotebookWithUrls.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, 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 should be used to install the re-instal default Notebook plugin with a simple url whitelist.
 | 
			
		||||
// e.g.
 | 
			
		||||
// await page.addInitScript({ path: path.join(__dirname, 'addInitNotebookWithUrls.js') });
 | 
			
		||||
const NOTEBOOK_NAME = 'Notebook';
 | 
			
		||||
const URL_WHITELIST = ['google.com'];
 | 
			
		||||
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const openmct = window.openmct;
 | 
			
		||||
    openmct.install(openmct.plugins.Notebook(NOTEBOOK_NAME, URL_WHITELIST));
 | 
			
		||||
});
 | 
			
		||||
@@ -21,7 +21,7 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('../../pluginFixtures.js');
 | 
			
		||||
const { createDomainObjectWithDefaults, createNotification } = require('../../appActions.js');
 | 
			
		||||
const { createDomainObjectWithDefaults, createNotification, expandEntireTree } = require('../../appActions.js');
 | 
			
		||||
 | 
			
		||||
test.describe('AppActions', () => {
 | 
			
		||||
    test('createDomainObjectsWithDefaults', async ({ page }) => {
 | 
			
		||||
@@ -109,4 +109,57 @@ test.describe('AppActions', () => {
 | 
			
		||||
        await expect(page.locator('.c-message-banner')).toHaveClass(/error/);
 | 
			
		||||
        await page.locator('[aria-label="Dismiss"]').click();
 | 
			
		||||
    });
 | 
			
		||||
    test('expandEntireTree', async ({ page }) => {
 | 
			
		||||
        await page.goto('./', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        const rootFolder = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Folder'
 | 
			
		||||
        });
 | 
			
		||||
        const folder1 = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Folder',
 | 
			
		||||
            parent: rootFolder.uuid
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Clock',
 | 
			
		||||
            parent: folder1.uuid
 | 
			
		||||
        });
 | 
			
		||||
        const folder2 = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Folder',
 | 
			
		||||
            parent: folder1.uuid
 | 
			
		||||
        });
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Folder',
 | 
			
		||||
            parent: folder1.uuid
 | 
			
		||||
        });
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Display Layout',
 | 
			
		||||
            parent: folder2.uuid
 | 
			
		||||
        });
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Folder',
 | 
			
		||||
            parent: folder2.uuid
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await page.goto('./#/browse/mine');
 | 
			
		||||
        await expandEntireTree(page);
 | 
			
		||||
        const treePane = page.getByRole('tree', {
 | 
			
		||||
            name: "Main Tree"
 | 
			
		||||
        });
 | 
			
		||||
        const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false });
 | 
			
		||||
        expect(await treePaneCollapsedItems.count()).toBe(0);
 | 
			
		||||
 | 
			
		||||
        await page.goto('./#/browse/mine');
 | 
			
		||||
        //Click the Create button
 | 
			
		||||
        await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
        // Click the object specified by 'type'
 | 
			
		||||
        await page.click(`li[role='menuitem']:text("Clock")`);
 | 
			
		||||
        await expandEntireTree(page, "Create Modal Tree");
 | 
			
		||||
        const locatorTree = page.getByRole("tree", {
 | 
			
		||||
            name: "Create Modal Tree"
 | 
			
		||||
        });
 | 
			
		||||
        const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]');
 | 
			
		||||
        expect(await locatorTreeCollapsedItems.count()).toBe(0);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,9 @@ test.describe('Move & link item tests', () => {
 | 
			
		||||
        // Attempt to move parent to its own grandparent
 | 
			
		||||
        await page.locator('button[title="Show selected item in tree"]').click();
 | 
			
		||||
 | 
			
		||||
        const treePane = page.locator('#tree-pane');
 | 
			
		||||
        const treePane = page.getByRole('tree', {
 | 
			
		||||
            name: 'Main Tree'
 | 
			
		||||
        });
 | 
			
		||||
        await treePane.getByRole('treeitem', {
 | 
			
		||||
            name: 'Parent Folder'
 | 
			
		||||
        }).click({
 | 
			
		||||
@@ -63,28 +65,30 @@ test.describe('Move & link item tests', () => {
 | 
			
		||||
            name: /Move/
 | 
			
		||||
        }).click();
 | 
			
		||||
 | 
			
		||||
        const locatorTree = page.locator('#locator-tree');
 | 
			
		||||
        const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', {
 | 
			
		||||
        const createModalTree = page.getByRole('tree', {
 | 
			
		||||
            name: "Create Modal Tree"
 | 
			
		||||
        });
 | 
			
		||||
        const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', {
 | 
			
		||||
            name: myItemsFolderName
 | 
			
		||||
        });
 | 
			
		||||
        await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
 | 
			
		||||
        await myItemsLocatorTreeItem.click();
 | 
			
		||||
 | 
			
		||||
        const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
 | 
			
		||||
        const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
 | 
			
		||||
            name: parentFolder.name
 | 
			
		||||
        });
 | 
			
		||||
        await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
 | 
			
		||||
        await parentFolderLocatorTreeItem.click();
 | 
			
		||||
        await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
 | 
			
		||||
 | 
			
		||||
        const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
 | 
			
		||||
        const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
 | 
			
		||||
            name: new RegExp(childFolder.name)
 | 
			
		||||
        });
 | 
			
		||||
        await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
 | 
			
		||||
        await childFolderLocatorTreeItem.click();
 | 
			
		||||
        await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
 | 
			
		||||
 | 
			
		||||
        const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
 | 
			
		||||
        const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
 | 
			
		||||
            name: grandchildFolder.name
 | 
			
		||||
        });
 | 
			
		||||
        await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
 | 
			
		||||
@@ -195,7 +199,9 @@ test.describe('Move & link item tests', () => {
 | 
			
		||||
        // Attempt to move parent to its own grandparent
 | 
			
		||||
        await page.locator('button[title="Show selected item in tree"]').click();
 | 
			
		||||
 | 
			
		||||
        const treePane = page.locator('#tree-pane');
 | 
			
		||||
        const treePane = page.getByRole('tree', {
 | 
			
		||||
            name: 'Main Tree'
 | 
			
		||||
        });
 | 
			
		||||
        await treePane.getByRole('treeitem', {
 | 
			
		||||
            name: 'Parent Folder'
 | 
			
		||||
        }).click({
 | 
			
		||||
@@ -206,28 +212,30 @@ test.describe('Move & link item tests', () => {
 | 
			
		||||
            name: /Move/
 | 
			
		||||
        }).click();
 | 
			
		||||
 | 
			
		||||
        const locatorTree = page.locator('#locator-tree');
 | 
			
		||||
        const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', {
 | 
			
		||||
        const createModalTree = page.getByRole('tree', {
 | 
			
		||||
            name: "Create Modal Tree"
 | 
			
		||||
        });
 | 
			
		||||
        const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', {
 | 
			
		||||
            name: myItemsFolderName
 | 
			
		||||
        });
 | 
			
		||||
        await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
 | 
			
		||||
        await myItemsLocatorTreeItem.click();
 | 
			
		||||
 | 
			
		||||
        const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
 | 
			
		||||
        const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
 | 
			
		||||
            name: parentFolder.name
 | 
			
		||||
        });
 | 
			
		||||
        await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
 | 
			
		||||
        await parentFolderLocatorTreeItem.click();
 | 
			
		||||
        await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
 | 
			
		||||
 | 
			
		||||
        const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
 | 
			
		||||
        const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
 | 
			
		||||
            name: new RegExp(childFolder.name)
 | 
			
		||||
        });
 | 
			
		||||
        await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
 | 
			
		||||
        await childFolderLocatorTreeItem.click();
 | 
			
		||||
        await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
 | 
			
		||||
 | 
			
		||||
        const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
 | 
			
		||||
        const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
 | 
			
		||||
            name: grandchildFolder.name
 | 
			
		||||
        });
 | 
			
		||||
        await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
 | 
			
		||||
 
 | 
			
		||||
@@ -32,8 +32,7 @@ test.describe('Display Layout', () => {
 | 
			
		||||
 | 
			
		||||
        // Create Sine Wave Generator
 | 
			
		||||
        sineWaveObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Sine Wave Generator',
 | 
			
		||||
            name: "Test Sine Wave Generator"
 | 
			
		||||
            type: 'Sine Wave Generator'
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
 | 
			
		||||
@@ -48,7 +47,9 @@ test.describe('Display Layout', () => {
 | 
			
		||||
        // 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 Display Layout and save changes
 | 
			
		||||
        const treePane = page.locator('#tree-pane');
 | 
			
		||||
        const treePane = page.getByRole('tree', {
 | 
			
		||||
            name: 'Main Tree'
 | 
			
		||||
        });
 | 
			
		||||
        const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
            name: new RegExp(sineWaveObject.name)
 | 
			
		||||
        });
 | 
			
		||||
@@ -80,7 +81,9 @@ test.describe('Display Layout', () => {
 | 
			
		||||
        // 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 Display Layout and save changes
 | 
			
		||||
        const treePane = page.locator('#tree-pane');
 | 
			
		||||
        const treePane = page.getByRole('tree', {
 | 
			
		||||
            name: 'Main Tree'
 | 
			
		||||
        });
 | 
			
		||||
        const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
            name: new RegExp(sineWaveObject.name)
 | 
			
		||||
        });
 | 
			
		||||
@@ -116,7 +119,9 @@ test.describe('Display Layout', () => {
 | 
			
		||||
        // 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 Display Layout and save changes
 | 
			
		||||
        const treePane = page.locator('#tree-pane');
 | 
			
		||||
        const treePane = page.getByRole('tree', {
 | 
			
		||||
            name: 'Main Tree'
 | 
			
		||||
        });
 | 
			
		||||
        const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
            name: new RegExp(sineWaveObject.name)
 | 
			
		||||
        });
 | 
			
		||||
@@ -131,7 +136,7 @@ test.describe('Display Layout', () => {
 | 
			
		||||
        await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
 | 
			
		||||
 | 
			
		||||
        // Bring up context menu and remove
 | 
			
		||||
        await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
 | 
			
		||||
        await sineWaveGeneratorTreeItem.nth(1).click({ button: 'right' });
 | 
			
		||||
        await page.locator('li[role="menuitem"]:has-text("Remove")').click();
 | 
			
		||||
        await page.locator('button:has-text("OK")').click();
 | 
			
		||||
 | 
			
		||||
@@ -146,8 +151,7 @@ test.describe('Display Layout', () => {
 | 
			
		||||
        });
 | 
			
		||||
        // Create a Display Layout
 | 
			
		||||
        const displayLayout = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Display Layout',
 | 
			
		||||
            name: "Test Display Layout"
 | 
			
		||||
            type: 'Display Layout'
 | 
			
		||||
        });
 | 
			
		||||
        // Edit Display Layout
 | 
			
		||||
        await page.locator('[title="Edit"]').click();
 | 
			
		||||
@@ -155,7 +159,9 @@ test.describe('Display Layout', () => {
 | 
			
		||||
        // 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 Display Layout and save changes
 | 
			
		||||
        const treePane = page.locator('#tree-pane');
 | 
			
		||||
        const treePane = page.getByRole('tree', {
 | 
			
		||||
            name: 'Main Tree'
 | 
			
		||||
        });
 | 
			
		||||
        const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
            name: new RegExp(sineWaveObject.name)
 | 
			
		||||
        });
 | 
			
		||||
@@ -173,7 +179,7 @@ test.describe('Display Layout', () => {
 | 
			
		||||
        await page.goto(sineWaveObject.url);
 | 
			
		||||
 | 
			
		||||
        // Bring up context menu and remove
 | 
			
		||||
        await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
 | 
			
		||||
        await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
 | 
			
		||||
        await page.locator('li[role="menuitem"]:has-text("Remove")').click();
 | 
			
		||||
        await page.locator('button:has-text("OK")').click();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,26 +25,33 @@ const { createDomainObjectWithDefaults } = require('../../../../appActions');
 | 
			
		||||
 | 
			
		||||
test.describe('Flexible Layout', () => {
 | 
			
		||||
    let sineWaveObject;
 | 
			
		||||
    let clockObject;
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        await page.goto('./', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Create Sine Wave Generator
 | 
			
		||||
        sineWaveObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Sine Wave Generator',
 | 
			
		||||
            name: "Test Sine Wave Generator"
 | 
			
		||||
            type: 'Sine Wave Generator'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Create Clock Object
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Clock',
 | 
			
		||||
            name: "Test Clock"
 | 
			
		||||
        clockObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Clock'
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    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',
 | 
			
		||||
            name: "Test Flexible Layout"
 | 
			
		||||
            type: 'Flexible Layout'
 | 
			
		||||
        });
 | 
			
		||||
        // Edit Flexible Layout
 | 
			
		||||
        await page.locator('[title="Edit"]').click();
 | 
			
		||||
@@ -52,8 +59,8 @@ test.describe('Flexible Layout', () => {
 | 
			
		||||
        // 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 page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
 | 
			
		||||
        await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty');
 | 
			
		||||
        await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
 | 
			
		||||
        await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
 | 
			
		||||
        // Check that panes can be dragged while Flexible Layout is in Edit mode
 | 
			
		||||
        let dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
 | 
			
		||||
        await expect(dragWrapper).toHaveAttribute('draggable', 'true');
 | 
			
		||||
@@ -65,10 +72,15 @@ test.describe('Flexible Layout', () => {
 | 
			
		||||
        await expect(dragWrapper).toHaveAttribute('draggable', 'false');
 | 
			
		||||
    });
 | 
			
		||||
    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',
 | 
			
		||||
            name: "Test Flexible Layout"
 | 
			
		||||
            type: 'Flexible Layout'
 | 
			
		||||
        });
 | 
			
		||||
        // Edit Flexible Layout
 | 
			
		||||
        await page.locator('[title="Edit"]').click();
 | 
			
		||||
@@ -76,7 +88,7 @@ test.describe('Flexible Layout', () => {
 | 
			
		||||
        // 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 to the Flexible Layout and save changes
 | 
			
		||||
        await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
 | 
			
		||||
        await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
 | 
			
		||||
        await page.locator('button[title="Save"]').click();
 | 
			
		||||
        await page.locator('text=Save and Finish Editing').click();
 | 
			
		||||
 | 
			
		||||
@@ -86,7 +98,7 @@ test.describe('Flexible Layout', () => {
 | 
			
		||||
        await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
 | 
			
		||||
 | 
			
		||||
        // Bring up context menu and remove
 | 
			
		||||
        await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
 | 
			
		||||
        await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
 | 
			
		||||
        await page.locator('li[role="menuitem"]:has-text("Remove")').click();
 | 
			
		||||
        await page.locator('button:has-text("OK")').click();
 | 
			
		||||
 | 
			
		||||
@@ -98,10 +110,16 @@ 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',
 | 
			
		||||
            name: "Test Flexible Layout"
 | 
			
		||||
            type: 'Flexible Layout'
 | 
			
		||||
        });
 | 
			
		||||
        // Edit Flexible Layout
 | 
			
		||||
        await page.locator('[title="Edit"]').click();
 | 
			
		||||
@@ -109,7 +127,7 @@ test.describe('Flexible Layout', () => {
 | 
			
		||||
        // 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
 | 
			
		||||
        await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
 | 
			
		||||
        await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
 | 
			
		||||
        await page.locator('button[title="Save"]').click();
 | 
			
		||||
        await page.locator('text=Save and Finish Editing').click();
 | 
			
		||||
 | 
			
		||||
@@ -122,7 +140,7 @@ test.describe('Flexible Layout', () => {
 | 
			
		||||
        await page.goto(sineWaveObject.url);
 | 
			
		||||
 | 
			
		||||
        // Bring up context menu and remove
 | 
			
		||||
        await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
 | 
			
		||||
        await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
 | 
			
		||||
        await page.locator('li[role="menuitem"]:has-text("Remove")').click();
 | 
			
		||||
        await page.locator('button:has-text("OK")').click();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,11 @@ This test suite is dedicated to tests which verify the basic operations surround
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('../../../../pluginFixtures');
 | 
			
		||||
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
 | 
			
		||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
 | 
			
		||||
const nbUtils = require('../../../../helper/notebookUtils');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
const NOTEBOOK_NAME = 'Notebook';
 | 
			
		||||
 | 
			
		||||
test.describe('Notebook CRUD Operations', () => {
 | 
			
		||||
    test.fixme('Can create a Notebook Object', async ({ page }) => {
 | 
			
		||||
@@ -73,8 +76,7 @@ test.describe('Notebook section tests', () => {
 | 
			
		||||
 | 
			
		||||
        // Create Notebook
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Notebook',
 | 
			
		||||
            name: "Test Notebook"
 | 
			
		||||
            type: NOTEBOOK_NAME
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    test('Default and new sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => {
 | 
			
		||||
@@ -135,8 +137,7 @@ test.describe('Notebook page tests', () => {
 | 
			
		||||
 | 
			
		||||
        // Create Notebook
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Notebook',
 | 
			
		||||
            name: "Test Notebook"
 | 
			
		||||
            type: NOTEBOOK_NAME
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    //Test will need to be implemented after a refactor in #5713
 | 
			
		||||
@@ -207,24 +208,30 @@ test.describe('Notebook search tests', () => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Notebook entry tests', () => {
 | 
			
		||||
    // Create Notebook with URL Whitelist
 | 
			
		||||
    let notebookObject;
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        // eslint-disable-next-line no-undef
 | 
			
		||||
        await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitNotebookWithUrls.js') });
 | 
			
		||||
        await page.goto('./', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        notebookObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: NOTEBOOK_NAME
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
 | 
			
		||||
    test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => {
 | 
			
		||||
        await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Create Notebook
 | 
			
		||||
        const notebook = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Notebook',
 | 
			
		||||
            name: "Embed Test Notebook"
 | 
			
		||||
        });
 | 
			
		||||
        // Create Overlay Plot
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Overlay Plot',
 | 
			
		||||
            name: "Dropped Overlay Plot"
 | 
			
		||||
            type: 'Overlay Plot'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await expandTreePaneItemByName(page, 'My Items');
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
        await page.goto(notebook.url);
 | 
			
		||||
        await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area');
 | 
			
		||||
 | 
			
		||||
        const embed = page.locator('.c-ne__embed__link');
 | 
			
		||||
@@ -234,22 +241,16 @@ test.describe('Notebook entry tests', () => {
 | 
			
		||||
        expect(embedName).toBe('Dropped Overlay Plot');
 | 
			
		||||
    });
 | 
			
		||||
    test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => {
 | 
			
		||||
        await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Create Notebook
 | 
			
		||||
        const notebook = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Notebook',
 | 
			
		||||
            name: "Embed Test Notebook"
 | 
			
		||||
        });
 | 
			
		||||
        // Create Overlay Plot
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Overlay Plot',
 | 
			
		||||
            name: "Dropped Overlay Plot"
 | 
			
		||||
            type: 'Overlay Plot'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await expandTreePaneItemByName(page, 'My Items');
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
        await page.goto(notebook.url);
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
        await nbUtils.enterTextEntry(page, 'Entry to drop into');
 | 
			
		||||
        await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', 'text=Entry to drop into');
 | 
			
		||||
@@ -263,19 +264,14 @@ test.describe('Notebook entry tests', () => {
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
 | 
			
		||||
    test.fixme('previous and new entries can be deleted', async ({ page }) => {});
 | 
			
		||||
    test.fixme('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
 | 
			
		||||
    test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
 | 
			
		||||
        const TEST_LINK = 'http://www.google.com';
 | 
			
		||||
        await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Create Notebook
 | 
			
		||||
        const notebook = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Notebook',
 | 
			
		||||
            name: "Entry Link Test"
 | 
			
		||||
        });
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
        await expandTreePaneItemByName(page, 'My Items');
 | 
			
		||||
 | 
			
		||||
        await page.goto(notebook.url);
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
 | 
			
		||||
@@ -293,19 +289,14 @@ test.describe('Notebook entry tests', () => {
 | 
			
		||||
 | 
			
		||||
        expect(await validLink.count()).toBe(1);
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => {
 | 
			
		||||
    test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => {
 | 
			
		||||
        const TEST_LINK = 'www.google.com';
 | 
			
		||||
        await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Create Notebook
 | 
			
		||||
        const notebook = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Notebook',
 | 
			
		||||
            name: "Entry Link Test"
 | 
			
		||||
        });
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
        await expandTreePaneItemByName(page, 'My Items');
 | 
			
		||||
 | 
			
		||||
        await page.goto(notebook.url);
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
 | 
			
		||||
@@ -313,20 +304,70 @@ test.describe('Notebook entry tests', () => {
 | 
			
		||||
 | 
			
		||||
        expect(await invalidLink.count()).toBe(0);
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ page }) => {
 | 
			
		||||
    test('when a link is entered, but it is not in the whitelisted urls, it does not become clickable when viewing', async ({ page }) => {
 | 
			
		||||
        const TEST_LINK = 'http://www.bing.com';
 | 
			
		||||
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
 | 
			
		||||
        const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
 | 
			
		||||
        expect(await invalidLink.count()).toBe(0);
 | 
			
		||||
    });
 | 
			
		||||
    test('when a valid link with a subdomain and a valid domain in the whitelisted urls is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
 | 
			
		||||
        const INVALID_TEST_LINK = 'http://bing.google.com';
 | 
			
		||||
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`);
 | 
			
		||||
 | 
			
		||||
        const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`);
 | 
			
		||||
 | 
			
		||||
        expect(await validLink.count()).toBe(1);
 | 
			
		||||
    });
 | 
			
		||||
    test('when a valid secure link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
 | 
			
		||||
        const TEST_LINK = 'https://www.google.com';
 | 
			
		||||
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
 | 
			
		||||
        const validLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
 | 
			
		||||
        // Start waiting for popup before clicking. Note no await.
 | 
			
		||||
        const popupPromise = page.waitForEvent('popup');
 | 
			
		||||
 | 
			
		||||
        await validLink.click();
 | 
			
		||||
        const popup = await popupPromise;
 | 
			
		||||
 | 
			
		||||
        // Wait for the popup to load.
 | 
			
		||||
        await popup.waitForLoadState();
 | 
			
		||||
        expect.soft(popup.url()).toContain('www.google.com');
 | 
			
		||||
 | 
			
		||||
        expect(await validLink.count()).toBe(1);
 | 
			
		||||
    });
 | 
			
		||||
    test('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ page }) => {
 | 
			
		||||
        const TEST_LINK = 'http://www.google.com?bad=';
 | 
			
		||||
        const TEST_LINK_BAD = `http://www.google.com?bad=<script>alert('gimme your cookies')</script>`;
 | 
			
		||||
        await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Create Notebook
 | 
			
		||||
        const notebook = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Notebook',
 | 
			
		||||
            name: "Entry Link Test"
 | 
			
		||||
        });
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
        await expandTreePaneItemByName(page, 'My Items');
 | 
			
		||||
 | 
			
		||||
        await page.goto(notebook.url);
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Inspect Notebook Entry Network Requests', async ({ page }) => {
 | 
			
		||||
        await page.getByText('Annotations').click();
 | 
			
		||||
        // Expand sidebar
 | 
			
		||||
        await page.locator('.c-notebook__toggle-nav-button').click();
 | 
			
		||||
 | 
			
		||||
@@ -162,20 +163,20 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
 | 
			
		||||
        await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').press('Enter');
 | 
			
		||||
 | 
			
		||||
        // Add three tags
 | 
			
		||||
        await page.hover(`button:has-text("Add Tag") >> nth=2`);
 | 
			
		||||
        await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
 | 
			
		||||
        await page.hover(`button:has-text("Add Tag")`);
 | 
			
		||||
        await page.locator(`button:has-text("Add Tag")`).click();
 | 
			
		||||
        await page.locator('[placeholder="Type to select tag"]').click();
 | 
			
		||||
        await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
 | 
			
		||||
        await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
 | 
			
		||||
 | 
			
		||||
        await page.hover(`button:has-text("Add Tag") >> nth=2`);
 | 
			
		||||
        await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
 | 
			
		||||
        await page.hover(`button:has-text("Add Tag")`);
 | 
			
		||||
        await page.locator(`button:has-text("Add Tag")`).click();
 | 
			
		||||
        await page.locator('[placeholder="Type to select tag"]').click();
 | 
			
		||||
        await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
 | 
			
		||||
        await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
 | 
			
		||||
 | 
			
		||||
        await page.hover(`button:has-text("Add Tag") >> nth=2`);
 | 
			
		||||
        await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
 | 
			
		||||
        await page.hover(`button:has-text("Add Tag")`);
 | 
			
		||||
        await page.locator(`button:has-text("Add Tag")`).click();
 | 
			
		||||
        await page.locator('[placeholder="Type to select tag"]').click();
 | 
			
		||||
        await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
 | 
			
		||||
        await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
 | 
			
		||||
@@ -231,6 +232,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
 | 
			
		||||
            type: 'issue',
 | 
			
		||||
            description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
 | 
			
		||||
        });
 | 
			
		||||
        await page.getByText('Annotations').click();
 | 
			
		||||
        await page.locator('text=To start a new entry, click here or drag and drop any object').click();
 | 
			
		||||
        await page.locator('[aria-label="Notebook Entry Input"]').click();
 | 
			
		||||
        await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
 | 
			
		||||
 
 | 
			
		||||
@@ -198,7 +198,9 @@ test.describe('Tagging in Notebooks @addInit', () => {
 | 
			
		||||
            page.click('.c-disclosure-triangle')
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        const treePane = page.locator('#tree-pane');
 | 
			
		||||
        const treePane = page.getByRole('tree', {
 | 
			
		||||
            name: 'Main Tree'
 | 
			
		||||
        });
 | 
			
		||||
        // Click Clock
 | 
			
		||||
        await treePane.getByRole('treeitem', {
 | 
			
		||||
            name: clock.name
 | 
			
		||||
 
 | 
			
		||||
@@ -160,35 +160,16 @@ async function testRegularTicks(page) {
 | 
			
		||||
 */
 | 
			
		||||
async function testLogTicks(page) {
 | 
			
		||||
    const yTicks = await page.locator('.gl-plot-y-tick-label');
 | 
			
		||||
    expect(await yTicks.count()).toBe(28);
 | 
			
		||||
    expect(await yTicks.count()).toBe(9);
 | 
			
		||||
    await expect(yTicks.nth(0)).toHaveText('-2.98');
 | 
			
		||||
    await expect(yTicks.nth(1)).toHaveText('-2.50');
 | 
			
		||||
    await expect(yTicks.nth(2)).toHaveText('-2.00');
 | 
			
		||||
    await expect(yTicks.nth(3)).toHaveText('-1.51');
 | 
			
		||||
    await expect(yTicks.nth(4)).toHaveText('-1.20');
 | 
			
		||||
    await expect(yTicks.nth(5)).toHaveText('-1.00');
 | 
			
		||||
    await expect(yTicks.nth(6)).toHaveText('-0.80');
 | 
			
		||||
    await expect(yTicks.nth(7)).toHaveText('-0.58');
 | 
			
		||||
    await expect(yTicks.nth(8)).toHaveText('-0.40');
 | 
			
		||||
    await expect(yTicks.nth(9)).toHaveText('-0.20');
 | 
			
		||||
    await expect(yTicks.nth(10)).toHaveText('-0.00');
 | 
			
		||||
    await expect(yTicks.nth(11)).toHaveText('0.20');
 | 
			
		||||
    await expect(yTicks.nth(12)).toHaveText('0.40');
 | 
			
		||||
    await expect(yTicks.nth(13)).toHaveText('0.58');
 | 
			
		||||
    await expect(yTicks.nth(14)).toHaveText('0.80');
 | 
			
		||||
    await expect(yTicks.nth(15)).toHaveText('1.00');
 | 
			
		||||
    await expect(yTicks.nth(16)).toHaveText('1.20');
 | 
			
		||||
    await expect(yTicks.nth(17)).toHaveText('1.51');
 | 
			
		||||
    await expect(yTicks.nth(18)).toHaveText('2.00');
 | 
			
		||||
    await expect(yTicks.nth(19)).toHaveText('2.50');
 | 
			
		||||
    await expect(yTicks.nth(20)).toHaveText('2.98');
 | 
			
		||||
    await expect(yTicks.nth(21)).toHaveText('3.50');
 | 
			
		||||
    await expect(yTicks.nth(22)).toHaveText('4.00');
 | 
			
		||||
    await expect(yTicks.nth(23)).toHaveText('4.50');
 | 
			
		||||
    await expect(yTicks.nth(24)).toHaveText('5.31');
 | 
			
		||||
    await expect(yTicks.nth(25)).toHaveText('7.00');
 | 
			
		||||
    await expect(yTicks.nth(26)).toHaveText('8.00');
 | 
			
		||||
    await expect(yTicks.nth(27)).toHaveText('9.00');
 | 
			
		||||
    await expect(yTicks.nth(1)).toHaveText('-1.51');
 | 
			
		||||
    await expect(yTicks.nth(2)).toHaveText('-0.58');
 | 
			
		||||
    await expect(yTicks.nth(3)).toHaveText('-0.00');
 | 
			
		||||
    await expect(yTicks.nth(4)).toHaveText('0.58');
 | 
			
		||||
    await expect(yTicks.nth(5)).toHaveText('1.51');
 | 
			
		||||
    await expect(yTicks.nth(6)).toHaveText('2.98');
 | 
			
		||||
    await expect(yTicks.nth(7)).toHaveText('5.31');
 | 
			
		||||
    await expect(yTicks.nth(8)).toHaveText('9.00');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										139
									
								
								e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, 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 log plot functionality. Note this test suite if very much under active development and should not
 | 
			
		||||
necessarily be used for reference when writing new tests in this area.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('../../../../pluginFixtures');
 | 
			
		||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
 | 
			
		||||
 | 
			
		||||
test.describe('Stacked Plot', () => {
 | 
			
		||||
    let stackedPlot;
 | 
			
		||||
    let swgA;
 | 
			
		||||
    let swgB;
 | 
			
		||||
    let swgC;
 | 
			
		||||
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        //Open a browser, navigate to the main page, and wait until all networkevents to resolve
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
        stackedPlot = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Stacked Plot"
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        swgA = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Sine Wave Generator",
 | 
			
		||||
            parent: stackedPlot.uuid
 | 
			
		||||
        });
 | 
			
		||||
        swgB = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Sine Wave Generator",
 | 
			
		||||
            parent: stackedPlot.uuid
 | 
			
		||||
        });
 | 
			
		||||
        swgC = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Sine Wave Generator",
 | 
			
		||||
            parent: stackedPlot.uuid
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Using the remove action removes the correct plot', async ({ page }) => {
 | 
			
		||||
        const swgAElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgA.name });
 | 
			
		||||
        const swgBElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgB.name });
 | 
			
		||||
        const swgCElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgC.name });
 | 
			
		||||
 | 
			
		||||
        await page.goto(stackedPlot.url);
 | 
			
		||||
 | 
			
		||||
        await page.click('button[title="Edit"]');
 | 
			
		||||
 | 
			
		||||
        // Expand the elements pool vertically
 | 
			
		||||
        await page.locator('.l-pane__handle').nth(2).hover({ trial: true });
 | 
			
		||||
        await page.mouse.down();
 | 
			
		||||
        await page.mouse.move(0, 100);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
        await swgBElementsPoolItem.click({ button: 'right' });
 | 
			
		||||
        await page.getByRole('menuitem').filter({ hasText: /Remove/ }).click();
 | 
			
		||||
        await page.getByRole('button').filter({ hasText: "OK" }).click();
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('#inspector-elements-tree .js-elements-pool__item')).toHaveCount(2);
 | 
			
		||||
 | 
			
		||||
        // Confirm that the elements pool contains the items we expect
 | 
			
		||||
        await expect(swgAElementsPoolItem).toHaveCount(1);
 | 
			
		||||
        await expect(swgBElementsPoolItem).toHaveCount(0);
 | 
			
		||||
        await expect(swgCElementsPoolItem).toHaveCount(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({ page }) => {
 | 
			
		||||
        await page.goto(stackedPlot.url);
 | 
			
		||||
 | 
			
		||||
        // Click on the 1st plot
 | 
			
		||||
        await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click();
 | 
			
		||||
 | 
			
		||||
        // Assert that the inspector shows the Y Axis properties for swgA
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
 | 
			
		||||
        await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name);
 | 
			
		||||
 | 
			
		||||
        // Click on the 2nd plot
 | 
			
		||||
        await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"] canvas`).nth(1).click();
 | 
			
		||||
 | 
			
		||||
        // Assert that the inspector shows the Y Axis properties for swgB
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
 | 
			
		||||
        await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name);
 | 
			
		||||
 | 
			
		||||
        // Click on the 3rd plot
 | 
			
		||||
        await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"] canvas`).nth(1).click();
 | 
			
		||||
 | 
			
		||||
        // Assert that the inspector shows the Y Axis properties for swgC
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
 | 
			
		||||
        await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name);
 | 
			
		||||
 | 
			
		||||
        // Go into edit mode
 | 
			
		||||
        await page.click('button[title="Edit"]');
 | 
			
		||||
 | 
			
		||||
        // Click on canvas for the 1st plot
 | 
			
		||||
        await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click();
 | 
			
		||||
 | 
			
		||||
        // Assert that the inspector shows the Y Axis properties for swgA
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
 | 
			
		||||
        await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name);
 | 
			
		||||
 | 
			
		||||
        //Click on canvas for the 2nd plot
 | 
			
		||||
        await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"]`).click();
 | 
			
		||||
 | 
			
		||||
        // Assert that the inspector shows the Y Axis properties for swgB
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
 | 
			
		||||
        await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name);
 | 
			
		||||
 | 
			
		||||
        //Click on canvas for the 3rd plot
 | 
			
		||||
        await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"]`).click();
 | 
			
		||||
 | 
			
		||||
        // Assert that the inspector shows the Y Axis properties for swgC
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
 | 
			
		||||
        await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
 | 
			
		||||
        await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -24,15 +24,22 @@ const { test, expect } = require('../../pluginFixtures.js');
 | 
			
		||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
 | 
			
		||||
 | 
			
		||||
test.describe('Recent Objects', () => {
 | 
			
		||||
    test('Recent Objects CRUD operations', async ({ page }) => {
 | 
			
		||||
    let recentObjectsList;
 | 
			
		||||
    let clock;
 | 
			
		||||
    let folderA;
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        await page.goto('./', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Set Recent Objects List locator for subsequent tests
 | 
			
		||||
        recentObjectsList = page.getByRole('list', {
 | 
			
		||||
            name: 'Recent Objects'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Create a folder and nest a Clock within it
 | 
			
		||||
        const recentObjectsList = page.locator('[aria-label="Recent Objects"]');
 | 
			
		||||
        const folderA = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
        folderA = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Folder'
 | 
			
		||||
        });
 | 
			
		||||
        const clock = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
        clock = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Clock',
 | 
			
		||||
            parent: folderA.uuid
 | 
			
		||||
        });
 | 
			
		||||
@@ -42,7 +49,8 @@ test.describe('Recent Objects', () => {
 | 
			
		||||
        await page.mouse.down();
 | 
			
		||||
        await page.mouse.move(0, 100);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
    test('Recent Objects CRUD operations', async ({ page }) => {
 | 
			
		||||
        // Verify that both created objects appear in the list and are in the correct order
 | 
			
		||||
        expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy();
 | 
			
		||||
        expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
 | 
			
		||||
@@ -52,7 +60,7 @@ test.describe('Recent Objects', () => {
 | 
			
		||||
        expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
        // Navigate to the folder by clicking on the main object name in the recent objects list item
 | 
			
		||||
        await recentObjectsList.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
 | 
			
		||||
        await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
 | 
			
		||||
        await page.waitForURL(`**/${folderA.uuid}?*`);
 | 
			
		||||
        expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
@@ -63,7 +71,11 @@ test.describe('Recent Objects', () => {
 | 
			
		||||
        await page.keyboard.press('Enter');
 | 
			
		||||
 | 
			
		||||
        // Verify rename has been applied in recent objects list item and objects paths
 | 
			
		||||
        expect(page.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
 | 
			
		||||
        expect(await page.getByRole('navigation', {
 | 
			
		||||
            name: `${clock.name} Breadcrumb`
 | 
			
		||||
        }).locator('a').filter({
 | 
			
		||||
            hasText: folderA.name
 | 
			
		||||
        }).count()).toBeGreaterThan(0);
 | 
			
		||||
        expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
        // Delete
 | 
			
		||||
@@ -79,7 +91,42 @@ test.describe('Recent Objects', () => {
 | 
			
		||||
        await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
 | 
			
		||||
        await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme("Clicking on the 'target button' scrolls the object into view in the tree and highlights it");
 | 
			
		||||
    test.fixme("Clicking on an object in the path of a recent object navigates to the object");
 | 
			
		||||
    test.fixme("Tests for context menu actions from recent objects");
 | 
			
		||||
    test("Clicking on an object in the path of a recent object navigates to the object", async ({ page, openmctConfig }) => {
 | 
			
		||||
        const { myItemsFolderName } = openmctConfig;
 | 
			
		||||
        test.info().annotations.push({
 | 
			
		||||
            type: 'issue',
 | 
			
		||||
            description: 'https://github.com/nasa/openmct/issues/6151'
 | 
			
		||||
        });
 | 
			
		||||
        await page.goto('./#/browse/mine');
 | 
			
		||||
 | 
			
		||||
        // Navigate to the folder by clicking on its entry in the Clock's breadcrumb
 | 
			
		||||
        const waitForFolderNavigation = page.waitForURL(`**/${folderA.uuid}?*`);
 | 
			
		||||
        await page.getByRole('navigation', {
 | 
			
		||||
            name: `${clock.name} Breadcrumb`
 | 
			
		||||
        }).locator('a').filter({
 | 
			
		||||
            hasText: folderA.name
 | 
			
		||||
        }).click();
 | 
			
		||||
 | 
			
		||||
        // Verify that the hash URL updates correctly
 | 
			
		||||
        await waitForFolderNavigation;
 | 
			
		||||
        // eslint-disable-next-line no-useless-escape
 | 
			
		||||
        expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}\?.*`));
 | 
			
		||||
 | 
			
		||||
        // Navigate to My Items by clicking on its entry in the Clock's breadcrumb
 | 
			
		||||
        const waitForMyItemsNavigation = page.waitForURL(`**/mine?*`);
 | 
			
		||||
        await page.getByRole('navigation', {
 | 
			
		||||
            name: `${clock.name} Breadcrumb`
 | 
			
		||||
        }).locator('a').filter({
 | 
			
		||||
            hasText: myItemsFolderName
 | 
			
		||||
        }).click();
 | 
			
		||||
 | 
			
		||||
        // Verify that the hash URL updates correctly
 | 
			
		||||
        await waitForMyItemsNavigation;
 | 
			
		||||
        // eslint-disable-next-line no-useless-escape
 | 
			
		||||
        expect(page.url()).toMatch(new RegExp(`.*mine\?.*`));
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme("Clicking on the 'target button' scrolls the object into view in the tree and highlights it", async ({ page }) => {
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme("Tests for context menu actions from recent objects", async ({ page }) => {
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -116,7 +116,9 @@ async function getAndAssertTreeItems(page, expected) {
 | 
			
		||||
 * @param {string} name
 | 
			
		||||
 */
 | 
			
		||||
async function expandTreePaneItemByName(page, name) {
 | 
			
		||||
    const treePane = page.locator('#tree-pane');
 | 
			
		||||
    const treePane = page.getByRole('tree', {
 | 
			
		||||
        name: 'Main Tree'
 | 
			
		||||
    });
 | 
			
		||||
    const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
 | 
			
		||||
    const expandTriangle = treeItem.locator('.c-disclosure-triangle');
 | 
			
		||||
    await expandTriangle.click();
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,7 @@ test.describe('Visual - Tree Pane', () => {
 | 
			
		||||
            name: 'Z Clock'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const treePane = "#tree-pane";
 | 
			
		||||
        const treePane = "[role=tree][aria-label='Main Tree']";
 | 
			
		||||
 | 
			
		||||
        await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, {
 | 
			
		||||
            scope: treePane
 | 
			
		||||
@@ -94,7 +94,7 @@ test.describe('Visual - Tree Pane', () => {
 | 
			
		||||
 * @param {string} name
 | 
			
		||||
 */
 | 
			
		||||
async function expandTreePaneItemByName(page, name) {
 | 
			
		||||
    const treePane = page.locator('#tree-pane');
 | 
			
		||||
    const treePane = page.getByTestId('tree-pane');
 | 
			
		||||
    const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
 | 
			
		||||
    const expandTriangle = treeItem.locator('.c-disclosure-triangle');
 | 
			
		||||
    await expandTriangle.click();
 | 
			
		||||
 
 | 
			
		||||
@@ -23,14 +23,18 @@
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
 | 
			
		||||
export default class SinewaveLimitProvider extends EventEmitter {
 | 
			
		||||
    #openmct;
 | 
			
		||||
    #observingStaleness;
 | 
			
		||||
    #watchingTheClock;
 | 
			
		||||
    #isRealTime;
 | 
			
		||||
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
        this.observingStaleness = {};
 | 
			
		||||
        this.watchingTheClock = false;
 | 
			
		||||
        this.isRealTime = undefined;
 | 
			
		||||
        this.#openmct = openmct;
 | 
			
		||||
        this.#observingStaleness = {};
 | 
			
		||||
        this.#watchingTheClock = false;
 | 
			
		||||
        this.#isRealTime = undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    supportsStaleness(domainObject) {
 | 
			
		||||
@@ -38,114 +42,116 @@ export default class SinewaveLimitProvider extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isStale(domainObject, options) {
 | 
			
		||||
        if (!this.providingStaleness(domainObject)) {
 | 
			
		||||
            return Promise.resolve({
 | 
			
		||||
                isStale: false,
 | 
			
		||||
                utc: 0
 | 
			
		||||
            });
 | 
			
		||||
        if (!this.#providingStaleness(domainObject)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const id = this.getObjectKeyString(domainObject);
 | 
			
		||||
        const id = this.#getObjectKeyString(domainObject);
 | 
			
		||||
 | 
			
		||||
        if (!this.observerExists(id)) {
 | 
			
		||||
            this.createObserver(id);
 | 
			
		||||
        if (!this.#observerExists(id)) {
 | 
			
		||||
            this.#createObserver(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Promise.resolve(this.observingStaleness[id].isStale);
 | 
			
		||||
        return Promise.resolve({
 | 
			
		||||
            isStale: this.#observingStaleness[id].isStale,
 | 
			
		||||
            utc: Date.now()
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    subscribeToStaleness(domainObject, callback) {
 | 
			
		||||
        const id = this.getObjectKeyString(domainObject);
 | 
			
		||||
        const id = this.#getObjectKeyString(domainObject);
 | 
			
		||||
 | 
			
		||||
        if (this.isRealTime === undefined) {
 | 
			
		||||
            this.updateRealTime(this.openmct.time.clock());
 | 
			
		||||
        if (this.#isRealTime === undefined) {
 | 
			
		||||
            this.#updateRealTime(this.#openmct.time.clock());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.handleClockUpdate();
 | 
			
		||||
        this.#handleClockUpdate();
 | 
			
		||||
 | 
			
		||||
        if (this.observerExists(id)) {
 | 
			
		||||
            this.addCallbackToObserver(id, callback);
 | 
			
		||||
        if (this.#observerExists(id)) {
 | 
			
		||||
            this.#addCallbackToObserver(id, callback);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.createObserver(id, callback);
 | 
			
		||||
            this.#createObserver(id, callback);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const intervalId = setInterval(() => {
 | 
			
		||||
            if (this.providingStaleness(domainObject)) {
 | 
			
		||||
                this.updateStaleness(id, !this.observingStaleness[id].isStale);
 | 
			
		||||
            if (this.#providingStaleness(domainObject)) {
 | 
			
		||||
                this.#updateStaleness(id, !this.#observingStaleness[id].isStale);
 | 
			
		||||
            }
 | 
			
		||||
        }, 10000);
 | 
			
		||||
 | 
			
		||||
        return () => {
 | 
			
		||||
            clearInterval(intervalId);
 | 
			
		||||
            this.updateStaleness(id, false);
 | 
			
		||||
            this.handleClockUpdate();
 | 
			
		||||
            this.destroyObserver(id);
 | 
			
		||||
            this.#updateStaleness(id, false);
 | 
			
		||||
            this.#handleClockUpdate();
 | 
			
		||||
            this.#destroyObserver(id);
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleClockUpdate() {
 | 
			
		||||
        let observers = Object.values(this.observingStaleness).length > 0;
 | 
			
		||||
    #handleClockUpdate() {
 | 
			
		||||
        let observers = Object.values(this.#observingStaleness).length > 0;
 | 
			
		||||
 | 
			
		||||
        if (observers && !this.watchingTheClock) {
 | 
			
		||||
            this.watchingTheClock = true;
 | 
			
		||||
            this.openmct.time.on('clock', this.updateRealTime, this);
 | 
			
		||||
        } else if (!observers && this.watchingTheClock) {
 | 
			
		||||
            this.watchingTheClock = false;
 | 
			
		||||
            this.openmct.time.off('clock', this.updateRealTime, this);
 | 
			
		||||
        if (observers && !this.#watchingTheClock) {
 | 
			
		||||
            this.#watchingTheClock = true;
 | 
			
		||||
            this.#openmct.time.on('clock', this.#updateRealTime, this);
 | 
			
		||||
        } else if (!observers && this.#watchingTheClock) {
 | 
			
		||||
            this.#watchingTheClock = false;
 | 
			
		||||
            this.#openmct.time.off('clock', this.#updateRealTime, this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateRealTime(clock) {
 | 
			
		||||
        this.isRealTime = clock !== undefined;
 | 
			
		||||
    #updateRealTime(clock) {
 | 
			
		||||
        this.#isRealTime = clock !== undefined;
 | 
			
		||||
 | 
			
		||||
        if (!this.isRealTime) {
 | 
			
		||||
            Object.keys(this.observingStaleness).forEach((id) => {
 | 
			
		||||
                this.updateStaleness(id, false);
 | 
			
		||||
        if (!this.#isRealTime) {
 | 
			
		||||
            Object.keys(this.#observingStaleness).forEach((id) => {
 | 
			
		||||
                this.#updateStaleness(id, false);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateStaleness(id, isStale) {
 | 
			
		||||
        this.observingStaleness[id].isStale = isStale;
 | 
			
		||||
        this.observingStaleness[id].utc = Date.now();
 | 
			
		||||
        this.observingStaleness[id].callback({
 | 
			
		||||
            isStale: this.observingStaleness[id].isStale,
 | 
			
		||||
            utc: this.observingStaleness[id].utc
 | 
			
		||||
    #updateStaleness(id, isStale) {
 | 
			
		||||
        this.#observingStaleness[id].isStale = isStale;
 | 
			
		||||
        this.#observingStaleness[id].utc = Date.now();
 | 
			
		||||
        this.#observingStaleness[id].callback({
 | 
			
		||||
            isStale: this.#observingStaleness[id].isStale,
 | 
			
		||||
            utc: this.#observingStaleness[id].utc
 | 
			
		||||
        });
 | 
			
		||||
        this.emit('stalenessEvent', {
 | 
			
		||||
            id,
 | 
			
		||||
            isStale: this.observingStaleness[id].isStale
 | 
			
		||||
            isStale: this.#observingStaleness[id].isStale
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    createObserver(id, callback) {
 | 
			
		||||
        this.observingStaleness[id] = {
 | 
			
		||||
    #createObserver(id, callback) {
 | 
			
		||||
        this.#observingStaleness[id] = {
 | 
			
		||||
            isStale: false,
 | 
			
		||||
            utc: Date.now()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (typeof callback === 'function') {
 | 
			
		||||
            this.addCallbackToObserver(id, callback);
 | 
			
		||||
            this.#addCallbackToObserver(id, callback);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroyObserver(id) {
 | 
			
		||||
        delete this.observingStaleness[id];
 | 
			
		||||
    #destroyObserver(id) {
 | 
			
		||||
        if (this.#observingStaleness[id]) {
 | 
			
		||||
            delete this.#observingStaleness[id];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    providingStaleness(domainObject) {
 | 
			
		||||
        return domainObject.telemetry?.staleness === true && this.isRealTime;
 | 
			
		||||
    #providingStaleness(domainObject) {
 | 
			
		||||
        return domainObject.telemetry?.staleness === true && this.#isRealTime;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getObjectKeyString(object) {
 | 
			
		||||
        return this.openmct.objects.makeKeyString(object.identifier);
 | 
			
		||||
    #getObjectKeyString(object) {
 | 
			
		||||
        return this.#openmct.objects.makeKeyString(object.identifier);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addCallbackToObserver(id, callback) {
 | 
			
		||||
        this.observingStaleness[id].callback = callback;
 | 
			
		||||
    #addCallbackToObserver(id, callback) {
 | 
			
		||||
        this.#observingStaleness[id].callback = callback;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    observerExists(id) {
 | 
			
		||||
        return this.observingStaleness?.[id];
 | 
			
		||||
    #observerExists(id) {
 | 
			
		||||
        return this.#observingStaleness?.[id];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,47 +1,35 @@
 | 
			
		||||
import CompositionAPI from './CompositionAPI';
 | 
			
		||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
 | 
			
		||||
import CompositionCollection from './CompositionCollection';
 | 
			
		||||
 | 
			
		||||
describe('The Composition API', function () {
 | 
			
		||||
    let publicAPI;
 | 
			
		||||
    let compositionAPI;
 | 
			
		||||
    let topicService;
 | 
			
		||||
    let mutationTopic;
 | 
			
		||||
 | 
			
		||||
    beforeEach(function () {
 | 
			
		||||
    beforeEach(function (done) {
 | 
			
		||||
        publicAPI = createOpenMct();
 | 
			
		||||
        compositionAPI = publicAPI.composition;
 | 
			
		||||
 | 
			
		||||
        mutationTopic = jasmine.createSpyObj('mutationTopic', [
 | 
			
		||||
            'listen'
 | 
			
		||||
        ]);
 | 
			
		||||
        topicService = jasmine.createSpy('topicService');
 | 
			
		||||
        topicService.and.returnValue(mutationTopic);
 | 
			
		||||
        publicAPI = {};
 | 
			
		||||
        publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
 | 
			
		||||
            'get',
 | 
			
		||||
            'mutate',
 | 
			
		||||
            'observe',
 | 
			
		||||
            'areIdsEqual'
 | 
			
		||||
        const mockObjectProvider = jasmine.createSpyObj("mock provider", [
 | 
			
		||||
            "create",
 | 
			
		||||
            "update",
 | 
			
		||||
            "get"
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
 | 
			
		||||
            return id1.namespace === id2.namespace && id1.key === id2.key;
 | 
			
		||||
        mockObjectProvider.create.and.returnValue(Promise.resolve(true));
 | 
			
		||||
        mockObjectProvider.update.and.returnValue(Promise.resolve(true));
 | 
			
		||||
        mockObjectProvider.get.and.callFake((identifier) => {
 | 
			
		||||
            return Promise.resolve({identifier});
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
 | 
			
		||||
            'checkPolicy'
 | 
			
		||||
        ]);
 | 
			
		||||
        publicAPI.composition.checkPolicy.and.returnValue(true);
 | 
			
		||||
        publicAPI.objects.addProvider('test', mockObjectProvider);
 | 
			
		||||
        publicAPI.objects.addProvider('custom', mockObjectProvider);
 | 
			
		||||
 | 
			
		||||
        publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
 | 
			
		||||
            'on'
 | 
			
		||||
        ]);
 | 
			
		||||
        publicAPI.objects.get.and.callFake(function (identifier) {
 | 
			
		||||
            return Promise.resolve({identifier: identifier});
 | 
			
		||||
        });
 | 
			
		||||
        publicAPI.$injector = jasmine.createSpyObj('$injector', [
 | 
			
		||||
            'get'
 | 
			
		||||
        ]);
 | 
			
		||||
        publicAPI.$injector.get.and.returnValue(topicService);
 | 
			
		||||
        compositionAPI = new CompositionAPI(publicAPI);
 | 
			
		||||
        publicAPI.on('start', done);
 | 
			
		||||
        publicAPI.startHeadless();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        return resetApplicationState(publicAPI);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('returns falsy if an object does not support composition', function () {
 | 
			
		||||
@@ -106,6 +94,9 @@ describe('The Composition API', function () {
 | 
			
		||||
            let listener;
 | 
			
		||||
            beforeEach(function () {
 | 
			
		||||
                listener = jasmine.createSpy('reorderListener');
 | 
			
		||||
                spyOn(publicAPI.objects, 'mutate');
 | 
			
		||||
                publicAPI.objects.mutate.and.callThrough();
 | 
			
		||||
 | 
			
		||||
                composition.on('reorder', listener);
 | 
			
		||||
 | 
			
		||||
                return composition.load();
 | 
			
		||||
@@ -136,18 +127,20 @@ describe('The Composition API', function () {
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        it('supports adding an object to composition', function () {
 | 
			
		||||
            let addListener = jasmine.createSpy('addListener');
 | 
			
		||||
            let mockChildObject = {
 | 
			
		||||
                identifier: {
 | 
			
		||||
                    key: 'mock-key',
 | 
			
		||||
                    namespace: ''
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            composition.on('add', addListener);
 | 
			
		||||
            composition.add(mockChildObject);
 | 
			
		||||
 | 
			
		||||
            expect(domainObject.composition.length).toBe(4);
 | 
			
		||||
            expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
 | 
			
		||||
            return new Promise((resolve) => {
 | 
			
		||||
                composition.on('add', resolve);
 | 
			
		||||
                composition.add(mockChildObject);
 | 
			
		||||
            }).then(() => {
 | 
			
		||||
                expect(domainObject.composition.length).toBe(4);
 | 
			
		||||
                expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -224,7 +224,7 @@ export default class CompositionProvider {
 | 
			
		||||
      * @private
 | 
			
		||||
      * @param {DomainObject} oldDomainObject
 | 
			
		||||
      */
 | 
			
		||||
    #onMutation(oldDomainObject) {
 | 
			
		||||
    #onMutation(newDomainObject, oldDomainObject) {
 | 
			
		||||
        const id = objectUtils.makeKeyString(oldDomainObject.identifier);
 | 
			
		||||
        const listeners = this.#listeningTo[id];
 | 
			
		||||
 | 
			
		||||
@@ -232,8 +232,8 @@ export default class CompositionProvider {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
 | 
			
		||||
        const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
 | 
			
		||||
        const oldComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
 | 
			
		||||
        const newComposition = newDomainObject.composition.map(objectUtils.makeKeyString);
 | 
			
		||||
 | 
			
		||||
        const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
 | 
			
		||||
        const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
 | 
			
		||||
@@ -248,8 +248,6 @@ export default class CompositionProvider {
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        listeners.composition = newComposition.map(objectUtils.parseKeyString);
 | 
			
		||||
 | 
			
		||||
        added.forEach(function (addedChild) {
 | 
			
		||||
            listeners.add.forEach(notify(addedChild));
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -99,8 +99,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
 | 
			
		||||
            objectListeners = this.listeningTo[keyString] = {
 | 
			
		||||
                add: [],
 | 
			
		||||
                remove: [],
 | 
			
		||||
                reorder: [],
 | 
			
		||||
                composition: [].slice.apply(domainObject.composition)
 | 
			
		||||
                reorder: []
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -172,8 +171,9 @@ export default class DefaultCompositionProvider extends CompositionProvider {
 | 
			
		||||
     */
 | 
			
		||||
    add(parent, childId) {
 | 
			
		||||
        if (!this.includes(parent, childId)) {
 | 
			
		||||
            parent.composition.push(childId);
 | 
			
		||||
            this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
 | 
			
		||||
            const composition = structuredClone(parent.composition);
 | 
			
		||||
            composition.push(childId);
 | 
			
		||||
            this.publicAPI.objects.mutate(parent, 'composition', composition);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,6 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<mct-tree
 | 
			
		||||
    id="locator-tree"
 | 
			
		||||
    :is-selector-tree="true"
 | 
			
		||||
    :initial-selection="model.parent"
 | 
			
		||||
    @tree-item-selection="handleItemSelection"
 | 
			
		||||
 
 | 
			
		||||
@@ -75,21 +75,23 @@ class MutableDomainObject {
 | 
			
		||||
        return eventOff;
 | 
			
		||||
    }
 | 
			
		||||
    $set(path, value) {
 | 
			
		||||
        const oldModel = JSON.parse(JSON.stringify(this));
 | 
			
		||||
        const oldValue = _.get(oldModel, path);
 | 
			
		||||
        MutableDomainObject.mutateObject(this, path, value);
 | 
			
		||||
 | 
			
		||||
        //Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
 | 
			
		||||
        this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
 | 
			
		||||
 | 
			
		||||
        //Emit a general "any object" event
 | 
			
		||||
        this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this);
 | 
			
		||||
        this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this, oldModel);
 | 
			
		||||
        //Emit wildcard event, with path so that callback knows what changed
 | 
			
		||||
        this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value);
 | 
			
		||||
        this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value, oldModel, oldValue);
 | 
			
		||||
 | 
			
		||||
        //Emit events specific to properties affected
 | 
			
		||||
        let parentPropertiesList = path.split('.');
 | 
			
		||||
        for (let index = parentPropertiesList.length; index > 0; index--) {
 | 
			
		||||
            let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
 | 
			
		||||
            this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath));
 | 
			
		||||
            this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath), _.get(oldModel, parentPropertyPath));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //TODO: Emit events for listeners of child properties when parent changes.
 | 
			
		||||
 
 | 
			
		||||
@@ -225,24 +225,21 @@ export default class ObjectAPI {
 | 
			
		||||
            throw new Error('Provider does not support get!');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let objectPromise = provider.get(identifier, abortSignal).then(result => {
 | 
			
		||||
        let objectPromise = provider.get(identifier, abortSignal).then(domainObject => {
 | 
			
		||||
            delete this.cache[keystring];
 | 
			
		||||
            domainObject = this.applyGetInterceptors(identifier, domainObject);
 | 
			
		||||
 | 
			
		||||
            result = this.applyGetInterceptors(identifier, result);
 | 
			
		||||
            if (result.isMutable) {
 | 
			
		||||
                result.$refresh(result);
 | 
			
		||||
            } else {
 | 
			
		||||
                let mutableDomainObject = this.toMutable(result);
 | 
			
		||||
                mutableDomainObject.$refresh(result);
 | 
			
		||||
            if (this.supportsMutation(identifier)) {
 | 
			
		||||
                const mutableDomainObject = this.toMutable(domainObject);
 | 
			
		||||
                mutableDomainObject.$refresh(domainObject);
 | 
			
		||||
                this.destroyMutable(mutableDomainObject);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }).catch((result) => {
 | 
			
		||||
            console.warn(`Failed to retrieve ${keystring}:`, result);
 | 
			
		||||
 | 
			
		||||
            return domainObject;
 | 
			
		||||
        }).catch((error) => {
 | 
			
		||||
            console.warn(`Failed to retrieve ${keystring}:`, error);
 | 
			
		||||
            delete this.cache[keystring];
 | 
			
		||||
 | 
			
		||||
            result = this.applyGetInterceptors(identifier);
 | 
			
		||||
            const result = this.applyGetInterceptors(identifier);
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        });
 | 
			
		||||
@@ -648,7 +645,7 @@ export default class ObjectAPI {
 | 
			
		||||
     * @param {module:openmct.DomainObject} object the object to observe
 | 
			
		||||
     * @param {string} path the property to observe
 | 
			
		||||
     * @param {Function} callback a callback to invoke when new values for
 | 
			
		||||
     *        this property are observed
 | 
			
		||||
     *        this property are observed.
 | 
			
		||||
     * @method observe
 | 
			
		||||
     * @memberof module:openmct.ObjectAPI#
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -399,7 +399,7 @@ describe("The Object API", () => {
 | 
			
		||||
                    unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
 | 
			
		||||
                    objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
 | 
			
		||||
                }).then(function () {
 | 
			
		||||
                    expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
 | 
			
		||||
                    expect(mutationCallback).toHaveBeenCalledWith('some-new-value', 'other-attribute-value');
 | 
			
		||||
                    unlisten();
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
@@ -419,14 +419,20 @@ describe("The Object API", () => {
 | 
			
		||||
 | 
			
		||||
                    objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
 | 
			
		||||
                }).then(function () {
 | 
			
		||||
                    expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
 | 
			
		||||
                    expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value', 'embedded-value');
 | 
			
		||||
                    expect(embeddedObjectCallback).toHaveBeenCalledWith({
 | 
			
		||||
                        embeddedKey: 'updated-embedded-value'
 | 
			
		||||
                    }, {
 | 
			
		||||
                        embeddedKey: 'embedded-value'
 | 
			
		||||
                    });
 | 
			
		||||
                    expect(objectAttributeCallback).toHaveBeenCalledWith({
 | 
			
		||||
                        embeddedObject: {
 | 
			
		||||
                            embeddedKey: 'updated-embedded-value'
 | 
			
		||||
                        }
 | 
			
		||||
                    }, {
 | 
			
		||||
                        embeddedObject: {
 | 
			
		||||
                            embeddedKey: 'embedded-value'
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    listeners.forEach(listener => listener());
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="c-overlay">
 | 
			
		||||
<div class="c-overlay js-overlay">
 | 
			
		||||
    <div
 | 
			
		||||
        class="c-overlay__blocker"
 | 
			
		||||
        @click="destroy"
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
                v-for="(button, index) in buttons"
 | 
			
		||||
                ref="buttons"
 | 
			
		||||
                :key="index"
 | 
			
		||||
                class="c-button"
 | 
			
		||||
                class="c-button js-overlay__button"
 | 
			
		||||
                tabindex="0"
 | 
			
		||||
                :class="{'c-button--major': focusIndex===index}"
 | 
			
		||||
                @focus="focusIndex=index"
 | 
			
		||||
 
 | 
			
		||||
@@ -202,16 +202,10 @@ class IndependentTimeContext extends TimeContext {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getUpstreamContext() {
 | 
			
		||||
        const objectKey = this.openmct.objects.makeKeyString(this.objectPath[this.objectPath.length - 1].identifier);
 | 
			
		||||
        const doesObjectHaveTimeContext = this.globalTimeContext.independentContexts.get(objectKey);
 | 
			
		||||
        if (doesObjectHaveTimeContext) {
 | 
			
		||||
            return undefined;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let timeContext = this.globalTimeContext;
 | 
			
		||||
        this.objectPath.some((item, index) => {
 | 
			
		||||
            const key = this.openmct.objects.makeKeyString(item.identifier);
 | 
			
		||||
            //last index is the view object itself
 | 
			
		||||
            //first index is the view object itself
 | 
			
		||||
            const itemContext = this.globalTimeContext.independentContexts.get(key);
 | 
			
		||||
            if (index > 0 && itemContext && itemContext.hasOwnContext()) {
 | 
			
		||||
                //upstream time context
 | 
			
		||||
 
 | 
			
		||||
@@ -93,18 +93,43 @@ describe("The Independent Time API", function () {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("follows a parent time context given the objectPath", () => {
 | 
			
		||||
        let timeContext = api.getContextForView([{
 | 
			
		||||
        api.getContextForView([{
 | 
			
		||||
            identifier: {
 | 
			
		||||
                namespace: '',
 | 
			
		||||
                key: 'blah'
 | 
			
		||||
            }
 | 
			
		||||
        }, {
 | 
			
		||||
        }]);
 | 
			
		||||
        let destroyTimeContext = api.addIndependentContext('blah', independentBounds);
 | 
			
		||||
        let timeContext = api.getContextForView([{
 | 
			
		||||
            identifier: {
 | 
			
		||||
                namespace: '',
 | 
			
		||||
                key: domainObjectKey
 | 
			
		||||
            }
 | 
			
		||||
        }, {
 | 
			
		||||
            identifier: {
 | 
			
		||||
                namespace: '',
 | 
			
		||||
                key: 'blah'
 | 
			
		||||
            }
 | 
			
		||||
        }]);
 | 
			
		||||
        let destroyTimeContext = api.addIndependentContext('blah', independentBounds);
 | 
			
		||||
        expect(timeContext.bounds()).toEqual(independentBounds);
 | 
			
		||||
        destroyTimeContext();
 | 
			
		||||
        expect(timeContext.bounds()).toEqual(bounds);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("uses an object's independent time context if the parent doesn't have one", () => {
 | 
			
		||||
        let timeContext = api.getContextForView([{
 | 
			
		||||
            identifier: {
 | 
			
		||||
                namespace: '',
 | 
			
		||||
                key: domainObjectKey
 | 
			
		||||
            }
 | 
			
		||||
        }, {
 | 
			
		||||
            identifier: {
 | 
			
		||||
                namespace: '',
 | 
			
		||||
                key: 'blah'
 | 
			
		||||
            }
 | 
			
		||||
        }]);
 | 
			
		||||
        expect(timeContext.bounds()).toEqual(bounds);
 | 
			
		||||
        let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
 | 
			
		||||
        expect(timeContext.bounds()).toEqual(independentBounds);
 | 
			
		||||
        destroyTimeContext();
 | 
			
		||||
        expect(timeContext.bounds()).toEqual(bounds);
 | 
			
		||||
 
 | 
			
		||||
@@ -48,11 +48,11 @@
 | 
			
		||||
                </tr>
 | 
			
		||||
                <lad-row
 | 
			
		||||
                    v-for="ladRow in ladTelemetryObjects[ladTable.key]"
 | 
			
		||||
                    :key="ladRow.key"
 | 
			
		||||
                    :key="combineKeys(ladTable.key, ladRow.key)"
 | 
			
		||||
                    :domain-object="ladRow.domainObject"
 | 
			
		||||
                    :path-to-table="ladTable.objectPath"
 | 
			
		||||
                    :has-units="hasUnits"
 | 
			
		||||
                    :is-stale="staleObjects.includes(ladRow.key)"
 | 
			
		||||
                    :is-stale="staleObjects.includes(combineKeys(ladTable.key, ladRow.key))"
 | 
			
		||||
                    @rowContextClick="updateViewContext"
 | 
			
		||||
                />
 | 
			
		||||
            </template>
 | 
			
		||||
@@ -160,10 +160,18 @@ export default {
 | 
			
		||||
                removeCallback
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        combineKeys(ladKey, telemetryObjectKey) {
 | 
			
		||||
            return `${ladKey}-${telemetryObjectKey}`;
 | 
			
		||||
        },
 | 
			
		||||
        removeLadTable(identifier) {
 | 
			
		||||
            let index = this.ladTableObjects.findIndex(ladTable => this.openmct.objects.makeKeyString(identifier) === ladTable.key);
 | 
			
		||||
            let ladTable = this.ladTableObjects[index];
 | 
			
		||||
 | 
			
		||||
            this.ladTelemetryObjects[ladTable.key].forEach(telemetryObject => {
 | 
			
		||||
                let combinedKey = this.combineKeys(ladTable.key, telemetryObject.key);
 | 
			
		||||
                this.unwatchStaleness(combinedKey);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.$delete(this.ladTelemetryObjects, ladTable.key);
 | 
			
		||||
            this.ladTableObjects.splice(index, 1);
 | 
			
		||||
        },
 | 
			
		||||
@@ -178,59 +186,58 @@ export default {
 | 
			
		||||
                let telemetryObject = {};
 | 
			
		||||
                telemetryObject.key = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
                telemetryObject.domainObject = domainObject;
 | 
			
		||||
                const combinedKey = this.combineKeys(ladTable.key, telemetryObject.key);
 | 
			
		||||
 | 
			
		||||
                let telemetryObjects = this.ladTelemetryObjects[ladTable.key];
 | 
			
		||||
                const telemetryObjects = this.ladTelemetryObjects[ladTable.key];
 | 
			
		||||
                telemetryObjects.push(telemetryObject);
 | 
			
		||||
 | 
			
		||||
                this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
 | 
			
		||||
 | 
			
		||||
                // if tracking already, possibly in another table, return
 | 
			
		||||
                if (this.stalenessSubscription[telemetryObject.key]) {
 | 
			
		||||
                    return;
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.stalenessSubscription[telemetryObject.key] = {};
 | 
			
		||||
                    this.stalenessSubscription[telemetryObject.key].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
 | 
			
		||||
                }
 | 
			
		||||
                this.stalenessSubscription[combinedKey] = {};
 | 
			
		||||
                this.stalenessSubscription[combinedKey].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
 | 
			
		||||
 | 
			
		||||
                this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
 | 
			
		||||
                    if (stalenessResponse !== undefined) {
 | 
			
		||||
                        this.handleStaleness(telemetryObject.key, stalenessResponse);
 | 
			
		||||
                        this.handleStaleness(combinedKey, stalenessResponse);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
 | 
			
		||||
                    this.handleStaleness(telemetryObject.key, stalenessResponse);
 | 
			
		||||
                    this.handleStaleness(combinedKey, stalenessResponse);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                this.stalenessSubscription[telemetryObject.key].unsubscribe = stalenessSubscription;
 | 
			
		||||
                this.stalenessSubscription[combinedKey].unsubscribe = stalenessSubscription;
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        removeTelemetryObject(ladTable) {
 | 
			
		||||
            return (identifier) => {
 | 
			
		||||
                const SKIP_CHECK = true;
 | 
			
		||||
                const keystring = this.openmct.objects.makeKeyString(identifier);
 | 
			
		||||
                let telemetryObjects = this.ladTelemetryObjects[ladTable.key];
 | 
			
		||||
                const telemetryObjects = this.ladTelemetryObjects[ladTable.key];
 | 
			
		||||
                const combinedKey = this.combineKeys(ladTable.key, keystring);
 | 
			
		||||
                let index = telemetryObjects.findIndex(telemetryObject => keystring === telemetryObject.key);
 | 
			
		||||
 | 
			
		||||
                this.unwatchStaleness(combinedKey);
 | 
			
		||||
 | 
			
		||||
                telemetryObjects.splice(index, 1);
 | 
			
		||||
 | 
			
		||||
                this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
 | 
			
		||||
 | 
			
		||||
                this.stalenessSubscription[keystring].unsubscribe();
 | 
			
		||||
                this.stalenessSubscription[keystring].stalenessUtils.destroy();
 | 
			
		||||
                this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        handleStaleness(id, stalenessResponse, skipCheck = false) {
 | 
			
		||||
            if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
 | 
			
		||||
                const index = this.staleObjects.indexOf(id);
 | 
			
		||||
                if (stalenessResponse.isStale) {
 | 
			
		||||
                    if (index === -1) {
 | 
			
		||||
                        this.staleObjects.push(id);
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (index !== -1) {
 | 
			
		||||
                        this.staleObjects.splice(index, 1);
 | 
			
		||||
                    }
 | 
			
		||||
        unwatchStaleness(combinedKey) {
 | 
			
		||||
            const SKIP_CHECK = true;
 | 
			
		||||
 | 
			
		||||
            this.stalenessSubscription[combinedKey].unsubscribe();
 | 
			
		||||
            this.stalenessSubscription[combinedKey].stalenessUtils.destroy();
 | 
			
		||||
            this.handleStaleness(combinedKey, { isStale: false }, SKIP_CHECK);
 | 
			
		||||
 | 
			
		||||
            delete this.stalenessSubscription[combinedKey];
 | 
			
		||||
        },
 | 
			
		||||
        handleStaleness(combinedKey, stalenessResponse, skipCheck = false) {
 | 
			
		||||
            if (skipCheck || this.stalenessSubscription[combinedKey].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
 | 
			
		||||
                const index = this.staleObjects.indexOf(combinedKey);
 | 
			
		||||
                const foundStaleObject = index > -1;
 | 
			
		||||
                if (stalenessResponse.isStale && !foundStaleObject) {
 | 
			
		||||
                    this.staleObjects.push(combinedKey);
 | 
			
		||||
                } else if (!stalenessResponse.isStale && foundStaleObject) {
 | 
			
		||||
                    this.staleObjects.splice(index, 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -232,10 +232,12 @@ export default {
 | 
			
		||||
            this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
 | 
			
		||||
 | 
			
		||||
            this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
 | 
			
		||||
                this.hanldeStaleness(keyString, stalenessResponse);
 | 
			
		||||
                if (stalenessResponse !== undefined) {
 | 
			
		||||
                    this.handleStaleness(keyString, stalenessResponse);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
 | 
			
		||||
                this.hanldeStaleness(keyString, stalenessResponse);
 | 
			
		||||
                this.handleStaleness(keyString, stalenessResponse);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription;
 | 
			
		||||
@@ -259,9 +261,10 @@ export default {
 | 
			
		||||
                    keyString,
 | 
			
		||||
                    isStale: false
 | 
			
		||||
                });
 | 
			
		||||
                delete this.stalenessSubscription[keyString];
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        hanldeStaleness(keyString, stalenessResponse) {
 | 
			
		||||
        handleStaleness(keyString, stalenessResponse) {
 | 
			
		||||
            if (this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
 | 
			
		||||
                this.emitStaleness({
 | 
			
		||||
                    keyString,
 | 
			
		||||
 
 | 
			
		||||
@@ -83,6 +83,11 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
 | 
			
		||||
            if (!this.stalenessSubscription[id]) {
 | 
			
		||||
                this.stalenessSubscription[id] = {};
 | 
			
		||||
                this.stalenessSubscription[id].stalenessUtils = new StalenessUtils(this.openmct, telemetryObject);
 | 
			
		||||
                this.openmct.telemetry.isStale(telemetryObject).then((stalenessResponse) => {
 | 
			
		||||
                    if (stalenessResponse !== undefined) {
 | 
			
		||||
                        this.handleStaleTelemetry(id, stalenessResponse);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                this.stalenessSubscription[id].unsubscribe = this.openmct.telemetry.subscribeToStaleness(
 | 
			
		||||
                    telemetryObject,
 | 
			
		||||
                    (stalenessResponse) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ export default class CreateAction extends PropertiesAction {
 | 
			
		||||
            _.set(this.domainObject, key, value);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const parentDomainObject = parentDomainObjectPath[0];
 | 
			
		||||
        const parentDomainObject = this.openmct.objects.toMutable(parentDomainObjectPath[0]);
 | 
			
		||||
 | 
			
		||||
        this.domainObject.modified = Date.now();
 | 
			
		||||
        this.domainObject.location = this.openmct.objects.makeKeyString(parentDomainObject.identifier);
 | 
			
		||||
@@ -85,6 +85,7 @@ export default class CreateAction extends PropertiesAction {
 | 
			
		||||
            console.error(err);
 | 
			
		||||
            this.openmct.notifications.error(`Error saving objects: ${err}`);
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.openmct.objects.destroyMutable(parentDomainObject);
 | 
			
		||||
            dialog.dismiss();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -142,18 +143,21 @@ export default class CreateAction extends PropertiesAction {
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.domainObject = domainObject;
 | 
			
		||||
        this.domainObject = this.openmct.objects.toMutable(domainObject);
 | 
			
		||||
 | 
			
		||||
        if (definition.initialize) {
 | 
			
		||||
            definition.initialize(domainObject);
 | 
			
		||||
            definition.initialize(this.domainObject);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const createWizard = new CreateWizard(this.openmct, domainObject, this.parentDomainObject);
 | 
			
		||||
        const createWizard = new CreateWizard(this.openmct, this.domainObject, this.parentDomainObject);
 | 
			
		||||
        const formStructure = createWizard.getFormStructure(true);
 | 
			
		||||
        formStructure.title = 'Create a New ' + definition.name;
 | 
			
		||||
 | 
			
		||||
        this.openmct.forms.showForm(formStructure)
 | 
			
		||||
            .then(this._onSave.bind(this))
 | 
			
		||||
            .catch(this._onCancel.bind(this));
 | 
			
		||||
            .catch(this._onCancel.bind(this))
 | 
			
		||||
            .finally(() => {
 | 
			
		||||
                this.openmct.objects.destroyMutable(this.domainObject);
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -107,48 +107,53 @@
 | 
			
		||||
                        height="100"
 | 
			
		||||
                    />
 | 
			
		||||
                </mask>
 | 
			
		||||
 | 
			
		||||
                <!-- Equipment (spacecraft) body holder. Transforms relative to the camera position. -->
 | 
			
		||||
                <g
 | 
			
		||||
                    v-if="hasHeading"
 | 
			
		||||
                    class="cr-vrover"
 | 
			
		||||
                    :style="camAngleAndPositionStyle"
 | 
			
		||||
                >
 | 
			
		||||
                    <!-- Equipment body. Rotates relative to the camera gimbal value for cams that gimbal. -->
 | 
			
		||||
                    <path
 | 
			
		||||
                        class="cr-vrover__body"
 | 
			
		||||
                        :style="camGimbalAngleStyle"
 | 
			
		||||
                        fill-rule="evenodd"
 | 
			
		||||
                        clip-rule="evenodd"
 | 
			
		||||
                        d="M5 0C2.23858 0 0 2.23858 0 5V95C0 97.7614 2.23858 100 5 100H95C97.7614 100 100 97.7614 100 95V5C100 2.23858 97.7614 0 95 0H5ZM85 59L50 24L15 59H33V75H67.0455V59H85Z"
 | 
			
		||||
                    />
 | 
			
		||||
                </g>
 | 
			
		||||
 | 
			
		||||
                <g
 | 
			
		||||
                    class="c-cr__cam-fov"
 | 
			
		||||
                    class="c-cr-cam-and-body"
 | 
			
		||||
                    :style="cameraHeadingStyle"
 | 
			
		||||
                >
 | 
			
		||||
                    <g mask="url(#mask2)">
 | 
			
		||||
                        <rect
 | 
			
		||||
                            class="c-cr__cam-fov-r"
 | 
			
		||||
                            x="49"
 | 
			
		||||
                            width="51"
 | 
			
		||||
                            height="100"
 | 
			
		||||
                            :style="cameraFOVStyleRightHalf"
 | 
			
		||||
                    <!-- Equipment (spacecraft) body holder. Transforms relative to the camera position. -->
 | 
			
		||||
                    <g
 | 
			
		||||
                        v-if="hasHeading"
 | 
			
		||||
                        class="cr-vrover"
 | 
			
		||||
                        :style="camAngleAndPositionStyle"
 | 
			
		||||
                    >
 | 
			
		||||
                        <!-- Equipment body. Rotates relative to the camera pan value for cams that gimbal. -->
 | 
			
		||||
                        <path
 | 
			
		||||
                            class="cr-vrover__body"
 | 
			
		||||
                            :style="camGimbalAngleStyle"
 | 
			
		||||
                            x
 | 
			
		||||
                            fill-rule="evenodd"
 | 
			
		||||
                            clip-rule="evenodd"
 | 
			
		||||
                            d="M5 0C2.23858 0 0 2.23858 0 5V95C0 97.7614 2.23858 100 5 100H95C97.7614 100 100 97.7614 100 95V5C100 2.23858 97.7614 0 95 0H5ZM85 59L50 24L15 59H33V75H67.0455V59H85Z"
 | 
			
		||||
                        />
 | 
			
		||||
                    </g>
 | 
			
		||||
                    <g mask="url(#mask1)">
 | 
			
		||||
                        <rect
 | 
			
		||||
                            class="c-cr__cam-fov-l"
 | 
			
		||||
                            width="51"
 | 
			
		||||
                            height="100"
 | 
			
		||||
                            :style="cameraFOVStyleLeftHalf"
 | 
			
		||||
 | 
			
		||||
                    <g
 | 
			
		||||
                        class="c-cr__cam-fov"
 | 
			
		||||
                    >
 | 
			
		||||
                        <g mask="url(#mask2)">
 | 
			
		||||
                            <rect
 | 
			
		||||
                                class="c-cr__cam-fov-r"
 | 
			
		||||
                                x="49"
 | 
			
		||||
                                width="51"
 | 
			
		||||
                                height="100"
 | 
			
		||||
                                :style="cameraFOVStyleRightHalf"
 | 
			
		||||
                            />
 | 
			
		||||
                        </g>
 | 
			
		||||
                        <g mask="url(#mask1)">
 | 
			
		||||
                            <rect
 | 
			
		||||
                                class="c-cr__cam-fov-l"
 | 
			
		||||
                                width="51"
 | 
			
		||||
                                height="100"
 | 
			
		||||
                                :style="cameraFOVStyleLeftHalf"
 | 
			
		||||
                            />
 | 
			
		||||
                        </g>
 | 
			
		||||
                        <polygon
 | 
			
		||||
                            class="c-cr__cam"
 | 
			
		||||
                            points="0,0 100,0 70,40 70,100 30,100 30,40"
 | 
			
		||||
                        />
 | 
			
		||||
                    </g>
 | 
			
		||||
                    <polygon
 | 
			
		||||
                        class="c-cr__cam"
 | 
			
		||||
                        points="0,0 100,0 70,40 70,100 30,100 30,40"
 | 
			
		||||
                    />
 | 
			
		||||
 | 
			
		||||
                </g>
 | 
			
		||||
            </g>
 | 
			
		||||
 | 
			
		||||
@@ -305,7 +310,7 @@ export default {
 | 
			
		||||
            return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` };
 | 
			
		||||
        },
 | 
			
		||||
        camGimbalAngleStyle() {
 | 
			
		||||
            const rotation = rotate(this.north, this.heading);
 | 
			
		||||
            const rotation = rotate(this.heading);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                transform: `rotate(${ rotation }deg)`
 | 
			
		||||
@@ -332,14 +337,6 @@ export default {
 | 
			
		||||
        hasHeading() {
 | 
			
		||||
            return this.heading !== undefined;
 | 
			
		||||
        },
 | 
			
		||||
        headingStyle() {
 | 
			
		||||
            /* Replaced with computed camGimbalStyle, but left here just in case. */
 | 
			
		||||
            const rotation = rotate(this.north, this.heading);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                transform: `rotate(${ rotation }deg)`
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        hasSunHeading() {
 | 
			
		||||
            return this.sunHeading !== undefined;
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -84,6 +84,8 @@ export default class MoveAction {
 | 
			
		||||
        this.addToNewParent(this.object, parent);
 | 
			
		||||
        this.removeFromOldParent(this.object);
 | 
			
		||||
 | 
			
		||||
        await this.saveTransaction();
 | 
			
		||||
 | 
			
		||||
        if (!inNavigationPath) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -102,8 +104,6 @@ export default class MoveAction {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.saveTransaction();
 | 
			
		||||
 | 
			
		||||
        this.navigateTo(newObjectPath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -381,7 +381,7 @@ export default {
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        updateSelection(selection) {
 | 
			
		||||
            if (selection?.[0]?.[1]?.context?.targetDetails?.entryId === undefined) {
 | 
			
		||||
            if (selection?.[0]?.[0]?.context?.targetDetails?.entryId === undefined) {
 | 
			
		||||
                this.selectedEntryId = '';
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -77,13 +77,13 @@
 | 
			
		||||
                    aria-label="Notebook Entry Input"
 | 
			
		||||
                    tabindex="0"
 | 
			
		||||
                    :contenteditable="canEdit"
 | 
			
		||||
                    v-bind.prop="formattedText"
 | 
			
		||||
                    @mouseover="checkEditability($event)"
 | 
			
		||||
                    @mouseleave="canEdit = true"
 | 
			
		||||
                    @focus="editingEntry()"
 | 
			
		||||
                    @blur="updateEntryValue($event)"
 | 
			
		||||
                    @keydown.enter.exact.prevent
 | 
			
		||||
                    @keyup.enter.exact.prevent="forceBlur($event)"
 | 
			
		||||
                    v-html="formattedText"
 | 
			
		||||
                >
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
@@ -99,7 +99,7 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <div>
 | 
			
		||||
            <div class="c-ne__tags c-tag-holder">
 | 
			
		||||
                <div
 | 
			
		||||
                    v-for="(tag, index) in entryTags"
 | 
			
		||||
                    :key="index"
 | 
			
		||||
@@ -250,7 +250,7 @@ export default {
 | 
			
		||||
            let text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA);
 | 
			
		||||
 | 
			
		||||
            if (this.editMode || !this.urlWhitelist) {
 | 
			
		||||
                return text;
 | 
			
		||||
                return { innerText: text };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            text = text.replace(URL_REGEX, (match) => {
 | 
			
		||||
@@ -268,7 +268,7 @@ export default {
 | 
			
		||||
                return result;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return text;
 | 
			
		||||
            return { innerHTML: text };
 | 
			
		||||
        },
 | 
			
		||||
        isSelectedEntry() {
 | 
			
		||||
            return this.selectedEntryId === this.entry.id;
 | 
			
		||||
@@ -456,7 +456,7 @@ export default {
 | 
			
		||||
            this.editMode = false;
 | 
			
		||||
            const value = $event.target.innerText;
 | 
			
		||||
            if (value !== this.entry.text && value.match(/\S/)) {
 | 
			
		||||
                this.entry.text = value;
 | 
			
		||||
                this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA);
 | 
			
		||||
                this.timestampAndUpdate();
 | 
			
		||||
            } else {
 | 
			
		||||
                this.$emit('cancelEdit');
 | 
			
		||||
@@ -472,16 +472,11 @@ export default {
 | 
			
		||||
            targetDomainObjects[keyString] = this.domainObject;
 | 
			
		||||
            this.openmct.selection.select(
 | 
			
		||||
                [
 | 
			
		||||
                    {
 | 
			
		||||
                        element: this.openmct.layout.$refs.browseObject.$el,
 | 
			
		||||
                        context: {
 | 
			
		||||
                            item: this.domainObject
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        element: event.currentTarget,
 | 
			
		||||
                        context: {
 | 
			
		||||
                            type: 'notebook-entry-selection',
 | 
			
		||||
                            item: this.domainObject,
 | 
			
		||||
                            targetDetails,
 | 
			
		||||
                            targetDomainObjects,
 | 
			
		||||
                            annotations: this.notebookAnnotations,
 | 
			
		||||
 
 | 
			
		||||
@@ -105,10 +105,6 @@ function installBaseNotebookFunctionality(openmct) {
 | 
			
		||||
 | 
			
		||||
function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {
 | 
			
		||||
    return function install(openmct) {
 | 
			
		||||
        if (openmct[NOTEBOOK_INSTALLED_KEY]) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const icon = 'icon-notebook';
 | 
			
		||||
        const description = 'Create and save timestamped notes with embedded object snapshots.';
 | 
			
		||||
        const snapshotContainer = getSnapshotContainer(openmct);
 | 
			
		||||
@@ -122,8 +118,6 @@ function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {
 | 
			
		||||
        openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);
 | 
			
		||||
 | 
			
		||||
        installBaseNotebookFunctionality(openmct);
 | 
			
		||||
 | 
			
		||||
        openmct[NOTEBOOK_INSTALLED_KEY] = true;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,16 +23,8 @@
 | 
			
		||||
<div
 | 
			
		||||
    v-if="loaded"
 | 
			
		||||
    class="gl-plot"
 | 
			
		||||
    :class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
 | 
			
		||||
>
 | 
			
		||||
    <plot-legend
 | 
			
		||||
        v-if="!isNestedWithinAStackedPlot"
 | 
			
		||||
        :cursor-locked="!!lockHighlightPoint"
 | 
			
		||||
        :series="seriesModels"
 | 
			
		||||
        :highlights="highlights"
 | 
			
		||||
        :legend="legend"
 | 
			
		||||
        @legendHoverChanged="legendHoverChanged"
 | 
			
		||||
    />
 | 
			
		||||
    <slot></slot>
 | 
			
		||||
    <div class="plot-wrapper-axis-and-display-area flex-elem grows">
 | 
			
		||||
        <div
 | 
			
		||||
            v-if="seriesModels.length"
 | 
			
		||||
@@ -42,13 +34,14 @@
 | 
			
		||||
                v-for="(yAxis, index) in yAxesIds"
 | 
			
		||||
                :id="yAxis.id"
 | 
			
		||||
                :key="`yAxis-${yAxis.id}-${index}`"
 | 
			
		||||
                :multiple-left-axes="multipleLeftAxes"
 | 
			
		||||
                :has-multiple-left-axes="hasMultipleLeftAxes"
 | 
			
		||||
                :position="yAxis.id > 2 ? 'right' : 'left'"
 | 
			
		||||
                :class="{'plot-yaxis-right': yAxis.id > 2}"
 | 
			
		||||
                :tick-width="yAxis.tickWidth"
 | 
			
		||||
                :used-tick-width="plotFirstLeftTickWidth"
 | 
			
		||||
                :plot-left-tick-width="yAxis.id > 2 ? yAxis.tickWidth: plotLeftTickWidth"
 | 
			
		||||
                @yKeyChanged="setYAxisKey"
 | 
			
		||||
                @tickWidthChanged="onTickWidthChange"
 | 
			
		||||
                @plotYTickWidth="onYTickWidthChange"
 | 
			
		||||
                @toggleAxisVisibility="toggleSeriesForYAxis"
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -69,7 +62,6 @@
 | 
			
		||||
                    v-show="gridLines && !options.compact"
 | 
			
		||||
                    :axis-type="'xAxis'"
 | 
			
		||||
                    :position="'right'"
 | 
			
		||||
                    @plotTickWidth="onTickWidthChange"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <mct-ticks
 | 
			
		||||
@@ -79,7 +71,7 @@
 | 
			
		||||
                    :axis-type="'yAxis'"
 | 
			
		||||
                    :position="'bottom'"
 | 
			
		||||
                    :axis-id="yAxis.id"
 | 
			
		||||
                    @plotTickWidth="onTickWidthChange"
 | 
			
		||||
                    @plotTickWidth="onYTickWidthChange"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <div
 | 
			
		||||
@@ -94,7 +86,6 @@
 | 
			
		||||
                        :highlights="highlights"
 | 
			
		||||
                        :annotated-points="annotatedPoints"
 | 
			
		||||
                        :annotation-selections="annotationSelections"
 | 
			
		||||
                        :show-limit-line-labels="showLimitLineLabels"
 | 
			
		||||
                        :hidden-y-axis-ids="hiddenYAxisIds"
 | 
			
		||||
                        :annotation-viewing-and-editing-allowed="annotationViewingAndEditingAllowed"
 | 
			
		||||
                        @plotReinitializeCanvas="initCanvas"
 | 
			
		||||
@@ -217,7 +208,6 @@ import LinearScale from "./LinearScale";
 | 
			
		||||
import PlotConfigurationModel from './configuration/PlotConfigurationModel';
 | 
			
		||||
import configStore from './configuration/ConfigStore';
 | 
			
		||||
 | 
			
		||||
import PlotLegend from "./legend/PlotLegend.vue";
 | 
			
		||||
import MctTicks from "./MctTicks.vue";
 | 
			
		||||
import MctChart from "./chart/MctChart.vue";
 | 
			
		||||
import XAxis from "./axis/XAxis.vue";
 | 
			
		||||
@@ -232,7 +222,6 @@ export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        XAxis,
 | 
			
		||||
        YAxis,
 | 
			
		||||
        PlotLegend,
 | 
			
		||||
        MctTicks,
 | 
			
		||||
        MctChart
 | 
			
		||||
    },
 | 
			
		||||
@@ -258,10 +247,14 @@ export default {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        plotTickWidth: {
 | 
			
		||||
            type: Number,
 | 
			
		||||
        parentYTickWidth: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            default() {
 | 
			
		||||
                return 0;
 | 
			
		||||
                return {
 | 
			
		||||
                    leftTickWidth: 0,
 | 
			
		||||
                    rightTickWidth: 0,
 | 
			
		||||
                    hasMultipleLeftAxes: false
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        limitLineLabels: {
 | 
			
		||||
@@ -296,7 +289,6 @@ export default {
 | 
			
		||||
            isRealTime: this.openmct.time.clock() !== undefined,
 | 
			
		||||
            loaded: false,
 | 
			
		||||
            isTimeOutOfSync: false,
 | 
			
		||||
            showLimitLineLabels: this.limitLineLabels,
 | 
			
		||||
            isFrozenOnMouseDown: false,
 | 
			
		||||
            cursorGuide: this.initCursorGuide,
 | 
			
		||||
            gridLines: this.initGridLines,
 | 
			
		||||
@@ -308,13 +300,14 @@ export default {
 | 
			
		||||
    computed: {
 | 
			
		||||
        xAxisStyle() {
 | 
			
		||||
            const rightAxis = this.yAxesIds.find(yAxis => yAxis.id > 2);
 | 
			
		||||
            const leftOffset = this.multipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING;
 | 
			
		||||
            const leftOffset = this.hasMultipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING;
 | 
			
		||||
            let style = {
 | 
			
		||||
                left: `${this.plotLeftTickWidth + leftOffset}px`
 | 
			
		||||
            };
 | 
			
		||||
            const parentRightAxisWidth = this.parentYTickWidth.rightTickWidth;
 | 
			
		||||
 | 
			
		||||
            if (rightAxis) {
 | 
			
		||||
                style.right = `${rightAxis.tickWidth + AXES_PADDING}px`;
 | 
			
		||||
            if (parentRightAxisWidth || rightAxis) {
 | 
			
		||||
                style.right = `${(parentRightAxisWidth || rightAxis.tickWidth) + AXES_PADDING}px`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return style;
 | 
			
		||||
@@ -322,8 +315,8 @@ export default {
 | 
			
		||||
        yAxesIds() {
 | 
			
		||||
            return this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
 | 
			
		||||
        },
 | 
			
		||||
        multipleLeftAxes() {
 | 
			
		||||
            return this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1;
 | 
			
		||||
        hasMultipleLeftAxes() {
 | 
			
		||||
            return this.parentYTickWidth.hasMultipleLeftAxes || this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1;
 | 
			
		||||
        },
 | 
			
		||||
        isNestedWithinAStackedPlot() {
 | 
			
		||||
            const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path));
 | 
			
		||||
@@ -334,22 +327,13 @@ export default {
 | 
			
		||||
            return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
 | 
			
		||||
        },
 | 
			
		||||
        annotationViewingAndEditingAllowed() {
 | 
			
		||||
            // only allow annotations viewing/editing if plot is paused or in fixed time mode
 | 
			
		||||
        // only allow annotations viewing/editing if plot is paused or in fixed time mode
 | 
			
		||||
            return this.isFrozen || !this.isRealTime;
 | 
			
		||||
        },
 | 
			
		||||
        plotLegendPositionClass() {
 | 
			
		||||
            return !this.isNestedWithinAStackedPlot ? `plot-legend-${this.config.legend.get('position')}` : '';
 | 
			
		||||
        },
 | 
			
		||||
        plotLegendExpandedStateClass() {
 | 
			
		||||
            if (this.isNestedWithinAStackedPlot) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
        plotFirstLeftTickWidth() {
 | 
			
		||||
            const firstYAxis = this.yAxes.find(yAxis => yAxis.id === 1);
 | 
			
		||||
 | 
			
		||||
            if (this.config.legend.get('expanded')) {
 | 
			
		||||
                return 'plot-legend-expanded';
 | 
			
		||||
            } else {
 | 
			
		||||
                return 'plot-legend-collapsed';
 | 
			
		||||
            }
 | 
			
		||||
            return firstYAxis ? firstYAxis.tickWidth : 0;
 | 
			
		||||
        },
 | 
			
		||||
        plotLeftTickWidth() {
 | 
			
		||||
            let leftTickWidth = 0;
 | 
			
		||||
@@ -360,17 +344,12 @@ export default {
 | 
			
		||||
 | 
			
		||||
                leftTickWidth = leftTickWidth + yAxis.tickWidth;
 | 
			
		||||
            });
 | 
			
		||||
            const parentLeftTickWidth = this.parentYTickWidth.leftTickWidth;
 | 
			
		||||
 | 
			
		||||
            return this.plotTickWidth || leftTickWidth;
 | 
			
		||||
            return parentLeftTickWidth || leftTickWidth;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
        limitLineLabels: {
 | 
			
		||||
            handler(limitLineLabels) {
 | 
			
		||||
                this.legendHoverChanged(limitLineLabels);
 | 
			
		||||
            },
 | 
			
		||||
            deep: true
 | 
			
		||||
        },
 | 
			
		||||
        initGridLines(newGridLines) {
 | 
			
		||||
            this.gridLines = newGridLines;
 | 
			
		||||
        },
 | 
			
		||||
@@ -406,8 +385,7 @@ export default {
 | 
			
		||||
            }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
        this.$emit('configLoaded', configId);
 | 
			
		||||
        this.$emit('configLoaded', true);
 | 
			
		||||
 | 
			
		||||
        this.listenTo(this.config.series, 'add', this.addSeries, this);
 | 
			
		||||
        this.listenTo(this.config.series, 'remove', this.removeSeries, this);
 | 
			
		||||
@@ -439,15 +417,20 @@ export default {
 | 
			
		||||
    methods: {
 | 
			
		||||
        updateSelection(selection) {
 | 
			
		||||
            const selectionContext = selection?.[0]?.[0]?.context?.item;
 | 
			
		||||
            if (!selectionContext
 | 
			
		||||
                || this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)) {
 | 
			
		||||
                // Selection changed, but it's us, so ignoring it
 | 
			
		||||
            // on clicking on a search result we highlight the annotation and zoom - we know it's an annotation result when isAnnotationSearchResult === true
 | 
			
		||||
            // We shouldn't zoom when we're selecting existing annotations to view them or creating new annotations.
 | 
			
		||||
            const selectionType = selection?.[0]?.[0]?.context?.type;
 | 
			
		||||
            const validSelectionTypes = ['clicked-on-plot-selection', 'plot-annotation-search-result'];
 | 
			
		||||
            const isAnnotationSearchResult = selectionType === 'plot-annotation-search-result';
 | 
			
		||||
 | 
			
		||||
            if (!validSelectionTypes.includes(selectionType)) {
 | 
			
		||||
                // wrong type of selection
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const selectionType = selection?.[0]?.[1]?.context?.type;
 | 
			
		||||
            if (selectionType !== 'plot-points-selection') {
 | 
			
		||||
                // wrong type of selection
 | 
			
		||||
            if (selectionContext
 | 
			
		||||
                && (!isAnnotationSearchResult)
 | 
			
		||||
                && this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -460,7 +443,18 @@ export default {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const selectedAnnotations = selection?.[0]?.[1]?.context?.annotations;
 | 
			
		||||
            const selectedAnnotations = selection?.[0]?.[0]?.context?.annotations;
 | 
			
		||||
            //This section is only for the annotations search results entry to displaying annotations
 | 
			
		||||
            if (isAnnotationSearchResult) {
 | 
			
		||||
                this.showAnnotationsFromSearchResults(selectedAnnotations);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //This section is common to all entry points for annotation display
 | 
			
		||||
            this.prepareExistingAnnotationSelection(selectedAnnotations);
 | 
			
		||||
        },
 | 
			
		||||
        showAnnotationsFromSearchResults(selectedAnnotations) {
 | 
			
		||||
        //Start section
 | 
			
		||||
 | 
			
		||||
            if (selectedAnnotations?.length) {
 | 
			
		||||
                // just use first annotation
 | 
			
		||||
                const boundingBoxes = Object.values(selectedAnnotations[0].targets);
 | 
			
		||||
@@ -494,10 +488,9 @@ export default {
 | 
			
		||||
                    min: minY,
 | 
			
		||||
                    max: maxY
 | 
			
		||||
                });
 | 
			
		||||
                //Zoom out just a touch so that the highlighted section for annotations doesn't take over the whole view - which is not a nice look.
 | 
			
		||||
                this.zoom('out', 0.2);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.prepareExistingAnnotationSelection(selectedAnnotations);
 | 
			
		||||
        },
 | 
			
		||||
        handleKeyDown(event) {
 | 
			
		||||
            if (event.key === 'Alt') {
 | 
			
		||||
@@ -575,6 +568,14 @@ export default {
 | 
			
		||||
        updateTicksAndSeriesForYAxis(newAxisId, oldAxisId) {
 | 
			
		||||
            this.updateAxisUsageCount(oldAxisId, -1);
 | 
			
		||||
            this.updateAxisUsageCount(newAxisId, 1);
 | 
			
		||||
 | 
			
		||||
            const foundYAxis = this.yAxes.find(yAxis => yAxis.id === oldAxisId);
 | 
			
		||||
            if (foundYAxis.seriesCount === 0) {
 | 
			
		||||
                this.onYTickWidthChange({
 | 
			
		||||
                    width: foundYAxis.tickWidth,
 | 
			
		||||
                    yAxisId: foundYAxis.id
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        updateAxisUsageCount(yAxisId, updateCountBy) {
 | 
			
		||||
@@ -688,9 +689,15 @@ export default {
 | 
			
		||||
                series.reset();
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        shareCommonParent(domainObjectToFind) {
 | 
			
		||||
            return false;
 | 
			
		||||
        },
 | 
			
		||||
        compositionPathContainsId(domainObjectToFind) {
 | 
			
		||||
            if (!domainObjectToFind.composition) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        compositionPathContainsId(domainObjectToClear) {
 | 
			
		||||
            return domainObjectToClear.composition.some((compositionIdentifier) => {
 | 
			
		||||
            return domainObjectToFind.composition.some((compositionIdentifier) => {
 | 
			
		||||
                return this.openmct.objects.areIdsEqual(compositionIdentifier, this.domainObject.identifier);
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
@@ -820,21 +827,29 @@ export default {
 | 
			
		||||
 | 
			
		||||
        marqueeAnnotations(annotationsToSelect) {
 | 
			
		||||
            annotationsToSelect.forEach(annotationToSelect => {
 | 
			
		||||
                const firstTargetKeyString = Object.keys(annotationToSelect.targets)[0];
 | 
			
		||||
                const firstTarget = annotationToSelect.targets[firstTargetKeyString];
 | 
			
		||||
                const rectangle = {
 | 
			
		||||
                    start: {
 | 
			
		||||
                        x: firstTarget.minX,
 | 
			
		||||
                        y: firstTarget.minY
 | 
			
		||||
                    },
 | 
			
		||||
                    end: {
 | 
			
		||||
                        x: firstTarget.maxX,
 | 
			
		||||
                        y: firstTarget.maxY
 | 
			
		||||
                    },
 | 
			
		||||
                    color: [1, 1, 1, 0.10]
 | 
			
		||||
                };
 | 
			
		||||
                this.rectangles.push(rectangle);
 | 
			
		||||
                Object.keys(annotationToSelect.targets).forEach(targetKeyString => {
 | 
			
		||||
                    const target = annotationToSelect.targets[targetKeyString];
 | 
			
		||||
                    const series = this.seriesModels.find(seriesModel => seriesModel.keyString === targetKeyString);
 | 
			
		||||
                    if (!series) {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    const yAxisId = series.get('yAxisId');
 | 
			
		||||
                    const rectangle = {
 | 
			
		||||
                        start: {
 | 
			
		||||
                            x: target.minX,
 | 
			
		||||
                            y: [target.minY],
 | 
			
		||||
                            yAxisIds: [yAxisId]
 | 
			
		||||
                        },
 | 
			
		||||
                        end: {
 | 
			
		||||
                            x: target.maxX,
 | 
			
		||||
                            y: [target.maxY],
 | 
			
		||||
                            yAxisIds: [yAxisId]
 | 
			
		||||
                        },
 | 
			
		||||
                        color: [1, 1, 1, 0.10]
 | 
			
		||||
                    };
 | 
			
		||||
                    this.rectangles.push(rectangle);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        gatherNearbyAnnotations() {
 | 
			
		||||
@@ -938,8 +953,13 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onTickWidthChange(data, fromDifferentObject) {
 | 
			
		||||
            const {width, yAxisId} = data;
 | 
			
		||||
        /**
 | 
			
		||||
       * Aggregate widths of all left and right y axes and send them up to any parent plots
 | 
			
		||||
       * @param {Object} tickWidthWithYAxisId - the width and yAxisId of the tick bar
 | 
			
		||||
       * @param fromDifferentObject
 | 
			
		||||
       */
 | 
			
		||||
        onYTickWidthChange(tickWidthWithYAxisId, fromDifferentObject) {
 | 
			
		||||
            const {width, yAxisId} = tickWidthWithYAxisId;
 | 
			
		||||
            if (yAxisId) {
 | 
			
		||||
                const index = this.yAxes.findIndex(yAxis => yAxis.id === yAxisId);
 | 
			
		||||
                if (fromDifferentObject) {
 | 
			
		||||
@@ -948,13 +968,23 @@ export default {
 | 
			
		||||
                } else {
 | 
			
		||||
                // Otherwise, only accept tick with if it's larger.
 | 
			
		||||
                    const newWidth = Math.max(width, this.yAxes[index].tickWidth);
 | 
			
		||||
                    if (newWidth !== this.yAxes[index].tickWidth) {
 | 
			
		||||
                    if (width !== this.yAxes[index].tickWidth) {
 | 
			
		||||
                        this.yAxes[index].tickWidth = newWidth;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
                this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id);
 | 
			
		||||
                const leftTickWidth = this.yAxes.filter(yAxis => yAxis.id < 3).reduce((previous, current) => {
 | 
			
		||||
                    return previous + current.tickWidth;
 | 
			
		||||
                }, 0);
 | 
			
		||||
                const rightTickWidth = this.yAxes.filter(yAxis => yAxis.id > 2).reduce((previous, current) => {
 | 
			
		||||
                    return previous + current.tickWidth;
 | 
			
		||||
                }, 0);
 | 
			
		||||
                this.$emit('plotYTickWidth', {
 | 
			
		||||
                    hasMultipleLeftAxes: this.hasMultipleLeftAxes,
 | 
			
		||||
                    leftTickWidth,
 | 
			
		||||
                    rightTickWidth
 | 
			
		||||
                }, id);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -1036,8 +1066,6 @@ export default {
 | 
			
		||||
 | 
			
		||||
        highlightValues(point) {
 | 
			
		||||
            this.highlightPoint = point;
 | 
			
		||||
            // TODO: used in StackedPlotController
 | 
			
		||||
            this.$emit('plotHighlightUpdate', point);
 | 
			
		||||
            if (this.lockHighlightPoint) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
@@ -1149,7 +1177,7 @@ export default {
 | 
			
		||||
                    endPixels: this.positionOverElement,
 | 
			
		||||
                    start: this.positionOverPlot,
 | 
			
		||||
                    end: this.positionOverPlot,
 | 
			
		||||
                    color: [1, 1, 1, 0.5]
 | 
			
		||||
                    color: [1, 1, 1, 0.25]
 | 
			
		||||
                };
 | 
			
		||||
                if (annotationEvent) {
 | 
			
		||||
                    this.marquee.annotationEvent = true;
 | 
			
		||||
@@ -1160,57 +1188,92 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        selectNearbyAnnotations(event) {
 | 
			
		||||
            // need to stop propagation right away to prevent selecting the plot itself
 | 
			
		||||
            event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
            if (!this.annotationViewingAndEditingAllowed || this.annotationSelections.length) {
 | 
			
		||||
            const nearbyAnnotations = this.gatherNearbyAnnotations();
 | 
			
		||||
 | 
			
		||||
            if (this.annotationViewingAndEditingAllowed && this.annotationSelections.length) {
 | 
			
		||||
                //no annotations were found, but we are adding some now
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const nearbyAnnotations = this.gatherNearbyAnnotations();
 | 
			
		||||
            const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations);
 | 
			
		||||
            this.selectPlotAnnotations({
 | 
			
		||||
                targetDetails,
 | 
			
		||||
                targetDomainObjects,
 | 
			
		||||
                annotations: nearbyAnnotations
 | 
			
		||||
            });
 | 
			
		||||
            if (this.annotationViewingAndEditingAllowed && nearbyAnnotations.length) {
 | 
			
		||||
                //show annotations if some were found
 | 
			
		||||
                const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations);
 | 
			
		||||
                this.selectPlotAnnotations({
 | 
			
		||||
                    targetDetails,
 | 
			
		||||
                    targetDomainObjects,
 | 
			
		||||
                    annotations: nearbyAnnotations
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Fall through to here if either there is no new selection add tags or no existing annotations were retrieved
 | 
			
		||||
            this.selectPlot();
 | 
			
		||||
        },
 | 
			
		||||
        selectPlotAnnotations({targetDetails, targetDomainObjects, annotations}) {
 | 
			
		||||
            const selection =
 | 
			
		||||
                    [
 | 
			
		||||
                        {
 | 
			
		||||
                            element: this.openmct.layout.$refs.browseObject.$el,
 | 
			
		||||
                            context: {
 | 
			
		||||
                                item: this.domainObject
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            element: this.$el,
 | 
			
		||||
                            context: {
 | 
			
		||||
                                type: 'plot-points-selection',
 | 
			
		||||
                                targetDetails,
 | 
			
		||||
                                targetDomainObjects,
 | 
			
		||||
                                annotations,
 | 
			
		||||
                                annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
 | 
			
		||||
                                onAnnotationChange: this.onAnnotationChange
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    ];
 | 
			
		||||
        selectPlot() {
 | 
			
		||||
            // should show plot itself if we didn't find any annotations
 | 
			
		||||
            const selection = this.createPathSelection();
 | 
			
		||||
            this.openmct.selection.select(selection, true);
 | 
			
		||||
        },
 | 
			
		||||
        selectNewPlotAnnotations(minX, minY, maxX, maxY, pointsInBox, event) {
 | 
			
		||||
            const boundingBox = {
 | 
			
		||||
                minX,
 | 
			
		||||
                minY,
 | 
			
		||||
                maxX,
 | 
			
		||||
                maxY
 | 
			
		||||
        createPathSelection() {
 | 
			
		||||
            let selection = [];
 | 
			
		||||
            selection.unshift({
 | 
			
		||||
                element: this.$el,
 | 
			
		||||
                context: {
 | 
			
		||||
                    item: this.domainObject
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            this.path.forEach((pathObject, index) => {
 | 
			
		||||
                selection.push({
 | 
			
		||||
                    element: this.openmct.layout.$refs.browseObject.$el,
 | 
			
		||||
                    context: {
 | 
			
		||||
                        item: pathObject
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return selection;
 | 
			
		||||
        },
 | 
			
		||||
        selectPlotAnnotations({targetDetails, targetDomainObjects, annotations}) {
 | 
			
		||||
            const annotationContext = {
 | 
			
		||||
                type: 'clicked-on-plot-selection',
 | 
			
		||||
                targetDetails,
 | 
			
		||||
                targetDomainObjects,
 | 
			
		||||
                annotations,
 | 
			
		||||
                annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
 | 
			
		||||
                onAnnotationChange: this.onAnnotationChange
 | 
			
		||||
            };
 | 
			
		||||
            const selection = this.createPathSelection();
 | 
			
		||||
            if (selection.length && this.openmct.objects.areIdsEqual(selection[0].context.item.identifier, this.domainObject.identifier)) {
 | 
			
		||||
                selection[0].context = {
 | 
			
		||||
                    ...selection[0].context,
 | 
			
		||||
                    ...annotationContext
 | 
			
		||||
                };
 | 
			
		||||
            } else {
 | 
			
		||||
                selection.unshift({
 | 
			
		||||
                    element: this.$el,
 | 
			
		||||
                    context: {
 | 
			
		||||
                        item: this.domainObject,
 | 
			
		||||
                        ...annotationContext
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.openmct.selection.select(selection, true);
 | 
			
		||||
        },
 | 
			
		||||
        selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBox, event) {
 | 
			
		||||
            let targetDomainObjects = {};
 | 
			
		||||
            let targetDetails = {};
 | 
			
		||||
            let annotations = {};
 | 
			
		||||
            let annotations = [];
 | 
			
		||||
            pointsInBox.forEach(pointInBox => {
 | 
			
		||||
                if (pointInBox.length) {
 | 
			
		||||
                    const seriesID = pointInBox[0].series.keyString;
 | 
			
		||||
                    targetDetails[seriesID] = boundingBox;
 | 
			
		||||
                    const boundingBoxWithId = boundingBoxPerYAxis.find(box => box.id === pointInBox[0].series.get('yAxisId'));
 | 
			
		||||
                    targetDetails[seriesID] = boundingBoxWithId?.boundingBox;
 | 
			
		||||
 | 
			
		||||
                    targetDomainObjects[seriesID] = pointInBox[0].series.domainObject;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
@@ -1225,10 +1288,23 @@ export default {
 | 
			
		||||
            rawAnnotations.forEach(rawAnnotation => {
 | 
			
		||||
                if (rawAnnotation.targets) {
 | 
			
		||||
                    const targetValues = Object.values(rawAnnotation.targets);
 | 
			
		||||
                    const targetKeys = Object.keys(rawAnnotation.targets);
 | 
			
		||||
                    if (targetValues && targetValues.length) {
 | 
			
		||||
                        // just get the first one
 | 
			
		||||
                        const boundingBox = Object.values(targetValues)?.[0];
 | 
			
		||||
                        const pointsInBox = this.getPointsInBox(boundingBox, rawAnnotation);
 | 
			
		||||
                        let boundingBoxPerYAxis = [];
 | 
			
		||||
                        targetValues.forEach((boundingBox, index) => {
 | 
			
		||||
                            const seriesId = targetKeys[index];
 | 
			
		||||
                            const series = this.seriesModels.find(seriesModel => seriesModel.keyString === seriesId);
 | 
			
		||||
                            if (!series) {
 | 
			
		||||
                                return;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            boundingBoxPerYAxis.push({
 | 
			
		||||
                                id: series.get('yAxisId'),
 | 
			
		||||
                                boundingBox
 | 
			
		||||
                            });
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        const pointsInBox = this.getPointsInBox(boundingBoxPerYAxis, rawAnnotation);
 | 
			
		||||
                        if (pointsInBox && pointsInBox.length) {
 | 
			
		||||
                            annotationsByPoints.push(pointsInBox.flat());
 | 
			
		||||
                        }
 | 
			
		||||
@@ -1238,10 +1314,17 @@ export default {
 | 
			
		||||
 | 
			
		||||
            return annotationsByPoints.flat();
 | 
			
		||||
        },
 | 
			
		||||
        getPointsInBox(boundingBox, rawAnnotation) {
 | 
			
		||||
        getPointsInBox(boundingBoxPerYAxis, rawAnnotation) {
 | 
			
		||||
            // load series models in KD-Trees
 | 
			
		||||
            const seriesKDTrees = [];
 | 
			
		||||
            this.seriesModels.forEach(seriesModel => {
 | 
			
		||||
                const boundingBoxWithId = boundingBoxPerYAxis.find(box => box.id === seriesModel.get('yAxisId'));
 | 
			
		||||
                const boundingBox = boundingBoxWithId?.boundingBox;
 | 
			
		||||
                //Series was probably added after the last annotations were saved
 | 
			
		||||
                if (!boundingBox) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const seriesData = seriesModel.getSeriesData();
 | 
			
		||||
                if (seriesData && seriesData.length) {
 | 
			
		||||
                    const kdTree = new KDBush(seriesData,
 | 
			
		||||
@@ -1283,25 +1366,31 @@ export default {
 | 
			
		||||
            return seriesKDTrees;
 | 
			
		||||
        },
 | 
			
		||||
        endAnnotationMarquee(event) {
 | 
			
		||||
            const minX = Math.min(this.marquee.start.x, this.marquee.end.x);
 | 
			
		||||
            const startMinY = this.marquee.start.y.reduce((previousY, currentY) => {
 | 
			
		||||
                return Math.min(previousY, currentY);
 | 
			
		||||
            }, this.marquee.start.y[0]);
 | 
			
		||||
            const endMinY = this.marquee.end.y.reduce((previousY, currentY) => {
 | 
			
		||||
                return Math.min(previousY, currentY);
 | 
			
		||||
            }, this.marquee.end.y[0]);
 | 
			
		||||
            const minY = Math.min(startMinY, endMinY);
 | 
			
		||||
            const maxX = Math.max(this.marquee.start.x, this.marquee.end.x);
 | 
			
		||||
            const maxY = Math.max(startMinY, endMinY);
 | 
			
		||||
            const boundingBox = {
 | 
			
		||||
                minX,
 | 
			
		||||
                minY,
 | 
			
		||||
                maxX,
 | 
			
		||||
                maxY
 | 
			
		||||
            };
 | 
			
		||||
            const pointsInBox = this.getPointsInBox(boundingBox);
 | 
			
		||||
            const boundingBoxPerYAxis = [];
 | 
			
		||||
            this.yAxisListWithRange.forEach((yAxis, yIndex) => {
 | 
			
		||||
                const minX = Math.min(this.marquee.start.x, this.marquee.end.x);
 | 
			
		||||
                const minY = Math.min(this.marquee.start.y[yIndex], this.marquee.end.y[yIndex]);
 | 
			
		||||
                const maxX = Math.max(this.marquee.start.x, this.marquee.end.x);
 | 
			
		||||
                const maxY = Math.max(this.marquee.start.y[yIndex], this.marquee.end.y[yIndex]);
 | 
			
		||||
                const boundingBox = {
 | 
			
		||||
                    minX,
 | 
			
		||||
                    minY,
 | 
			
		||||
                    maxX,
 | 
			
		||||
                    maxY
 | 
			
		||||
                };
 | 
			
		||||
                boundingBoxPerYAxis.push({
 | 
			
		||||
                    id: yAxis.get('id'),
 | 
			
		||||
                    boundingBox
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const pointsInBox = this.getPointsInBox(boundingBoxPerYAxis);
 | 
			
		||||
            if (!pointsInBox) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.annotationSelections = pointsInBox.flat();
 | 
			
		||||
            this.selectNewPlotAnnotations(minX, minY, maxX, maxY, pointsInBox, event);
 | 
			
		||||
            this.selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBox, event);
 | 
			
		||||
        },
 | 
			
		||||
        endZoomMarquee() {
 | 
			
		||||
            const startPixels = this.marquee.startPixels;
 | 
			
		||||
@@ -1681,7 +1770,9 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        destroy() {
 | 
			
		||||
            configStore.deleteStore(this.config.id);
 | 
			
		||||
            if (this.config) {
 | 
			
		||||
                configStore.deleteStore(this.config.id);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.stopListening();
 | 
			
		||||
 | 
			
		||||
@@ -1722,9 +1813,6 @@ export default {
 | 
			
		||||
                this.config.series.models.forEach(this.loadSeriesData, this);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        legendHoverChanged(data) {
 | 
			
		||||
            this.showLimitLineLabels = data;
 | 
			
		||||
        },
 | 
			
		||||
        toggleCursorGuide() {
 | 
			
		||||
            this.cursorGuide = !this.cursorGuide;
 | 
			
		||||
            this.$emit('cursorGuide', this.cursorGuide);
 | 
			
		||||
 
 | 
			
		||||
@@ -86,6 +86,8 @@ import eventHelpers from "./lib/eventHelpers";
 | 
			
		||||
import { ticks, getLogTicks, getFormattedTicks } from "./tickUtils";
 | 
			
		||||
import configStore from "./configuration/ConfigStore";
 | 
			
		||||
 | 
			
		||||
const SECONDARY_TICK_NUMBER = 2;
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct', 'domainObject'],
 | 
			
		||||
    props: {
 | 
			
		||||
@@ -205,7 +207,7 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.axisType === 'yAxis' && this.axis.get('logMode')) {
 | 
			
		||||
                return getLogTicks(range.min, range.max, number, 4);
 | 
			
		||||
                return getLogTicks(range.min, range.max, number, SECONDARY_TICK_NUMBER);
 | 
			
		||||
            } else {
 | 
			
		||||
                return ticks(range.min, range.max, number);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -36,12 +36,26 @@
 | 
			
		||||
            :model="{progressPerc: undefined}"
 | 
			
		||||
        />
 | 
			
		||||
        <mct-plot
 | 
			
		||||
            :class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
 | 
			
		||||
            :init-grid-lines="gridLines"
 | 
			
		||||
            :init-cursor-guide="cursorGuide"
 | 
			
		||||
            :options="options"
 | 
			
		||||
            :limit-line-labels="limitLineLabels"
 | 
			
		||||
            @loadingUpdated="loadingUpdated"
 | 
			
		||||
            @statusUpdated="setStatus"
 | 
			
		||||
        />
 | 
			
		||||
            @configLoaded="updateReady"
 | 
			
		||||
            @lockHighlightPoint="lockHighlightPointUpdated"
 | 
			
		||||
            @highlights="highlightsUpdated"
 | 
			
		||||
        >
 | 
			
		||||
            <plot-legend
 | 
			
		||||
                v-if="configReady"
 | 
			
		||||
                :cursor-locked="lockHighlightPoint"
 | 
			
		||||
                :highlights="highlights"
 | 
			
		||||
                @legendHoverChanged="legendHoverChanged"
 | 
			
		||||
                @expanded="updateExpanded"
 | 
			
		||||
                @position="updatePosition"
 | 
			
		||||
            />
 | 
			
		||||
        </mct-plot>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -50,13 +64,15 @@
 | 
			
		||||
import eventHelpers from './lib/eventHelpers';
 | 
			
		||||
import ImageExporter from '../../exporters/ImageExporter';
 | 
			
		||||
import MctPlot from './MctPlot.vue';
 | 
			
		||||
import PlotLegend from "./legend/PlotLegend.vue";
 | 
			
		||||
import ProgressBar from "../../ui/components/ProgressBar.vue";
 | 
			
		||||
import StalenessUtils from '@/utils/staleness';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        MctPlot,
 | 
			
		||||
        ProgressBar
 | 
			
		||||
        ProgressBar,
 | 
			
		||||
        PlotLegend
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct', 'domainObject', 'path'],
 | 
			
		||||
    props: {
 | 
			
		||||
@@ -77,7 +93,13 @@ export default {
 | 
			
		||||
            gridLines: !this.options.compact,
 | 
			
		||||
            loading: false,
 | 
			
		||||
            status: '',
 | 
			
		||||
            staleObjects: []
 | 
			
		||||
            staleObjects: [],
 | 
			
		||||
            limitLineLabels: undefined,
 | 
			
		||||
            lockHighlightPoint: false,
 | 
			
		||||
            highlights: [],
 | 
			
		||||
            expanded: false,
 | 
			
		||||
            position: undefined,
 | 
			
		||||
            configReady: false
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
@@ -87,6 +109,16 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return '';
 | 
			
		||||
        },
 | 
			
		||||
        plotLegendPositionClass() {
 | 
			
		||||
            return this.position ? `plot-legend-${this.position}` : '';
 | 
			
		||||
        },
 | 
			
		||||
        plotLegendExpandedStateClass() {
 | 
			
		||||
            if (this.expanded) {
 | 
			
		||||
                return 'plot-legend-expanded';
 | 
			
		||||
            } else {
 | 
			
		||||
                return 'plot-legend-collapsed';
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
@@ -134,6 +166,7 @@ export default {
 | 
			
		||||
            this.stalenessSubscription[keystring].unsubscribe();
 | 
			
		||||
            this.stalenessSubscription[keystring].stalenessUtils.destroy();
 | 
			
		||||
            this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
 | 
			
		||||
            delete this.stalenessSubscription[keystring];
 | 
			
		||||
        },
 | 
			
		||||
        handleStaleness(id, stalenessResponse, skipCheck = false) {
 | 
			
		||||
            if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse, id)) {
 | 
			
		||||
@@ -183,6 +216,24 @@ export default {
 | 
			
		||||
                exportPNG: this.exportPNG,
 | 
			
		||||
                exportJPG: this.exportJPG
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        lockHighlightPointUpdated(data) {
 | 
			
		||||
            this.lockHighlightPoint = data;
 | 
			
		||||
        },
 | 
			
		||||
        highlightsUpdated(data) {
 | 
			
		||||
            this.highlights = data;
 | 
			
		||||
        },
 | 
			
		||||
        legendHoverChanged(data) {
 | 
			
		||||
            this.limitLineLabels = data;
 | 
			
		||||
        },
 | 
			
		||||
        updateExpanded(expanded) {
 | 
			
		||||
            this.expanded = expanded;
 | 
			
		||||
        },
 | 
			
		||||
        updatePosition(position) {
 | 
			
		||||
            this.position = position;
 | 
			
		||||
        },
 | 
			
		||||
        updateReady(ready) {
 | 
			
		||||
            this.configReady = ready;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -152,7 +152,7 @@ export default {
 | 
			
		||||
            this.selectedXKeyOptionKey = this.xKeyOptions.length > 0 ? this.getXKeyOption(xAxisKey).key : xAxisKey;
 | 
			
		||||
        },
 | 
			
		||||
        onTickWidthChange(width) {
 | 
			
		||||
            this.$emit('tickWidthChanged', width);
 | 
			
		||||
            this.$emit('plotXTickWidth', width);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -101,7 +101,13 @@ export default {
 | 
			
		||||
                return 0;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        multipleLeftAxes: {
 | 
			
		||||
        usedTickWidth: {
 | 
			
		||||
            type: Number,
 | 
			
		||||
            default() {
 | 
			
		||||
                return 0;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        hasMultipleLeftAxes: {
 | 
			
		||||
            type: Boolean,
 | 
			
		||||
            default() {
 | 
			
		||||
                return false;
 | 
			
		||||
@@ -138,14 +144,14 @@ export default {
 | 
			
		||||
            let style = {
 | 
			
		||||
                width: `${this.tickWidth + AXIS_PADDING}px`
 | 
			
		||||
            };
 | 
			
		||||
            const multipleAxesPadding = this.multipleLeftAxes ? AXIS_PADDING : 0;
 | 
			
		||||
            const multipleAxesPadding = this.hasMultipleLeftAxes ? AXIS_PADDING : 0;
 | 
			
		||||
 | 
			
		||||
            if (this.position === 'right') {
 | 
			
		||||
                style.left = `-${this.tickWidth + AXIS_PADDING}px`;
 | 
			
		||||
            } else {
 | 
			
		||||
                const thisIsTheSecondLeftAxis = (this.id - 1) > 0;
 | 
			
		||||
                if (this.multipleLeftAxes && thisIsTheSecondLeftAxis) {
 | 
			
		||||
                    style.left = 0;
 | 
			
		||||
                if (this.hasMultipleLeftAxes && thisIsTheSecondLeftAxis) {
 | 
			
		||||
                    style.left = `${this.plotLeftTickWidth - this.usedTickWidth - this.tickWidth}px`;
 | 
			
		||||
                    style['border-right'] = `1px solid`;
 | 
			
		||||
                } else {
 | 
			
		||||
                    style.left = `${ this.plotLeftTickWidth - this.tickWidth + multipleAxesPadding}px`;
 | 
			
		||||
@@ -202,6 +208,7 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.listenTo(series, 'change:yAxisId', this.addOrRemoveSeries.bind(this, series), this);
 | 
			
		||||
            this.listenTo(series, 'change:color', this.updateSeriesColors.bind(this, series), this);
 | 
			
		||||
        },
 | 
			
		||||
        removeSeries(plotSeries) {
 | 
			
		||||
            const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), plotSeries.get('identifier')));
 | 
			
		||||
@@ -216,6 +223,9 @@ export default {
 | 
			
		||||
                return model.get('yKey') === this.seriesModels[0].get('yKey');
 | 
			
		||||
            });
 | 
			
		||||
            this.singleSeries = this.seriesModels.length === 1;
 | 
			
		||||
            this.updateSeriesColors();
 | 
			
		||||
        },
 | 
			
		||||
        updateSeriesColors() {
 | 
			
		||||
            this.seriesColors = this.seriesModels.map(model => {
 | 
			
		||||
                return model.get('color').asHexString();
 | 
			
		||||
            });
 | 
			
		||||
@@ -252,7 +262,7 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        onTickWidthChange(data) {
 | 
			
		||||
            this.$emit('tickWidthChanged', {
 | 
			
		||||
            this.$emit('plotYTickWidth', {
 | 
			
		||||
                width: data.width,
 | 
			
		||||
                yAxisId: this.id
 | 
			
		||||
            });
 | 
			
		||||
 
 | 
			
		||||
@@ -105,6 +105,9 @@ export default class MCTChartAlarmLineSet {
 | 
			
		||||
 | 
			
		||||
    reset() {
 | 
			
		||||
        this.limits = [];
 | 
			
		||||
        if (this.series.limits) {
 | 
			
		||||
            this.getLimitPoints(this.series);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
 
 | 
			
		||||
@@ -121,6 +121,7 @@ export default {
 | 
			
		||||
        hiddenYAxisIds() {
 | 
			
		||||
            this.hiddenYAxisIds.forEach(id => {
 | 
			
		||||
                this.resetYOffsetAndSeriesDataForYAxis(id);
 | 
			
		||||
                this.drawLimitLines();
 | 
			
		||||
            });
 | 
			
		||||
            this.scheduleDraw();
 | 
			
		||||
        }
 | 
			
		||||
@@ -196,15 +197,26 @@ export default {
 | 
			
		||||
            this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
 | 
			
		||||
            this.listenTo(series, 'change:limitLines', this.changeLimitLines, this);
 | 
			
		||||
            this.listenTo(series, 'change:yAxisId', this.resetAxisAndRedraw, this);
 | 
			
		||||
            // TODO: Which other changes is the listener below reacting to?
 | 
			
		||||
            this.listenTo(series, 'change', this.scheduleDraw);
 | 
			
		||||
            this.listenTo(series, 'add', this.onAddPoint);
 | 
			
		||||
            this.makeChartElement(series);
 | 
			
		||||
            this.makeLimitLines(series);
 | 
			
		||||
        },
 | 
			
		||||
        onAddPoint(point, insertIndex, series) {
 | 
			
		||||
            const mainYAxisId = this.config.yAxis.get('id');
 | 
			
		||||
            const seriesYAxisId = series.get('yAxisId');
 | 
			
		||||
            const xRange = this.config.xAxis.get('displayRange');
 | 
			
		||||
            //TODO: get the yAxis of this series
 | 
			
		||||
            const yRange = this.config.yAxis.get('displayRange');
 | 
			
		||||
 | 
			
		||||
            let yRange;
 | 
			
		||||
            if (seriesYAxisId === mainYAxisId) {
 | 
			
		||||
                yRange = this.config.yAxis.get('displayRange');
 | 
			
		||||
            } else {
 | 
			
		||||
                yRange = this.config.additionalYAxes.find(
 | 
			
		||||
                    yAxis => yAxis.get('id') === seriesYAxisId
 | 
			
		||||
                ).get('displayRange');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const xValue = series.getXVal(point);
 | 
			
		||||
            const yValue = series.getYVal(point);
 | 
			
		||||
 | 
			
		||||
@@ -521,7 +533,6 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
        updateLimitsAndDraw() {
 | 
			
		||||
            this.drawLimitLines();
 | 
			
		||||
            this.scheduleDraw();
 | 
			
		||||
        },
 | 
			
		||||
        scheduleDraw() {
 | 
			
		||||
            if (!this.drawScheduled) {
 | 
			
		||||
@@ -615,9 +626,13 @@ export default {
 | 
			
		||||
            alarmSets.forEach(this.drawAlarmPoints, this);
 | 
			
		||||
        },
 | 
			
		||||
        drawLimitLines() {
 | 
			
		||||
            Array.from(this.$refs.limitArea.children).forEach((el) => el.remove());
 | 
			
		||||
            this.config.series.models.forEach(series => {
 | 
			
		||||
                const yAxisId = series.get('yAxisId');
 | 
			
		||||
                this.drawLimitLinesForSeries(yAxisId, series);
 | 
			
		||||
 | 
			
		||||
                if (this.hiddenYAxisIds.indexOf(yAxisId) < 0) {
 | 
			
		||||
                    this.drawLimitLinesForSeries(yAxisId, series);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        drawLimitLinesForSeries(yAxisId, series) {
 | 
			
		||||
@@ -631,12 +646,11 @@ export default {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Array.from(this.$refs.limitArea.children).forEach((el) => el.remove());
 | 
			
		||||
            let limitPointOverlap = [];
 | 
			
		||||
            this.limitLines.forEach((limitLine) => {
 | 
			
		||||
                let limitContainerEl = this.$refs.limitArea;
 | 
			
		||||
                limitLine.limits.forEach((limit) => {
 | 
			
		||||
                    if (!series.includes(limit.seriesKey)) {
 | 
			
		||||
                    if (series.keyString !== limit.seriesKey) {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@@ -744,6 +758,10 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        annotatedPointWithinRange(annotatedPoint, xRange, yRange) {
 | 
			
		||||
            if (!yRange) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const xValue = annotatedPoint.series.getXVal(annotatedPoint.point);
 | 
			
		||||
            const yValue = annotatedPoint.series.getYVal(annotatedPoint.point);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -68,27 +68,26 @@ export default class PlotConfigurationModel extends Model {
 | 
			
		||||
        //Add any axes in addition to the main yAxis above - we must always have at least 1 y-axis
 | 
			
		||||
        //Addition axes ids will be the MAIN_Y_AXES_ID + x where x is between 1 and MAX_ADDITIONAL_AXES
 | 
			
		||||
        this.additionalYAxes = [];
 | 
			
		||||
        if (Array.isArray(options.model.additionalYAxes)) {
 | 
			
		||||
            const maxLength = Math.min(MAX_ADDITIONAL_AXES, options.model.additionalYAxes.length);
 | 
			
		||||
            for (let yAxisCount = 0; yAxisCount < maxLength; yAxisCount++) {
 | 
			
		||||
                const yAxis = options.model.additionalYAxes[yAxisCount];
 | 
			
		||||
        const hasAdditionalAxesConfiguration = Array.isArray(options.model.additionalYAxes);
 | 
			
		||||
 | 
			
		||||
        for (let yAxisCount = 0; yAxisCount < MAX_ADDITIONAL_AXES; yAxisCount++) {
 | 
			
		||||
            const yAxisId = MAIN_Y_AXES_ID + yAxisCount + 1;
 | 
			
		||||
            const yAxis = hasAdditionalAxesConfiguration && options.model.additionalYAxes.find(additionalYAxis => additionalYAxis?.id === yAxisId);
 | 
			
		||||
            if (yAxis) {
 | 
			
		||||
                this.additionalYAxes.push(new YAxisModel({
 | 
			
		||||
                    model: yAxis,
 | 
			
		||||
                    plot: this,
 | 
			
		||||
                    openmct: options.openmct,
 | 
			
		||||
                    id: yAxis.id || (MAIN_Y_AXES_ID + yAxisCount + 1)
 | 
			
		||||
                    id: yAxis.id
 | 
			
		||||
                }));
 | 
			
		||||
            } else {
 | 
			
		||||
                this.additionalYAxes.push(new YAxisModel({
 | 
			
		||||
                    plot: this,
 | 
			
		||||
                    openmct: options.openmct,
 | 
			
		||||
                    id: yAxisId
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If the saved options config doesn't include information about all the additional axes, we initialize the remaining here
 | 
			
		||||
        for (let axesCount = this.additionalYAxes.length; axesCount < MAX_ADDITIONAL_AXES; axesCount++) {
 | 
			
		||||
            this.additionalYAxes.push(new YAxisModel({
 | 
			
		||||
                plot: this,
 | 
			
		||||
                openmct: options.openmct,
 | 
			
		||||
                id: MAIN_Y_AXES_ID + axesCount + 1
 | 
			
		||||
            }));
 | 
			
		||||
        }
 | 
			
		||||
        // end add additional axes
 | 
			
		||||
 | 
			
		||||
        this.legend = new LegendModel({
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ export default class PlotSeries extends Model {
 | 
			
		||||
 | 
			
		||||
        super(options);
 | 
			
		||||
 | 
			
		||||
        this.logMode = options.collection.plot.model.yAxis.logMode;
 | 
			
		||||
        this.logMode = this.getLogMode(options);
 | 
			
		||||
 | 
			
		||||
        this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
 | 
			
		||||
        this.listenTo(this, 'change:yKey', this.onYKeyChange, this);
 | 
			
		||||
@@ -87,6 +87,17 @@ export default class PlotSeries extends Model {
 | 
			
		||||
        this.unPlottableValues = [undefined, Infinity, -Infinity];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getLogMode(options) {
 | 
			
		||||
        const yAxisId = this.get('yAxisId');
 | 
			
		||||
        if (yAxisId === 1) {
 | 
			
		||||
            return options.collection.plot.model.yAxis.logMode;
 | 
			
		||||
        } else {
 | 
			
		||||
            const foundYAxis = options.collection.plot.model.additionalYAxes.find(yAxis => yAxis.id === yAxisId);
 | 
			
		||||
 | 
			
		||||
            return foundYAxis ? foundYAxis.logMode : false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set defaults for telemetry series.
 | 
			
		||||
     * @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
 | 
			
		||||
 
 | 
			
		||||
@@ -56,6 +56,10 @@ export default class SeriesCollection extends Collection {
 | 
			
		||||
            const series = this.byIdentifier(seriesConfig.identifier);
 | 
			
		||||
            if (series) {
 | 
			
		||||
                series.persistedConfig = seriesConfig;
 | 
			
		||||
                if (!series.persistedConfig.yAxisId) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (series.get('yAxisId') !== series.persistedConfig.yAxisId) {
 | 
			
		||||
                    series.set('yAxisId', series.persistedConfig.yAxisId);
 | 
			
		||||
                }
 | 
			
		||||
@@ -63,6 +67,10 @@ export default class SeriesCollection extends Collection {
 | 
			
		||||
        }, this);
 | 
			
		||||
    }
 | 
			
		||||
    watchTelemetryContainer(domainObject) {
 | 
			
		||||
        if (domainObject.type === 'telemetry.plot.stacked') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const composition = this.openmct.composition.get(domainObject);
 | 
			
		||||
        this.listenTo(composition, 'add', this.addTelemetryObject, this);
 | 
			
		||||
        this.listenTo(composition, 'remove', this.removeTelemetryObject, this);
 | 
			
		||||
 
 | 
			
		||||
@@ -287,7 +287,8 @@ export default class YAxisModel extends Model {
 | 
			
		||||
        this.resetSeries();
 | 
			
		||||
    }
 | 
			
		||||
    resetSeries() {
 | 
			
		||||
        this.plot.series.forEach((plotSeries) => {
 | 
			
		||||
        const series = this.getSeriesForYAxis(this.seriesCollection);
 | 
			
		||||
        series.forEach((plotSeries) => {
 | 
			
		||||
            plotSeries.logMode = this.get('logMode');
 | 
			
		||||
            plotSeries.reset(plotSeries.getSeriesData());
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@
 | 
			
		||||
    <ul
 | 
			
		||||
        v-if="!isStackedPlotObject"
 | 
			
		||||
        class="c-tree"
 | 
			
		||||
        aria-label="Plot Series Properties"
 | 
			
		||||
    >
 | 
			
		||||
        <h2 title="Plot series display properties in this object">Plot Series</h2>
 | 
			
		||||
        <plot-options-item
 | 
			
		||||
@@ -43,6 +44,7 @@
 | 
			
		||||
            v-for="(yAxis, index) in yAxesWithSeries"
 | 
			
		||||
            :key="`yAxis-${index}`"
 | 
			
		||||
            class="l-inspector-part js-yaxis-properties"
 | 
			
		||||
            :aria-label="yAxesWithSeries.length > 1 ? `Y Axis ${yAxis.id} Properties` : 'Y Axis Properties'"
 | 
			
		||||
        >
 | 
			
		||||
            <h2 title="Y axis settings for this object">Y Axis {{ yAxesWithSeries.length > 1 ? yAxis.id : '' }}</h2>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
@@ -93,7 +95,7 @@
 | 
			
		||||
        </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="plotSeries.length && (isStackedPlotObject || !isNestedWithinAStackedPlot)"
 | 
			
		||||
        v-if="isStackedPlotObject || !isNestedWithinAStackedPlot"
 | 
			
		||||
        class="grid-properties"
 | 
			
		||||
    >
 | 
			
		||||
        <ul
 | 
			
		||||
@@ -190,10 +192,13 @@ export default {
 | 
			
		||||
    mounted() {
 | 
			
		||||
        eventHelpers.extend(this);
 | 
			
		||||
        this.config = this.getConfig();
 | 
			
		||||
        this.initYAxesConfiguration();
 | 
			
		||||
        if (!this.isStackedPlotObject) {
 | 
			
		||||
            this.initYAxesConfiguration();
 | 
			
		||||
            this.registerListeners();
 | 
			
		||||
        } else {
 | 
			
		||||
            this.initLegendConfiguration();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.registerListeners();
 | 
			
		||||
        this.initLegendConfiguration();
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
@@ -245,9 +250,9 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        getConfig() {
 | 
			
		||||
            this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
            const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
            return configStore.get(this.configId);
 | 
			
		||||
            return configStore.get(configId);
 | 
			
		||||
        },
 | 
			
		||||
        registerListeners() {
 | 
			
		||||
            if (this.config) {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@
 | 
			
		||||
    <ul
 | 
			
		||||
        v-if="!isStackedPlotObject"
 | 
			
		||||
        class="c-tree"
 | 
			
		||||
        aria-label="Plot Series Properties"
 | 
			
		||||
    >
 | 
			
		||||
        <h2 title="Display properties for this object">Plot Series</h2>
 | 
			
		||||
        <li
 | 
			
		||||
@@ -53,7 +54,6 @@
 | 
			
		||||
    >
 | 
			
		||||
        <h2 title="Legend options">Legend</h2>
 | 
			
		||||
        <legend-form
 | 
			
		||||
            v-if="plotSeries.length"
 | 
			
		||||
            class="grid-properties"
 | 
			
		||||
            :legend="config.legend"
 | 
			
		||||
        />
 | 
			
		||||
@@ -97,20 +97,23 @@ export default {
 | 
			
		||||
    mounted() {
 | 
			
		||||
        eventHelpers.extend(this);
 | 
			
		||||
        this.config = this.getConfig();
 | 
			
		||||
        this.yAxes = [{
 | 
			
		||||
            id: this.config.yAxis.id,
 | 
			
		||||
            seriesCount: 0
 | 
			
		||||
        }];
 | 
			
		||||
        if (this.config.additionalYAxes) {
 | 
			
		||||
            this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => {
 | 
			
		||||
                return {
 | 
			
		||||
                    id: yAxis.id,
 | 
			
		||||
                    seriesCount: 0
 | 
			
		||||
                };
 | 
			
		||||
            }));
 | 
			
		||||
        if (!this.isStackedPlotObject) {
 | 
			
		||||
            this.yAxes = [{
 | 
			
		||||
                id: this.config.yAxis.id,
 | 
			
		||||
                seriesCount: 0
 | 
			
		||||
            }];
 | 
			
		||||
            if (this.config.additionalYAxes) {
 | 
			
		||||
                this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => {
 | 
			
		||||
                    return {
 | 
			
		||||
                        id: yAxis.id,
 | 
			
		||||
                        seriesCount: 0
 | 
			
		||||
                    };
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.registerListeners();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.registerListeners();
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,11 +12,12 @@ export default function PlotsInspectorViewProvider(openmct) {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let object = selection[0][0].context.item;
 | 
			
		||||
            let parent = selection[0].length > 1 && selection[0][1].context.item;
 | 
			
		||||
 | 
			
		||||
            const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay';
 | 
			
		||||
            const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked';
 | 
			
		||||
            const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked';
 | 
			
		||||
 | 
			
		||||
            return isStackedPlotObject || isOverlayPlotObject;
 | 
			
		||||
            return isOverlayPlotObject || isParentStackedPlotObject;
 | 
			
		||||
        },
 | 
			
		||||
        view: function (selection) {
 | 
			
		||||
            let component;
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,10 @@ export default function StackedPlotsInspectorViewProvider(openmct) {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const object = selection[0][0].context.item;
 | 
			
		||||
            const parent = selection[0].length > 1 && selection[0][1].context.item;
 | 
			
		||||
 | 
			
		||||
            const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay';
 | 
			
		||||
            const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked';
 | 
			
		||||
            const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked';
 | 
			
		||||
 | 
			
		||||
            return !isOverlayPlotObject && isParentStackedPlotObject;
 | 
			
		||||
            return isStackedPlotObject;
 | 
			
		||||
        },
 | 
			
		||||
        view: function (selection) {
 | 
			
		||||
            let component;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div v-if="loaded">
 | 
			
		||||
    <ul class="l-inspector-part">
 | 
			
		||||
    <ul
 | 
			
		||||
        class="l-inspector-part"
 | 
			
		||||
        :aria-label="id > 1 ? `Y Axis ${id} Properties` : 'Y Axis Properties'"
 | 
			
		||||
    >
 | 
			
		||||
        <h2>Y Axis {{ id > 1 ? id : '' }}</h2>
 | 
			
		||||
        <li class="grid-row">
 | 
			
		||||
            <div
 | 
			
		||||
@@ -227,8 +230,8 @@ export default {
 | 
			
		||||
            let prefix = 'yAxis';
 | 
			
		||||
            if (this.isAdditionalYAxis) {
 | 
			
		||||
                let index = -1;
 | 
			
		||||
                if (this.additionalYAxes) {
 | 
			
		||||
                    index = this.additionalYAxes.findIndex((yAxis) => {
 | 
			
		||||
                if (this.domainObject?.configuration?.additionalYAxes) {
 | 
			
		||||
                    index = this.domainObject?.configuration?.additionalYAxes.findIndex((yAxis) => {
 | 
			
		||||
                        return yAxis.id === this.id;
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -49,10 +49,10 @@
 | 
			
		||||
                title="Cursor is point locked. Click anywhere in the plot to unlock."
 | 
			
		||||
            ></div>
 | 
			
		||||
            <plot-legend-item-collapsed
 | 
			
		||||
                v-for="(seriesObject, seriesIndex) in series"
 | 
			
		||||
                :key="`${seriesObject.keyString}-${seriesIndex}`"
 | 
			
		||||
                v-for="(seriesObject, seriesIndex) in seriesModels"
 | 
			
		||||
                :key="`${seriesObject.keyString}-${seriesIndex}-collapsed`"
 | 
			
		||||
                :highlights="highlights"
 | 
			
		||||
                :value-to-show-when-collapsed="legend.get('valueToShowWhenCollapsed')"
 | 
			
		||||
                :value-to-show-when-collapsed="valueToShowWhenCollapsed"
 | 
			
		||||
                :series-object="seriesObject"
 | 
			
		||||
                @legendHoverChanged="legendHoverChanged"
 | 
			
		||||
            />
 | 
			
		||||
@@ -95,11 +95,10 @@
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    <plot-legend-item-expanded
 | 
			
		||||
                        v-for="(seriesObject, seriesIndex) in series"
 | 
			
		||||
                        v-for="(seriesObject, seriesIndex) in seriesModels"
 | 
			
		||||
                        :key="`${seriesObject.keyString}-${seriesIndex}-expanded`"
 | 
			
		||||
                        :series-object="seriesObject"
 | 
			
		||||
                        :highlights="highlights"
 | 
			
		||||
                        :legend="legend"
 | 
			
		||||
                        @legendHoverChanged="legendHoverChanged"
 | 
			
		||||
                    />
 | 
			
		||||
                </tbody>
 | 
			
		||||
@@ -111,6 +110,9 @@
 | 
			
		||||
<script>
 | 
			
		||||
import PlotLegendItemCollapsed from "./PlotLegendItemCollapsed.vue";
 | 
			
		||||
import PlotLegendItemExpanded from "./PlotLegendItemExpanded.vue";
 | 
			
		||||
import configStore from "../configuration/ConfigStore";
 | 
			
		||||
import eventHelpers from "../lib/eventHelpers";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        PlotLegendItemExpanded,
 | 
			
		||||
@@ -124,57 +126,120 @@ export default {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        series: {
 | 
			
		||||
            type: Array,
 | 
			
		||||
            default() {
 | 
			
		||||
                return [];
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        highlights: {
 | 
			
		||||
            type: Array,
 | 
			
		||||
            default() {
 | 
			
		||||
                return [];
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        legend: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            default() {
 | 
			
		||||
                return {};
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            isLegendExpanded: this.legend.get('expanded') === true
 | 
			
		||||
            isLegendExpanded: false,
 | 
			
		||||
            seriesModels: [],
 | 
			
		||||
            loaded: false
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        showUnitsWhenExpanded() {
 | 
			
		||||
            return this.legend.get('showUnitsWhenExpanded') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('showUnitsWhenExpanded') === true;
 | 
			
		||||
        },
 | 
			
		||||
        showMinimumWhenExpanded() {
 | 
			
		||||
            return this.legend.get('showMinimumWhenExpanded') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('showMinimumWhenExpanded') === true;
 | 
			
		||||
        },
 | 
			
		||||
        showMaximumWhenExpanded() {
 | 
			
		||||
            return this.legend.get('showMaximumWhenExpanded') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('showMaximumWhenExpanded') === true;
 | 
			
		||||
        },
 | 
			
		||||
        showValueWhenExpanded() {
 | 
			
		||||
            return this.legend.get('showValueWhenExpanded') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('showValueWhenExpanded') === true;
 | 
			
		||||
        },
 | 
			
		||||
        showTimestampWhenExpanded() {
 | 
			
		||||
            return this.legend.get('showTimestampWhenExpanded') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('showTimestampWhenExpanded') === true;
 | 
			
		||||
        },
 | 
			
		||||
        isLegendHidden() {
 | 
			
		||||
            return this.legend.get('hideLegendWhenSmall') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('hideLegendWhenSmall') === true;
 | 
			
		||||
        },
 | 
			
		||||
        valueToShowWhenCollapsed() {
 | 
			
		||||
            return this.loaded && this.legend.get('valueToShowWhenCollapsed');
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.seriesModels = [];
 | 
			
		||||
        eventHelpers.extend(this);
 | 
			
		||||
        this.config = this.getConfig();
 | 
			
		||||
        this.legend = this.config.legend;
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
        this.isLegendExpanded = this.legend.get('expanded') === true;
 | 
			
		||||
        this.listenTo(this.config.legend, 'change:position', this.updatePosition, this);
 | 
			
		||||
        this.updatePosition();
 | 
			
		||||
 | 
			
		||||
        this.initialize();
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        if (this.objectComposition) {
 | 
			
		||||
            this.objectComposition.off('add', this.addTelemetryObject);
 | 
			
		||||
            this.objectComposition.off('remove', this.removeTelemetryObject);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.stopListening();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        initialize() {
 | 
			
		||||
            if (this.domainObject.type === 'telemetry.plot.stacked') {
 | 
			
		||||
                this.objectComposition = this.openmct.composition.get(this.domainObject);
 | 
			
		||||
                this.objectComposition.on('add', this.addTelemetryObject);
 | 
			
		||||
                this.objectComposition.on('remove', this.removeTelemetryObject);
 | 
			
		||||
                this.objectComposition.load();
 | 
			
		||||
            } else {
 | 
			
		||||
                this.registerListeners(this.config);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        getConfig() {
 | 
			
		||||
            const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
            return configStore.get(configId);
 | 
			
		||||
        },
 | 
			
		||||
        addTelemetryObject(object) {
 | 
			
		||||
            //get the config for each child
 | 
			
		||||
            const configId = this.openmct.objects.makeKeyString(object.identifier);
 | 
			
		||||
            const config = configStore.get(configId);
 | 
			
		||||
            if (config) {
 | 
			
		||||
                this.registerListeners(config);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        removeTelemetryObject(identifier) {
 | 
			
		||||
            const configId = this.openmct.objects.makeKeyString(identifier);
 | 
			
		||||
            const config = configStore.get(configId);
 | 
			
		||||
            if (config) {
 | 
			
		||||
                config.series.forEach(this.removeSeries, this);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        registerListeners(config) {
 | 
			
		||||
        //listen to any changes to the telemetry endpoints that are associated with the child
 | 
			
		||||
            this.listenTo(config.series, 'add', this.addSeries, this);
 | 
			
		||||
            this.listenTo(config.series, 'remove', this.removeSeries, this);
 | 
			
		||||
            config.series.forEach(this.addSeries, this);
 | 
			
		||||
        },
 | 
			
		||||
        addSeries(series) {
 | 
			
		||||
            this.$set(this.seriesModels, this.seriesModels.length, series);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        removeSeries(plotSeries) {
 | 
			
		||||
            this.stopListening(plotSeries);
 | 
			
		||||
 | 
			
		||||
            const seriesIndex = this.seriesModels.findIndex(series => series.keyString === plotSeries.keyString);
 | 
			
		||||
            this.seriesModels.splice(seriesIndex, 1);
 | 
			
		||||
        },
 | 
			
		||||
        expandLegend() {
 | 
			
		||||
            this.isLegendExpanded = !this.isLegendExpanded;
 | 
			
		||||
            this.legend.set('expanded', this.isLegendExpanded);
 | 
			
		||||
            this.$emit('expanded', this.isLegendExpanded);
 | 
			
		||||
        },
 | 
			
		||||
        legendHoverChanged(data) {
 | 
			
		||||
            this.$emit('legendHoverChanged', data);
 | 
			
		||||
        },
 | 
			
		||||
        updatePosition() {
 | 
			
		||||
            this.$emit('position', this.legend.get('position'));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -57,15 +57,12 @@
 | 
			
		||||
import {getLimitClass} from "@/plugins/plot/chart/limitUtil";
 | 
			
		||||
import eventHelpers from "../lib/eventHelpers";
 | 
			
		||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
 | 
			
		||||
import configStore from "../configuration/ConfigStore";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    mixins: [stalenessMixin],
 | 
			
		||||
    inject: ['openmct', 'domainObject'],
 | 
			
		||||
    props: {
 | 
			
		||||
        valueToShowWhenCollapsed: {
 | 
			
		||||
            type: String,
 | 
			
		||||
            required: true
 | 
			
		||||
        },
 | 
			
		||||
        seriesObject: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            required: true,
 | 
			
		||||
@@ -88,10 +85,14 @@ export default {
 | 
			
		||||
            formattedYValue: '',
 | 
			
		||||
            formattedXValue: '',
 | 
			
		||||
            mctLimitStateClass: '',
 | 
			
		||||
            formattedYValueFromStats: ''
 | 
			
		||||
            formattedYValueFromStats: '',
 | 
			
		||||
            loaded: false
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        valueToShowWhenCollapsed() {
 | 
			
		||||
            return this.loaded ? this.legend.get('valueToShowWhenCollapsed') : [];
 | 
			
		||||
        },
 | 
			
		||||
        valueToDisplayWhenCollapsedClass() {
 | 
			
		||||
            return `value-to-display-${ this.valueToShowWhenCollapsed }`;
 | 
			
		||||
        },
 | 
			
		||||
@@ -109,6 +110,9 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        eventHelpers.extend(this);
 | 
			
		||||
        this.config = this.getConfig();
 | 
			
		||||
        this.legend = this.config.legend;
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
        this.listenTo(this.seriesObject, 'change:color', (newColor) => {
 | 
			
		||||
            this.updateColor(newColor);
 | 
			
		||||
        }, this);
 | 
			
		||||
@@ -122,8 +126,13 @@ export default {
 | 
			
		||||
        this.stopListening();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        getConfig() {
 | 
			
		||||
            const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
            return configStore.get(configId);
 | 
			
		||||
        },
 | 
			
		||||
        initialize(highlightedObject) {
 | 
			
		||||
            const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
 | 
			
		||||
            const seriesObject = highlightedObject?.series || this.seriesObject;
 | 
			
		||||
 | 
			
		||||
            this.isMissing = seriesObject.domainObject.status === 'missing';
 | 
			
		||||
            this.colorAsHexString = seriesObject.get('color').asHexString();
 | 
			
		||||
 
 | 
			
		||||
@@ -83,6 +83,7 @@
 | 
			
		||||
import {getLimitClass} from "@/plugins/plot/chart/limitUtil";
 | 
			
		||||
import eventHelpers from "@/plugins/plot/lib/eventHelpers";
 | 
			
		||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
 | 
			
		||||
import configStore from "../configuration/ConfigStore";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    mixins: [stalenessMixin],
 | 
			
		||||
@@ -100,10 +101,6 @@ export default {
 | 
			
		||||
            default() {
 | 
			
		||||
                return [];
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        legend: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            required: true
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
@@ -116,24 +113,25 @@ export default {
 | 
			
		||||
            formattedXValue: '',
 | 
			
		||||
            formattedMinY: '',
 | 
			
		||||
            formattedMaxY: '',
 | 
			
		||||
            mctLimitStateClass: ''
 | 
			
		||||
            mctLimitStateClass: '',
 | 
			
		||||
            loaded: false
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        showUnitsWhenExpanded() {
 | 
			
		||||
            return this.legend.get('showUnitsWhenExpanded') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('showUnitsWhenExpanded') === true;
 | 
			
		||||
        },
 | 
			
		||||
        showMinimumWhenExpanded() {
 | 
			
		||||
            return this.legend.get('showMinimumWhenExpanded') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('showMinimumWhenExpanded') === true;
 | 
			
		||||
        },
 | 
			
		||||
        showMaximumWhenExpanded() {
 | 
			
		||||
            return this.legend.get('showMaximumWhenExpanded') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('showMaximumWhenExpanded') === true;
 | 
			
		||||
        },
 | 
			
		||||
        showValueWhenExpanded() {
 | 
			
		||||
            return this.legend.get('showValueWhenExpanded') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('showValueWhenExpanded') === true;
 | 
			
		||||
        },
 | 
			
		||||
        showTimestampWhenExpanded() {
 | 
			
		||||
            return this.legend.get('showTimestampWhenExpanded') === true;
 | 
			
		||||
            return this.loaded && this.legend.get('showTimestampWhenExpanded') === true;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
@@ -146,6 +144,9 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        eventHelpers.extend(this);
 | 
			
		||||
        this.config = this.getConfig();
 | 
			
		||||
        this.legend = this.config.legend;
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
        this.listenTo(this.seriesObject, 'change:color', (newColor) => {
 | 
			
		||||
            this.updateColor(newColor);
 | 
			
		||||
        }, this);
 | 
			
		||||
@@ -159,8 +160,13 @@ export default {
 | 
			
		||||
        this.stopListening();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        getConfig() {
 | 
			
		||||
            const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
            return configStore.get(configId);
 | 
			
		||||
        },
 | 
			
		||||
        initialize(highlightedObject) {
 | 
			
		||||
            const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
 | 
			
		||||
            const seriesObject = highlightedObject?.series || this.seriesObject;
 | 
			
		||||
 | 
			
		||||
            this.isMissing = seriesObject.domainObject.status === 'missing';
 | 
			
		||||
            this.colorAsHexString = seriesObject.get('color').asHexString();
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ import EventEmitter from "EventEmitter";
 | 
			
		||||
import PlotOptions from "./inspector/PlotOptions.vue";
 | 
			
		||||
import PlotConfigurationModel from "./configuration/PlotConfigurationModel";
 | 
			
		||||
 | 
			
		||||
const TEST_KEY_ID = 'test-key';
 | 
			
		||||
const TEST_KEY_ID = 'some-other-key';
 | 
			
		||||
 | 
			
		||||
describe("the plugin", function () {
 | 
			
		||||
    let element;
 | 
			
		||||
@@ -533,6 +533,30 @@ describe("the plugin", function () {
 | 
			
		||||
                expect(openmct.telemetry.request).toHaveBeenCalledTimes(2);
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            describe('limits', () => {
 | 
			
		||||
 | 
			
		||||
                it('lines are not displayed by default', () => {
 | 
			
		||||
                    let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
 | 
			
		||||
                    expect(limitEl.length).toBe(0);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it('lines are displayed when configuration is set to true', (done) => {
 | 
			
		||||
                    const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
 | 
			
		||||
                    const config = configStore.get(configId);
 | 
			
		||||
                    config.yAxis.set('displayRange', {
 | 
			
		||||
                        min: 0,
 | 
			
		||||
                        max: 4
 | 
			
		||||
                    });
 | 
			
		||||
                    config.series.models[0].set('limitLines', true);
 | 
			
		||||
 | 
			
		||||
                    Vue.nextTick(() => {
 | 
			
		||||
                        let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
 | 
			
		||||
                        expect(limitEl.length).toBe(4);
 | 
			
		||||
                        done();
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe('controls in time strip view', () => {
 | 
			
		||||
@@ -867,24 +891,5 @@ describe("the plugin", function () {
 | 
			
		||||
                expect(colorSwatch).toBeDefined();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe('limits', () => {
 | 
			
		||||
 | 
			
		||||
            it('lines are not displayed by default', () => {
 | 
			
		||||
                let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
 | 
			
		||||
                expect(limitEl.length).toBe(0);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            xit('lines are displayed when configuration is set to true', (done) => {
 | 
			
		||||
                config.series.models[0].set('limitLines', true);
 | 
			
		||||
 | 
			
		||||
                Vue.nextTick(() => {
 | 
			
		||||
                    let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
 | 
			
		||||
                    expect(limitEl.length).toBe(4);
 | 
			
		||||
                    done();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -27,31 +27,34 @@
 | 
			
		||||
    :class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
 | 
			
		||||
>
 | 
			
		||||
    <plot-legend
 | 
			
		||||
        v-if="compositionObjectsConfigLoaded"
 | 
			
		||||
        :cursor-locked="!!lockHighlightPoint"
 | 
			
		||||
        :series="seriesModels"
 | 
			
		||||
        :highlights="highlights"
 | 
			
		||||
        :legend="legend"
 | 
			
		||||
        @legendHoverChanged="legendHoverChanged"
 | 
			
		||||
        @expanded="updateExpanded"
 | 
			
		||||
        @position="updatePosition"
 | 
			
		||||
    />
 | 
			
		||||
    <div class="l-view-section">
 | 
			
		||||
    <div
 | 
			
		||||
        class="l-view-section"
 | 
			
		||||
    >
 | 
			
		||||
        <stacked-plot-item
 | 
			
		||||
            v-for="object in compositionObjects"
 | 
			
		||||
            :key="object.id"
 | 
			
		||||
            v-for="objectWrapper in compositionObjects"
 | 
			
		||||
            :key="objectWrapper.keyString"
 | 
			
		||||
            class="c-plot--stacked-container"
 | 
			
		||||
            :child-object="object"
 | 
			
		||||
            :child-object="objectWrapper.object"
 | 
			
		||||
            :options="options"
 | 
			
		||||
            :grid-lines="gridLines"
 | 
			
		||||
            :color-palette="colorPalette"
 | 
			
		||||
            :cursor-guide="cursorGuide"
 | 
			
		||||
            :show-limit-line-labels="showLimitLineLabels"
 | 
			
		||||
            :plot-tick-width="maxTickWidth"
 | 
			
		||||
            @plotTickWidth="onTickWidthChange"
 | 
			
		||||
            :parent-y-tick-width="maxTickWidth"
 | 
			
		||||
            @plotYTickWidth="onYTickWidthChange"
 | 
			
		||||
            @loadingUpdated="loadingUpdated"
 | 
			
		||||
            @cursorGuide="onCursorGuideChange"
 | 
			
		||||
            @gridLines="onGridLinesChange"
 | 
			
		||||
            @lockHighlightPoint="lockHighlightPointUpdated"
 | 
			
		||||
            @highlights="highlightsUpdated"
 | 
			
		||||
            @configLoaded="registerSeriesListeners"
 | 
			
		||||
            @configLoaded="configLoadedForObject(objectWrapper.keyString)"
 | 
			
		||||
        />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -66,14 +69,13 @@ import ColorPalette from "@/ui/color/ColorPalette";
 | 
			
		||||
import PlotLegend from "../legend/PlotLegend.vue";
 | 
			
		||||
import StackedPlotItem from './StackedPlotItem.vue';
 | 
			
		||||
import ImageExporter from '../../../exporters/ImageExporter';
 | 
			
		||||
import eventHelpers from "@/plugins/plot/lib/eventHelpers";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        StackedPlotItem,
 | 
			
		||||
        PlotLegend
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct', 'domainObject', 'composition', 'path'],
 | 
			
		||||
    inject: ['openmct', 'domainObject', 'path'],
 | 
			
		||||
    props: {
 | 
			
		||||
        options: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
@@ -87,48 +89,59 @@ export default {
 | 
			
		||||
            hideExportButtons: false,
 | 
			
		||||
            cursorGuide: false,
 | 
			
		||||
            gridLines: true,
 | 
			
		||||
            loading: false,
 | 
			
		||||
            configLoaded: {},
 | 
			
		||||
            compositionObjects: [],
 | 
			
		||||
            tickWidthMap: {},
 | 
			
		||||
            legend: {},
 | 
			
		||||
            loaded: false,
 | 
			
		||||
            lockHighlightPoint: false,
 | 
			
		||||
            highlights: [],
 | 
			
		||||
            seriesModels: [],
 | 
			
		||||
            showLimitLineLabels: undefined,
 | 
			
		||||
            colorPalette: new ColorPalette()
 | 
			
		||||
            colorPalette: new ColorPalette(),
 | 
			
		||||
            compositionObjectsConfigLoaded: false,
 | 
			
		||||
            position: 'top',
 | 
			
		||||
            expanded: false
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        plotLegendPositionClass() {
 | 
			
		||||
            return `plot-legend-${this.config.legend.get('position')}`;
 | 
			
		||||
            return `plot-legend-${this.position}`;
 | 
			
		||||
        },
 | 
			
		||||
        plotLegendExpandedStateClass() {
 | 
			
		||||
            if (this.config.legend.get('expanded')) {
 | 
			
		||||
            if (this.expanded) {
 | 
			
		||||
                return 'plot-legend-expanded';
 | 
			
		||||
            } else {
 | 
			
		||||
                return 'plot-legend-collapsed';
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
       * Returns the maximum width of the left and right y axes ticks of this stacked plots children
 | 
			
		||||
       * @returns {{rightTickWidth: number, leftTickWidth: number, hasMultipleLeftAxes: boolean}}
 | 
			
		||||
       */
 | 
			
		||||
        maxTickWidth() {
 | 
			
		||||
            return Math.max(...Object.values(this.tickWidthMap));
 | 
			
		||||
            const tickWidthValues = Object.values(this.tickWidthMap);
 | 
			
		||||
            const maxLeftTickWidth = Math.max(...tickWidthValues.map(tickWidthItem => tickWidthItem.leftTickWidth));
 | 
			
		||||
            const maxRightTickWidth = Math.max(...tickWidthValues.map(tickWidthItem => tickWidthItem.rightTickWidth));
 | 
			
		||||
            const hasMultipleLeftAxes = tickWidthValues.some(tickWidthItem => tickWidthItem.hasMultipleLeftAxes === true);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                leftTickWidth: maxLeftTickWidth,
 | 
			
		||||
                rightTickWidth: maxRightTickWidth,
 | 
			
		||||
                hasMultipleLeftAxes
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        this.destroy();
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        eventHelpers.extend(this);
 | 
			
		||||
        this.seriesConfig = {};
 | 
			
		||||
 | 
			
		||||
        //We only need to initialize the stacked plot config for legend properties
 | 
			
		||||
        const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
        this.config = this.getConfig(configId);
 | 
			
		||||
 | 
			
		||||
        this.legend = this.config.legend;
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
        this.imageExporter = new ImageExporter(this.openmct);
 | 
			
		||||
 | 
			
		||||
        this.composition = this.openmct.composition.get(this.domainObject);
 | 
			
		||||
        this.composition.on('add', this.addChild);
 | 
			
		||||
        this.composition.on('remove', this.removeChild);
 | 
			
		||||
        this.composition.on('reorder', this.compositionReorder);
 | 
			
		||||
@@ -142,7 +155,6 @@ export default {
 | 
			
		||||
                    id: configId,
 | 
			
		||||
                    domainObject: this.domainObject,
 | 
			
		||||
                    openmct: this.openmct,
 | 
			
		||||
                    palette: this.colorPalette,
 | 
			
		||||
                    callback: (data) => {
 | 
			
		||||
                        this.data = data;
 | 
			
		||||
                    }
 | 
			
		||||
@@ -155,10 +167,19 @@ export default {
 | 
			
		||||
        loadingUpdated(loaded) {
 | 
			
		||||
            this.loading = loaded;
 | 
			
		||||
        },
 | 
			
		||||
        destroy() {
 | 
			
		||||
            this.stopListening();
 | 
			
		||||
            configStore.deleteStore(this.config.id);
 | 
			
		||||
        configLoadedForObject(childObjIdentifier) {
 | 
			
		||||
            const childObjId = this.openmct.objects.makeKeyString(childObjIdentifier);
 | 
			
		||||
            this.configLoaded[childObjId] = true;
 | 
			
		||||
            this.setConfigLoadedForComposition();
 | 
			
		||||
        },
 | 
			
		||||
        setConfigLoadedForComposition() {
 | 
			
		||||
            this.compositionObjectsConfigLoaded = this.compositionObjects.length && this.compositionObjects.every(childObject => {
 | 
			
		||||
                const id = childObject.keyString;
 | 
			
		||||
 | 
			
		||||
                return this.configLoaded[id] === true;
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        destroy() {
 | 
			
		||||
            this.composition.off('add', this.addChild);
 | 
			
		||||
            this.composition.off('remove', this.removeChild);
 | 
			
		||||
            this.composition.off('reorder', this.compositionReorder);
 | 
			
		||||
@@ -167,9 +188,16 @@ export default {
 | 
			
		||||
        addChild(child) {
 | 
			
		||||
            const id = this.openmct.objects.makeKeyString(child.identifier);
 | 
			
		||||
 | 
			
		||||
            this.$set(this.tickWidthMap, id, 0);
 | 
			
		||||
            this.$set(this.tickWidthMap, id, {
 | 
			
		||||
                leftTickWidth: 0,
 | 
			
		||||
                rightTickWidth: 0
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.compositionObjects.push(child);
 | 
			
		||||
            this.compositionObjects.push({
 | 
			
		||||
                object: child,
 | 
			
		||||
                keyString: id
 | 
			
		||||
            });
 | 
			
		||||
            this.setConfigLoadedForComposition();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        removeChild(childIdentifier) {
 | 
			
		||||
@@ -177,26 +205,36 @@ export default {
 | 
			
		||||
 | 
			
		||||
            this.$delete(this.tickWidthMap, id);
 | 
			
		||||
 | 
			
		||||
            const childObj = this.compositionObjects.filter((c) => {
 | 
			
		||||
                const identifier = c.keyString;
 | 
			
		||||
 | 
			
		||||
                return identifier === id;
 | 
			
		||||
            })[0];
 | 
			
		||||
 | 
			
		||||
            if (childObj) {
 | 
			
		||||
                if (childObj.object.type !== 'telemetry.plot.overlay') {
 | 
			
		||||
                    const config = this.getConfig(childObj.keyString);
 | 
			
		||||
                    if (config) {
 | 
			
		||||
                        config.series.remove(config.series.at(0));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.compositionObjects = this.compositionObjects.filter((c) => {
 | 
			
		||||
                const identifier = c.keyString;
 | 
			
		||||
 | 
			
		||||
                return identifier !== id;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const configIndex = this.domainObject.configuration.series.findIndex((seriesConfig) => {
 | 
			
		||||
                return this.openmct.objects.areIdsEqual(seriesConfig.identifier, childIdentifier);
 | 
			
		||||
            });
 | 
			
		||||
            if (configIndex > -1) {
 | 
			
		||||
                this.domainObject.configuration.series.splice(configIndex, 1);
 | 
			
		||||
                const cSeries = this.domainObject.configuration.series.slice();
 | 
			
		||||
                this.openmct.objects.mutate(this.domainObject, 'configuration.series', cSeries);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.removeSeries({
 | 
			
		||||
                keyString: id
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const childObj = this.compositionObjects.filter((c) => {
 | 
			
		||||
                const identifier = this.openmct.objects.makeKeyString(c.identifier);
 | 
			
		||||
 | 
			
		||||
                return identifier === id;
 | 
			
		||||
            })[0];
 | 
			
		||||
            if (childObj) {
 | 
			
		||||
                const index = this.compositionObjects.indexOf(childObj);
 | 
			
		||||
                this.compositionObjects.splice(index, 1);
 | 
			
		||||
            }
 | 
			
		||||
            this.setConfigLoadedForComposition();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        compositionReorder(reorderPlan) {
 | 
			
		||||
@@ -209,7 +247,10 @@ export default {
 | 
			
		||||
 | 
			
		||||
        resetTelemetryAndTicks(domainObject) {
 | 
			
		||||
            this.compositionObjects = [];
 | 
			
		||||
            this.tickWidthMap = {};
 | 
			
		||||
            this.tickWidthMap = {
 | 
			
		||||
                leftTickWidth: 0,
 | 
			
		||||
                rightTickWidth: 0
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        exportJPG() {
 | 
			
		||||
@@ -232,12 +273,18 @@ export default {
 | 
			
		||||
                    this.hideExportButtons = false;
 | 
			
		||||
                }.bind(this));
 | 
			
		||||
        },
 | 
			
		||||
        onTickWidthChange(width, plotId) {
 | 
			
		||||
        /**
 | 
			
		||||
         * @typedef {Object} PlotYTickData
 | 
			
		||||
         * @property {Number} leftTickWidth the width of the ticks for all the y axes on the left of the plot.
 | 
			
		||||
         * @property {Number} rightTickWidth the width of the ticks for all the y axes on the right of the plot.
 | 
			
		||||
         * @property {Boolean} hasMultipleLeftAxes whether or not there is more than one left y axis.
 | 
			
		||||
         */
 | 
			
		||||
        onYTickWidthChange(data, plotId) {
 | 
			
		||||
            if (!Object.prototype.hasOwnProperty.call(this.tickWidthMap, plotId)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.$set(this.tickWidthMap, plotId, width);
 | 
			
		||||
            this.$set(this.tickWidthMap, plotId, data);
 | 
			
		||||
        },
 | 
			
		||||
        legendHoverChanged(data) {
 | 
			
		||||
            this.showLimitLineLabels = data;
 | 
			
		||||
@@ -245,39 +292,18 @@ export default {
 | 
			
		||||
        lockHighlightPointUpdated(data) {
 | 
			
		||||
            this.lockHighlightPoint = data;
 | 
			
		||||
        },
 | 
			
		||||
        updateExpanded(expanded) {
 | 
			
		||||
            this.expanded = expanded;
 | 
			
		||||
        },
 | 
			
		||||
        updatePosition(position) {
 | 
			
		||||
            this.position = position;
 | 
			
		||||
        },
 | 
			
		||||
        updateReady(ready) {
 | 
			
		||||
            this.configReady = ready;
 | 
			
		||||
        },
 | 
			
		||||
        highlightsUpdated(data) {
 | 
			
		||||
            this.highlights = data;
 | 
			
		||||
        },
 | 
			
		||||
        registerSeriesListeners(configId) {
 | 
			
		||||
            const config = this.getConfig(configId);
 | 
			
		||||
            this.seriesConfig[configId] = config;
 | 
			
		||||
            const childObject = config.get('domainObject');
 | 
			
		||||
 | 
			
		||||
            //TODO differentiate between objects with composition and those without
 | 
			
		||||
            if (childObject.type === 'telemetry.plot.overlay') {
 | 
			
		||||
                this.listenTo(config.series, 'add', this.addSeries, this);
 | 
			
		||||
                this.listenTo(config.series, 'remove', this.removeSeries, this);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            config.series.models.forEach(this.addSeries, this);
 | 
			
		||||
        },
 | 
			
		||||
        addSeries(series) {
 | 
			
		||||
            const childObject = series.domainObject;
 | 
			
		||||
            //don't add the series if it can have child series this will happen in registerSeriesListeners
 | 
			
		||||
            if (childObject.type !== 'telemetry.plot.overlay') {
 | 
			
		||||
                const index = this.seriesModels.length;
 | 
			
		||||
                this.$set(this.seriesModels, index, series);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        },
 | 
			
		||||
        removeSeries(plotSeries) {
 | 
			
		||||
            const index = this.seriesModels.findIndex(seriesModel => seriesModel.keyString === plotSeries.keyString);
 | 
			
		||||
            if (index > -1) {
 | 
			
		||||
                this.$delete(this.seriesModels, index);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.stopListening(plotSeries);
 | 
			
		||||
        },
 | 
			
		||||
        onCursorGuideChange(cursorGuide) {
 | 
			
		||||
            this.cursorGuide = cursorGuide === true;
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,9 @@
 | 
			
		||||
 at runtime from the About dialog for additional information.
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
<div></div>
 | 
			
		||||
<div
 | 
			
		||||
    :aria-label="`Stacked Plot Item ${childObject.name}`"
 | 
			
		||||
></div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
@@ -28,6 +30,7 @@ import MctPlot from '../MctPlot.vue';
 | 
			
		||||
import Vue from "vue";
 | 
			
		||||
import conditionalStylesMixin from "./mixins/objectStyles-mixin";
 | 
			
		||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
 | 
			
		||||
import StalenessUtils from '@/utils/staleness';
 | 
			
		||||
import configStore from "@/plugins/plot/configuration/ConfigStore";
 | 
			
		||||
import PlotConfigurationModel from "@/plugins/plot/configuration/PlotConfigurationModel";
 | 
			
		||||
import ProgressBar from "../../../ui/components/ProgressBar.vue";
 | 
			
		||||
@@ -72,13 +75,22 @@ export default {
 | 
			
		||||
                return undefined;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        plotTickWidth: {
 | 
			
		||||
            type: Number,
 | 
			
		||||
        parentYTickWidth: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            default() {
 | 
			
		||||
                return 0;
 | 
			
		||||
                return {
 | 
			
		||||
                    leftTickWidth: 0,
 | 
			
		||||
                    rightTickWidth: 0,
 | 
			
		||||
                    hasMultipleLeftAxes: false
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            staleObjects: []
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
        gridLines(newGridLines) {
 | 
			
		||||
            this.updateComponentProp('gridLines', newGridLines);
 | 
			
		||||
@@ -86,20 +98,29 @@ export default {
 | 
			
		||||
        cursorGuide(newCursorGuide) {
 | 
			
		||||
            this.updateComponentProp('cursorGuide', newCursorGuide);
 | 
			
		||||
        },
 | 
			
		||||
        plotTickWidth(width) {
 | 
			
		||||
            this.updateComponentProp('plotTickWidth', width);
 | 
			
		||||
        parentYTickWidth(width) {
 | 
			
		||||
            this.updateComponentProp('parentYTickWidth', width);
 | 
			
		||||
        },
 | 
			
		||||
        showLimitLineLabels: {
 | 
			
		||||
            handler(data) {
 | 
			
		||||
                this.updateComponentProp('limitLineLabels', data);
 | 
			
		||||
            },
 | 
			
		||||
            deep: true
 | 
			
		||||
        },
 | 
			
		||||
        staleObjects() {
 | 
			
		||||
            this.isStale = this.staleObjects.length > 0;
 | 
			
		||||
            this.updateComponentProp('isStale', this.isStale);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.stalenessSubscription = {};
 | 
			
		||||
        this.updateView();
 | 
			
		||||
        this.isEditing = this.openmct.editor.isEditing();
 | 
			
		||||
        this.openmct.editor.on('isEditing', this.setEditState);
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        this.openmct.editor.off('isEditing', this.setEditState);
 | 
			
		||||
 | 
			
		||||
        if (this.removeSelectable) {
 | 
			
		||||
            this.removeSelectable();
 | 
			
		||||
        }
 | 
			
		||||
@@ -107,8 +128,22 @@ export default {
 | 
			
		||||
        if (this.component) {
 | 
			
		||||
            this.component.$destroy();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.destroyStalenessListeners();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        setEditState(isEditing) {
 | 
			
		||||
            this.isEditing = isEditing;
 | 
			
		||||
 | 
			
		||||
            if (this.isEditing) {
 | 
			
		||||
                this.setSelection();
 | 
			
		||||
            } else {
 | 
			
		||||
                if (this.removeSelectable) {
 | 
			
		||||
                    this.removeSelectable();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        updateComponentProp(prop, value) {
 | 
			
		||||
            if (this.component) {
 | 
			
		||||
                this.component[prop] = value;
 | 
			
		||||
@@ -117,15 +152,15 @@ export default {
 | 
			
		||||
        updateView() {
 | 
			
		||||
            this.isStale = false;
 | 
			
		||||
 | 
			
		||||
            this.triggerUnsubscribeFromStaleness();
 | 
			
		||||
            this.destroyStalenessListeners();
 | 
			
		||||
 | 
			
		||||
            if (this.component) {
 | 
			
		||||
                this.component.$destroy();
 | 
			
		||||
                this.component = undefined;
 | 
			
		||||
                this.component = null;
 | 
			
		||||
                this.$el.innerHTML = '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const onTickWidthChange = this.onTickWidthChange;
 | 
			
		||||
            const onYTickWidthChange = this.onYTickWidthChange;
 | 
			
		||||
            const onLockHighlightPointUpdated = this.onLockHighlightPointUpdated;
 | 
			
		||||
            const onHighlightsUpdated = this.onHighlightsUpdated;
 | 
			
		||||
            const onConfigLoaded = this.onConfigLoaded;
 | 
			
		||||
@@ -144,9 +179,18 @@ export default {
 | 
			
		||||
            let viewContainer = document.createElement('div');
 | 
			
		||||
            this.$el.append(viewContainer);
 | 
			
		||||
 | 
			
		||||
            this.subscribeToStaleness(object, (isStale) => {
 | 
			
		||||
                this.updateComponentProp('isStale', isStale);
 | 
			
		||||
            });
 | 
			
		||||
            if (this.openmct.telemetry.isTelemetryObject(object)) {
 | 
			
		||||
                this.subscribeToStaleness(object, (isStale) => {
 | 
			
		||||
                    this.updateComponentProp('isStale', isStale);
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                // possibly overlay or other composition based plot
 | 
			
		||||
                this.composition = this.openmct.composition.get(object);
 | 
			
		||||
 | 
			
		||||
                this.composition.on('add', this.watchStaleness);
 | 
			
		||||
                this.composition.on('remove', this.unwatchStaleness);
 | 
			
		||||
                this.composition.load();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.component = new Vue({
 | 
			
		||||
                el: viewContainer,
 | 
			
		||||
@@ -162,7 +206,7 @@ export default {
 | 
			
		||||
                data() {
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...getProps(),
 | 
			
		||||
                        onTickWidthChange,
 | 
			
		||||
                        onYTickWidthChange,
 | 
			
		||||
                        onLockHighlightPointUpdated,
 | 
			
		||||
                        onHighlightsUpdated,
 | 
			
		||||
                        onConfigLoaded,
 | 
			
		||||
@@ -178,10 +222,72 @@ export default {
 | 
			
		||||
                        this.loading = loaded;
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                template: '<div v-if="!isMissing" ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\', \'is-stale\': isStale}"><progress-bar v-show="loading !== false" class="c-telemetry-table__progress-bar" :model="{progressPerc: undefined}" /><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :limit-line-labels="limitLineLabels" :color-palette="colorPalette" :options="options" @plotTickWidth="onTickWidthChange" @lockHighlightPoint="onLockHighlightPointUpdated" @highlights="onHighlightsUpdated" @configLoaded="onConfigLoaded" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
 | 
			
		||||
                template: `
 | 
			
		||||
                  <div v-if="!isMissing" ref="plotWrapper"
 | 
			
		||||
                      class="l-view-section u-style-receiver js-style-receiver"
 | 
			
		||||
                      :class="{'s-status-timeconductor-unsynced': status && status === 'timeconductor-unsynced', 'is-stale': isStale}">
 | 
			
		||||
                      <progress-bar
 | 
			
		||||
                          v-show="loading !== false"
 | 
			
		||||
                          class="c-telemetry-table__progress-bar"
 | 
			
		||||
                          :model="{progressPerc: undefined}" />
 | 
			
		||||
                      <mct-plot
 | 
			
		||||
                          :init-grid-lines="gridLines"
 | 
			
		||||
                          :init-cursor-guide="cursorGuide"
 | 
			
		||||
                          :parent-y-tick-width="parentYTickWidth"
 | 
			
		||||
                          :limit-line-labels="limitLineLabels"
 | 
			
		||||
                          :color-palette="colorPalette"
 | 
			
		||||
                          :options="options"
 | 
			
		||||
                          @plotYTickWidth="onYTickWidthChange"
 | 
			
		||||
                          @lockHighlightPoint="onLockHighlightPointUpdated"
 | 
			
		||||
                          @highlights="onHighlightsUpdated"
 | 
			
		||||
                          @configLoaded="onConfigLoaded"
 | 
			
		||||
                          @cursorGuide="onCursorGuideChange"
 | 
			
		||||
                          @gridLines="onGridLinesChange"
 | 
			
		||||
                          @statusUpdated="setStatus"
 | 
			
		||||
                          @loadingUpdated="loadingUpdated"/>
 | 
			
		||||
                  </div>`
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.setSelection();
 | 
			
		||||
            if (this.isEditing) {
 | 
			
		||||
                this.setSelection();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        watchStaleness(domainObject) {
 | 
			
		||||
            const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
            this.stalenessSubscription[keyString] = {};
 | 
			
		||||
            this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
 | 
			
		||||
 | 
			
		||||
            this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
 | 
			
		||||
                if (stalenessResponse !== undefined) {
 | 
			
		||||
                    this.handleStaleness(keyString, stalenessResponse);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
 | 
			
		||||
                this.handleStaleness(keyString, stalenessResponse);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription;
 | 
			
		||||
        },
 | 
			
		||||
        unwatchStaleness(domainObject) {
 | 
			
		||||
            const SKIP_CHECK = true;
 | 
			
		||||
            const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
            this.stalenessSubscription[keyString].unsubscribe();
 | 
			
		||||
            this.stalenessSubscription[keyString].stalenessUtils.destroy();
 | 
			
		||||
            this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK);
 | 
			
		||||
 | 
			
		||||
            delete this.stalenessSubscription[keyString];
 | 
			
		||||
        },
 | 
			
		||||
        handleStaleness(keyString, stalenessResponse, skipCheck = false) {
 | 
			
		||||
            if (skipCheck || this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
 | 
			
		||||
                const index = this.staleObjects.indexOf(keyString);
 | 
			
		||||
                const foundStaleObject = index > -1;
 | 
			
		||||
                if (stalenessResponse.isStale && !foundStaleObject) {
 | 
			
		||||
                    this.staleObjects.push(keyString);
 | 
			
		||||
                } else if (!stalenessResponse.isStale && foundStaleObject) {
 | 
			
		||||
                    this.staleObjects.splice(index, 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        onLockHighlightPointUpdated() {
 | 
			
		||||
            this.$emit('lockHighlightPoint', ...arguments);
 | 
			
		||||
@@ -192,8 +298,8 @@ export default {
 | 
			
		||||
        onConfigLoaded() {
 | 
			
		||||
            this.$emit('configLoaded', ...arguments);
 | 
			
		||||
        },
 | 
			
		||||
        onTickWidthChange() {
 | 
			
		||||
            this.$emit('plotTickWidth', ...arguments);
 | 
			
		||||
        onYTickWidthChange() {
 | 
			
		||||
            this.$emit('plotYTickWidth', ...arguments);
 | 
			
		||||
        },
 | 
			
		||||
        onCursorGuideChange() {
 | 
			
		||||
            this.$emit('cursorGuide', ...arguments);
 | 
			
		||||
@@ -221,7 +327,7 @@ export default {
 | 
			
		||||
                limitLineLabels: this.showLimitLineLabels,
 | 
			
		||||
                gridLines: this.gridLines,
 | 
			
		||||
                cursorGuide: this.cursorGuide,
 | 
			
		||||
                plotTickWidth: this.plotTickWidth,
 | 
			
		||||
                parentYTickWidth: this.parentYTickWidth,
 | 
			
		||||
                options: this.options,
 | 
			
		||||
                status: this.status,
 | 
			
		||||
                colorPalette: this.colorPalette,
 | 
			
		||||
@@ -230,7 +336,7 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
        getPlotObject() {
 | 
			
		||||
            if (this.childObject.configuration && this.childObject.configuration.series) {
 | 
			
		||||
                //If the object has a configuration, allow initialization of the config from it's persisted config
 | 
			
		||||
                //If the object has a configuration (like an overlay plot), allow initialization of the config from it's persisted config
 | 
			
		||||
                return this.childObject;
 | 
			
		||||
            } else {
 | 
			
		||||
                //If object is missing, warn and return object
 | 
			
		||||
@@ -281,6 +387,20 @@ export default {
 | 
			
		||||
 | 
			
		||||
                return this.childObject;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        destroyStalenessListeners() {
 | 
			
		||||
            this.triggerUnsubscribeFromStaleness();
 | 
			
		||||
 | 
			
		||||
            if (this.composition) {
 | 
			
		||||
                this.composition.off('add', this.watchStaleness);
 | 
			
		||||
                this.composition.off('remove', this.unwatchStaleness);
 | 
			
		||||
                this.composition = null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
 | 
			
		||||
                stalenessSubscription.unsubscribe();
 | 
			
		||||
                stalenessSubscription.stalenessUtils.destroy();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,6 @@ export default function StackedPlotViewProvider(openmct) {
 | 
			
		||||
                        provide: {
 | 
			
		||||
                            openmct,
 | 
			
		||||
                            domainObject,
 | 
			
		||||
                            composition: openmct.composition.get(domainObject),
 | 
			
		||||
                            path: objectPath
 | 
			
		||||
                        },
 | 
			
		||||
                        data() {
 | 
			
		||||
 
 | 
			
		||||
@@ -173,7 +173,7 @@ describe("the plugin", function () {
 | 
			
		||||
        let testTelemetryObject2;
 | 
			
		||||
        let config;
 | 
			
		||||
        let component;
 | 
			
		||||
        let mockComposition;
 | 
			
		||||
        let mockCompositionList = [];
 | 
			
		||||
        let plotViewComponentObject;
 | 
			
		||||
 | 
			
		||||
        afterAll(() => {
 | 
			
		||||
@@ -271,14 +271,34 @@ describe("the plugin", function () {
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            mockComposition = new EventEmitter();
 | 
			
		||||
            mockComposition.load = () => {
 | 
			
		||||
                mockComposition.emit('add', testTelemetryObject);
 | 
			
		||||
            stackedPlotObject.composition = [{
 | 
			
		||||
                identifier: testTelemetryObject.identifier
 | 
			
		||||
            }];
 | 
			
		||||
 | 
			
		||||
                return [testTelemetryObject];
 | 
			
		||||
            };
 | 
			
		||||
            mockCompositionList = [];
 | 
			
		||||
            spyOn(openmct.composition, 'get').and.callFake((domainObject) => {
 | 
			
		||||
                //We need unique compositions here - one for the StackedPlot view and one for the PlotLegend view
 | 
			
		||||
                const numObjects = domainObject.composition.length;
 | 
			
		||||
                const mockComposition = new EventEmitter();
 | 
			
		||||
                mockComposition.load = () => {
 | 
			
		||||
                    if (numObjects === 1) {
 | 
			
		||||
                        mockComposition.emit('add', testTelemetryObject);
 | 
			
		||||
 | 
			
		||||
            spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
 | 
			
		||||
                        return [testTelemetryObject];
 | 
			
		||||
                    } else if (numObjects === 2) {
 | 
			
		||||
                        mockComposition.emit('add', testTelemetryObject);
 | 
			
		||||
                        mockComposition.emit('add', testTelemetryObject2);
 | 
			
		||||
 | 
			
		||||
                        return [testTelemetryObject, testTelemetryObject2];
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return [];
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                mockCompositionList.push(mockComposition);
 | 
			
		||||
 | 
			
		||||
                return mockComposition;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            let viewContainer = document.createElement("div");
 | 
			
		||||
            child.append(viewContainer);
 | 
			
		||||
@@ -290,7 +310,6 @@ describe("the plugin", function () {
 | 
			
		||||
                provide: {
 | 
			
		||||
                    openmct: openmct,
 | 
			
		||||
                    domainObject: stackedPlotObject,
 | 
			
		||||
                    composition: openmct.composition.get(stackedPlotObject),
 | 
			
		||||
                    path: [stackedPlotObject]
 | 
			
		||||
                },
 | 
			
		||||
                template: "<stacked-plot></stacked-plot>"
 | 
			
		||||
@@ -321,7 +340,7 @@ describe("the plugin", function () {
 | 
			
		||||
            expect(legend.length).toBe(6);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("Renders X-axis ticks for the telemetry object", (done) => {
 | 
			
		||||
        it("Renders X-axis ticks for the telemetry object", () => {
 | 
			
		||||
            let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
 | 
			
		||||
            expect(xAxisElement.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
@@ -329,13 +348,8 @@ describe("the plugin", function () {
 | 
			
		||||
                min: 0,
 | 
			
		||||
                max: 4
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Vue.nextTick(() => {
 | 
			
		||||
                let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
 | 
			
		||||
                expect(ticks.length).toBe(9);
 | 
			
		||||
 | 
			
		||||
                done();
 | 
			
		||||
            });
 | 
			
		||||
            let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
 | 
			
		||||
            expect(ticks.length).toBe(9);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("Renders Y-axis ticks for the telemetry object", (done) => {
 | 
			
		||||
@@ -401,17 +415,22 @@ describe("the plugin", function () {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('plots a new series when a new telemetry object is added', (done) => {
 | 
			
		||||
            mockComposition.emit('add', testTelemetryObject2);
 | 
			
		||||
            //setting composition here so that any new triggers to composition.load with correctly load the mockComposition in the beforeEach
 | 
			
		||||
            stackedPlotObject.composition = [testTelemetryObject, testTelemetryObject2];
 | 
			
		||||
            mockCompositionList[0].emit('add', testTelemetryObject2);
 | 
			
		||||
 | 
			
		||||
            Vue.nextTick(() => {
 | 
			
		||||
                let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
 | 
			
		||||
                expect(legend.length).toBe(2);
 | 
			
		||||
                expect(legend[1].innerHTML).toEqual("Test Object2");
 | 
			
		||||
                done();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('removes plots from series when a telemetry object is removed', (done) => {
 | 
			
		||||
            mockComposition.emit('remove', testTelemetryObject.identifier);
 | 
			
		||||
            stackedPlotObject.composition = [];
 | 
			
		||||
            mockCompositionList[0].emit('remove', testTelemetryObject.identifier);
 | 
			
		||||
            Vue.nextTick(() => {
 | 
			
		||||
                expect(plotViewComponentObject.compositionObjects.length).toBe(0);
 | 
			
		||||
                done();
 | 
			
		||||
@@ -429,16 +448,6 @@ describe("the plugin", function () {
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("Renders a new series when added to one of the plots", (done) => {
 | 
			
		||||
            mockComposition.emit('add', testTelemetryObject2);
 | 
			
		||||
            Vue.nextTick(() => {
 | 
			
		||||
                let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
 | 
			
		||||
                expect(legend.length).toBe(2);
 | 
			
		||||
                expect(legend[1].innerHTML).toEqual("Test Object2");
 | 
			
		||||
                done();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("Adds a new point to the plot", (done) => {
 | 
			
		||||
            let originalLength = config.series.models[0].getSeriesData().length;
 | 
			
		||||
            config.series.models[0].add({
 | 
			
		||||
@@ -459,7 +468,7 @@ describe("the plugin", function () {
 | 
			
		||||
                max: 10
 | 
			
		||||
            });
 | 
			
		||||
            Vue.nextTick(() => {
 | 
			
		||||
                expect(plotViewComponentObject.$children[1].component.$children[1].xScale.domain()).toEqual({
 | 
			
		||||
                expect(plotViewComponentObject.$children[0].component.$children[1].xScale.domain()).toEqual({
 | 
			
		||||
                    min: 0,
 | 
			
		||||
                    max: 10
 | 
			
		||||
                });
 | 
			
		||||
@@ -476,7 +485,7 @@ describe("the plugin", function () {
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
            Vue.nextTick(() => {
 | 
			
		||||
                const yAxesScales = plotViewComponentObject.$children[1].component.$children[1].yScale;
 | 
			
		||||
                const yAxesScales = plotViewComponentObject.$children[0].component.$children[1].yScale;
 | 
			
		||||
                yAxesScales.forEach((yAxisScale) => {
 | 
			
		||||
                    expect(yAxisScale.scale.domain()).toEqual({
 | 
			
		||||
                        min: 10,
 | 
			
		||||
 
 | 
			
		||||
@@ -293,6 +293,7 @@ define([
 | 
			
		||||
            this.stalenessSubscription[keyString].unsubscribe();
 | 
			
		||||
            this.stalenessSubscription[keyString].stalenessUtils.destroy();
 | 
			
		||||
            this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK);
 | 
			
		||||
            delete this.stalenessSubscription[keyString];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        clearData() {
 | 
			
		||||
 
 | 
			
		||||
@@ -227,6 +227,10 @@ export default {
 | 
			
		||||
            if (this.isFixed) {
 | 
			
		||||
                offsets = this.timeOptions.fixedOffsets;
 | 
			
		||||
            } else {
 | 
			
		||||
                if (this.timeOptions.clockOffsets === undefined) {
 | 
			
		||||
                    this.timeOptions.clockOffsets = this.openmct.time.clockOffsets();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                offsets = this.timeOptions.clockOffsets;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -390,7 +390,7 @@ $colorItemTreeHoverBg: rgba(#fff, 0.1);
 | 
			
		||||
$colorItemTreeHoverFg: #fff;
 | 
			
		||||
$colorItemTreeIcon: $colorKey; // Used
 | 
			
		||||
$colorItemTreeIconHover: $colorItemTreeIcon; // Used
 | 
			
		||||
$colorItemTreeFg: $colorBodyFg;
 | 
			
		||||
$colorItemTreeFg: #ccc;
 | 
			
		||||
$colorItemTreeSelectedBg: $colorSelectedBg;
 | 
			
		||||
$colorItemTreeSelectedFg: $colorItemTreeHoverFg;
 | 
			
		||||
$filterItemTreeSelected: $filterHov;
 | 
			
		||||
 
 | 
			
		||||
@@ -394,7 +394,7 @@ $colorItemTreeHoverBg: rgba(#fff, 0.03);
 | 
			
		||||
$colorItemTreeHoverFg: #fff;
 | 
			
		||||
$colorItemTreeIcon: $colorKey; // Used
 | 
			
		||||
$colorItemTreeIconHover: $colorItemTreeIcon; // Used
 | 
			
		||||
$colorItemTreeFg: $colorBodyFg;
 | 
			
		||||
$colorItemTreeFg: $colorA;
 | 
			
		||||
$colorItemTreeSelectedBg: $colorSelectedBg;
 | 
			
		||||
$colorItemTreeSelectedFg: $colorItemTreeHoverFg;
 | 
			
		||||
$filterItemTreeSelected: $filterHov;
 | 
			
		||||
 
 | 
			
		||||
@@ -94,7 +94,7 @@ $messageListIconD: 32px;
 | 
			
		||||
$tableResizeColHitareaD: 6px;
 | 
			
		||||
/*************** Misc */
 | 
			
		||||
$drawingObjBorderW: 3px;
 | 
			
		||||
 | 
			
		||||
$tagBorderRadius: 3px;
 | 
			
		||||
/************************** MOBILE */
 | 
			
		||||
$mobileMenuIconD: 24px; // Used
 | 
			
		||||
$mobileTreeItemH: 35px; // Used
 | 
			
		||||
 
 | 
			
		||||
@@ -270,9 +270,11 @@ button {
 | 
			
		||||
    flex: 0 0 auto;
 | 
			
		||||
    width: $d;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    visibility: hidden;
 | 
			
		||||
 | 
			
		||||
    &.is-enabled {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        visibility: visible;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
            color: $colorDisclosureCtrlHov;
 | 
			
		||||
@@ -403,16 +405,18 @@ textarea {
 | 
			
		||||
 | 
			
		||||
    &--autocomplete {
 | 
			
		||||
        &__wrapper {
 | 
			
		||||
            display: inline-flex;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-direction: row;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &__input {
 | 
			
		||||
            min-width: 100px;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
 | 
			
		||||
            // Fend off from afford-arrow
 | 
			
		||||
            min-height: 2em;
 | 
			
		||||
            padding-right: 2.5em !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -435,7 +439,10 @@ textarea {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &__afford-arrow {
 | 
			
		||||
            $p: 2px;
 | 
			
		||||
            font-size: 0.8em;
 | 
			
		||||
            padding-bottom: $p;
 | 
			
		||||
            padding-top: $p;
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            right: 2px;
 | 
			
		||||
            z-index: 2;
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,6 @@
 | 
			
		||||
@import "../ui/inspector/elements.scss";
 | 
			
		||||
@import "../ui/inspector/inspector.scss";
 | 
			
		||||
@import "../ui/inspector/location.scss";
 | 
			
		||||
@import "../ui/inspector/annotations/annotation-inspector.scss";
 | 
			
		||||
@import "../ui/layout/app-logo.scss";
 | 
			
		||||
@import "../ui/layout/create-button.scss";
 | 
			
		||||
@import "../ui/layout/layout.scss";
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,8 @@
 | 
			
		||||
<ul
 | 
			
		||||
    v-if="orderedPath.length"
 | 
			
		||||
    class="c-location"
 | 
			
		||||
    :aria-label="`${domainObject.name} Breadcrumb`"
 | 
			
		||||
    role="navigation"
 | 
			
		||||
>
 | 
			
		||||
    <li
 | 
			
		||||
        v-for="pathObject in orderedPath"
 | 
			
		||||
@@ -34,6 +36,7 @@
 | 
			
		||||
            :domain-object="pathObject.domainObject"
 | 
			
		||||
            :object-path="pathObject.objectPath"
 | 
			
		||||
            :read-only="readOnly"
 | 
			
		||||
            :navigate-to-path="navigateToPath(pathObject.objectPath)"
 | 
			
		||||
        />
 | 
			
		||||
    </li>
 | 
			
		||||
</ul>
 | 
			
		||||
@@ -110,6 +113,18 @@ export default {
 | 
			
		||||
                this.orderedPath = pathWithDomainObject.slice(1, pathWithDomainObject.length - 1).reverse();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        /**
 | 
			
		||||
         * Generate the hash url for the given object path, removing the '/ROOT' prefix if present.
 | 
			
		||||
         * @param {import('../../api/objects/ObjectAPI').DomainObject[]} objectPath
 | 
			
		||||
         */
 | 
			
		||||
        navigateToPath(objectPath) {
 | 
			
		||||
            /** @type {String} */
 | 
			
		||||
            const path = `/browse/${this.openmct.objects.getRelativePath(objectPath)}`;
 | 
			
		||||
 | 
			
		||||
            return path.replace('ROOT/', '');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,11 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div class="c-tag-applier">
 | 
			
		||||
<div class="c-tag-applier has-tag-applier">
 | 
			
		||||
    <TagSelection
 | 
			
		||||
        v-for="(addedTag, index) in addedTags"
 | 
			
		||||
        :key="index"
 | 
			
		||||
        :class="{ 'w-tag-wrapper--tag-selector' : addedTag.newTag }"
 | 
			
		||||
        :selected-tag="addedTag.newTag ? null : addedTag"
 | 
			
		||||
        :new-tag="addedTag.newTag"
 | 
			
		||||
        :added-tags="addedTags"
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,8 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div class="c-tag__parent">
 | 
			
		||||
    <div class="c-tag_selection">
 | 
			
		||||
<div class="w-tag-wrapper">
 | 
			
		||||
    <template v-if="newTag">
 | 
			
		||||
        <AutoCompleteField
 | 
			
		||||
            v-if="newTag"
 | 
			
		||||
            ref="tagSelection"
 | 
			
		||||
@@ -32,8 +32,9 @@
 | 
			
		||||
            :item-css-class="'icon-circle'"
 | 
			
		||||
            @onChange="tagSelected"
 | 
			
		||||
        />
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-else>
 | 
			
		||||
        <div
 | 
			
		||||
            v-else
 | 
			
		||||
            class="c-tag"
 | 
			
		||||
            :class="{'c-tag-edit': !readOnly}"
 | 
			
		||||
            :style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
 | 
			
		||||
@@ -48,7 +49,7 @@
 | 
			
		||||
                @click="removeTag"
 | 
			
		||||
            ></button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    </template>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,30 @@
 | 
			
		||||
@mixin tagHolder() {
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
 | 
			
		||||
  > * {
 | 
			
		||||
    $m: $interiorMarginSm;
 | 
			
		||||
 | 
			
		||||
    margin: 0 $m $m 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/******************************* TAGS */
 | 
			
		||||
.c-tag {
 | 
			
		||||
  border-radius: 10px; //TODO: convert to theme constant
 | 
			
		||||
  border-radius: $tagBorderRadius;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  padding: 1px 10px; //TODO: convert to theme constant
 | 
			
		||||
 | 
			
		||||
  > * + * {
 | 
			
		||||
    margin-left: $interiorMargin;
 | 
			
		||||
  }
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  padding: 1px 6px; //TODO: convert to theme constant
 | 
			
		||||
  transition: $transIn;
 | 
			
		||||
 | 
			
		||||
  &__remove-btn {
 | 
			
		||||
    color: inherit !important;
 | 
			
		||||
    display: none;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    padding: 1px !important;
 | 
			
		||||
    padding: 0; // Overrides default <button> padding
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 2px;
 | 
			
		||||
    transition: $transIn;
 | 
			
		||||
    width: 0;
 | 
			
		||||
 | 
			
		||||
@@ -28,28 +39,47 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/******************************* TAG EDITOR */
 | 
			
		||||
.c-tag-applier {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
.c-tag-holder {
 | 
			
		||||
  @include tagHolder;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  > * + * {
 | 
			
		||||
    margin-left: $interiorMargin;
 | 
			
		||||
.w-tag-wrapper {
 | 
			
		||||
    $m: $interiorMarginSm;
 | 
			
		||||
 | 
			
		||||
    margin: 0 $m $m 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/******************************* TAGS IN INSPECTOR / TAG SELECTION & APPLICATION */
 | 
			
		||||
.c-tag-applier {
 | 
			
		||||
  $tagApplierPadding: 3px 6px;
 | 
			
		||||
  @include tagHolder;
 | 
			
		||||
  grid-column: 1 / 3;
 | 
			
		||||
 | 
			
		||||
  &__tags {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__add-btn {
 | 
			
		||||
    border-radius: $tagBorderRadius;
 | 
			
		||||
    padding: 3px 10px 3px 4px;
 | 
			
		||||
 | 
			
		||||
    &:before { font-size: 0.9em; }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .c-tag {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    padding-right: 3px !important;
 | 
			
		||||
    padding: $tagApplierPadding;
 | 
			
		||||
 | 
			
		||||
    &__remove-btn {
 | 
			
		||||
      display: block;
 | 
			
		||||
    > * + * { margin-left: $interiorMarginSm; }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .c-tag-selection {
 | 
			
		||||
    .c-input--autocomplete__input {
 | 
			
		||||
      min-height: auto !important;
 | 
			
		||||
      padding: $tagApplierPadding;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -62,10 +92,18 @@
 | 
			
		||||
.has-tag-applier {
 | 
			
		||||
  // Apply this class to all components that should trigger tag removal btn on hover
 | 
			
		||||
   &:hover {
 | 
			
		||||
     .c-tag__remove-btn {
 | 
			
		||||
       width: 1.1em;
 | 
			
		||||
       opacity: 0.7;
 | 
			
		||||
     .c-tag {
 | 
			
		||||
       padding-right: 17px !important;
 | 
			
		||||
       transition: $transOut;
 | 
			
		||||
     }
 | 
			
		||||
 | 
			
		||||
     .c-tag__remove-btn {
 | 
			
		||||
       //display: block;
 | 
			
		||||
       //margin-left: $interiorMarginSm;
 | 
			
		||||
       width: 1em;
 | 
			
		||||
       opacity: 0.8;
 | 
			
		||||
       transition: $transOut;
 | 
			
		||||
       //transition-delay: 250ms;
 | 
			
		||||
     }
 | 
			
		||||
   }
 | 
			
		||||
 }
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
    @drop="emitDropEvent"
 | 
			
		||||
>
 | 
			
		||||
    <div
 | 
			
		||||
        class="c-tree__item c-elements-pool__item"
 | 
			
		||||
        class="c-tree__item c-elements-pool__item js-elements-pool__item"
 | 
			
		||||
        :class="{
 | 
			
		||||
            'is-context-clicked': contextClickActive,
 | 
			
		||||
            'hover': hover,
 | 
			
		||||
 
 | 
			
		||||
@@ -109,6 +109,8 @@ import StylesInspectorView from "@/ui/inspector/styles/StylesInspectorView.vue";
 | 
			
		||||
import SavedStylesInspectorView from "@/ui/inspector/styles/SavedStylesInspectorView.vue";
 | 
			
		||||
import AnnotationsInspectorView from "./annotations/AnnotationsInspectorView.vue";
 | 
			
		||||
 | 
			
		||||
const OVERLAY_PLOT_TYPE = "telemetry.plot.overlay";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        StylesInspectorView,
 | 
			
		||||
@@ -189,12 +191,12 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
        refreshComposition(selection) {
 | 
			
		||||
            if (selection.length > 0 && selection[0].length > 0) {
 | 
			
		||||
                let parentObject = selection[0][0].context.item;
 | 
			
		||||
                const parentObject = selection[0][0].context.item;
 | 
			
		||||
 | 
			
		||||
                this.hasComposition = Boolean(
 | 
			
		||||
                    parentObject && this.openmct.composition.get(parentObject)
 | 
			
		||||
                );
 | 
			
		||||
                this.isOverlayPlot = selection[0][0].context.item.type === 'telemetry.plot.overlay';
 | 
			
		||||
                this.isOverlayPlot = parentObject?.type === OVERLAY_PLOT_TYPE;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        refreshTabs(selection) {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div></div>
 | 
			
		||||
<div aria-label="Inspector Views"></div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@
 | 
			
		||||
        <ul
 | 
			
		||||
            v-if="hasElements"
 | 
			
		||||
            id="inspector-elements-tree"
 | 
			
		||||
            class="c-tree c-elements-pool__tree"
 | 
			
		||||
            class="c-tree c-elements-pool__tree js-elements-pool__tree"
 | 
			
		||||
        >
 | 
			
		||||
            <div class="c-elements-pool__instructions"> Select and drag an element to move it into a different axis. </div>
 | 
			
		||||
            <element-item-group
 | 
			
		||||
@@ -145,7 +145,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
            this.unlistenComposition();
 | 
			
		||||
 | 
			
		||||
            if (this.parentObject) {
 | 
			
		||||
            if (this.parentObject && this.parentObject.type === 'telemetry.plot.overlay') {
 | 
			
		||||
                this.setYAxisIds();
 | 
			
		||||
                this.composition = this.openmct.composition.get(this.parentObject);
 | 
			
		||||
 | 
			
		||||
@@ -175,6 +175,7 @@ export default {
 | 
			
		||||
        setYAxisIds() {
 | 
			
		||||
            const configId = this.openmct.objects.makeKeyString(this.parentObject.identifier);
 | 
			
		||||
            this.config = configStore.get(configId);
 | 
			
		||||
            this.yAxes = [];
 | 
			
		||||
            this.yAxes.push({
 | 
			
		||||
                id: this.config.yAxis.id,
 | 
			
		||||
                elements: this.parentObject.configuration.series.filter(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,83 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, 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.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div class="c-annotation__row">
 | 
			
		||||
    <textarea
 | 
			
		||||
        v-model="contentModel"
 | 
			
		||||
        class="c-annotation__text_area"
 | 
			
		||||
        type="text"
 | 
			
		||||
    ></textarea>
 | 
			
		||||
    <div>
 | 
			
		||||
        <span>{{ modifiedOnDate }}</span>
 | 
			
		||||
        <span>{{ modifiedOnTime }}</span>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Moment from 'moment';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    props: {
 | 
			
		||||
        annotation: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            default() {
 | 
			
		||||
                return {};
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        contentModel: {
 | 
			
		||||
            get() {
 | 
			
		||||
                return this.annotation.contentText;
 | 
			
		||||
            },
 | 
			
		||||
            set(contentText) {
 | 
			
		||||
                console.debug(`Set tag called with ${contentText}`);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        modifiedOnDate() {
 | 
			
		||||
            return this.formatTime(this.annotation.modified, 'YYYY-MM-DD');
 | 
			
		||||
        },
 | 
			
		||||
        modifiedOnTime() {
 | 
			
		||||
            return this.formatTime(this.annotation.modified, 'HH:mm:ss');
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        getAvailableTagByID(tagID) {
 | 
			
		||||
            return this.openmct.annotation.getAvailableTags().find(tag => {
 | 
			
		||||
                return tag.id === tagID;
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        formatTime(unixTime, timeFormat) {
 | 
			
		||||
            return Moment.utc(unixTime).format(timeFormat);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div
 | 
			
		||||
    class="c-inspector__properties c-inspect-properties has-tag-applier"
 | 
			
		||||
    class="c-inspector__properties c-inspect-properties"
 | 
			
		||||
    aria-label="Tags Inspector"
 | 
			
		||||
>
 | 
			
		||||
    <div
 | 
			
		||||
@@ -111,25 +111,31 @@ export default {
 | 
			
		||||
            return this?.selection?.[0]?.[0]?.context?.item;
 | 
			
		||||
        },
 | 
			
		||||
        targetDetails() {
 | 
			
		||||
            return this?.selection?.[0]?.[1]?.context?.targetDetails ?? {};
 | 
			
		||||
            return this?.selection?.[0]?.[0]?.context?.targetDetails ?? {};
 | 
			
		||||
        },
 | 
			
		||||
        shouldShowTagsEditor() {
 | 
			
		||||
            return Object.keys(this.targetDetails).length > 0;
 | 
			
		||||
            const showingTagsEditor = Object.keys(this.targetDetails).length > 0;
 | 
			
		||||
 | 
			
		||||
            if (showingTagsEditor) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        },
 | 
			
		||||
        targetDomainObjects() {
 | 
			
		||||
            return this?.selection?.[0]?.[1]?.context?.targetDomainObjects ?? {};
 | 
			
		||||
            return this?.selection?.[0]?.[0]?.context?.targetDomainObjects ?? {};
 | 
			
		||||
        },
 | 
			
		||||
        selectedAnnotations() {
 | 
			
		||||
            return this?.selection?.[0]?.[1]?.context?.annotations;
 | 
			
		||||
            return this?.selection?.[0]?.[0]?.context?.annotations;
 | 
			
		||||
        },
 | 
			
		||||
        annotationType() {
 | 
			
		||||
            return this?.selection?.[0]?.[1]?.context?.annotationType;
 | 
			
		||||
            return this?.selection?.[0]?.[0]?.context?.annotationType;
 | 
			
		||||
        },
 | 
			
		||||
        annotationFilter() {
 | 
			
		||||
            return this?.selection?.[0]?.[1]?.context?.annotationFilter;
 | 
			
		||||
            return this?.selection?.[0]?.[0]?.context?.annotationFilter;
 | 
			
		||||
        },
 | 
			
		||||
        onAnnotationChange() {
 | 
			
		||||
            return this?.selection?.[0]?.[1]?.context?.onAnnotationChange;
 | 
			
		||||
            return this?.selection?.[0]?.[0]?.context?.onAnnotationChange;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    async mounted() {
 | 
			
		||||
@@ -195,6 +201,7 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        async loadAnnotationForTargetObject(target) {
 | 
			
		||||
            console.debug(`📝 Loading annotations for target`, target);
 | 
			
		||||
            const targetID = this.openmct.objects.makeKeyString(target.identifier);
 | 
			
		||||
            const allAnnotationsForTarget = await this.openmct.annotation.getAnnotations(target.identifier);
 | 
			
		||||
            const filteredAnnotationsForSelection = allAnnotationsForTarget.filter(annotation => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
.c-inspect-annotations {
 | 
			
		||||
    > * + * {
 | 
			
		||||
        margin-top: $interiorMargin;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__content{
 | 
			
		||||
        > * + * {
 | 
			
		||||
            margin-top: $interiorMargin;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__content {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -39,7 +39,6 @@
 | 
			
		||||
 | 
			
		||||
    &__group {
 | 
			
		||||
        flex: 1 1 auto;
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
        margin-top: $interiorMarginLg;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import CreateAction from '@/plugins/formActions/CreateAction';
 | 
			
		||||
import objectUtils from 'objectUtils';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
@@ -74,23 +73,9 @@ export default {
 | 
			
		||||
            this.openmct.menus.showSuperMenu(x, y, this.sortedItems, menuOptions);
 | 
			
		||||
        },
 | 
			
		||||
        create(key) {
 | 
			
		||||
            // Hack for support.  TODO: rewrite create action.
 | 
			
		||||
            // 1. Get contextual object from navigation
 | 
			
		||||
            // 2. Get legacy type from legacy api
 | 
			
		||||
            // 3. Instantiate create action with type, parent, context
 | 
			
		||||
            // 4. perform action.
 | 
			
		||||
            return this.openmct.objects.get(this.openmct.router.path[0].identifier)
 | 
			
		||||
                .then((currentObject) => {
 | 
			
		||||
                    const createAction = new CreateAction(this.openmct, key, currentObject);
 | 
			
		||||
            const createAction = new CreateAction(this.openmct, key, this.openmct.router.path[0]);
 | 
			
		||||
 | 
			
		||||
                    createAction.invoke();
 | 
			
		||||
                });
 | 
			
		||||
        },
 | 
			
		||||
        convertToLegacy(domainObject) {
 | 
			
		||||
            let keyString = objectUtils.makeKeyString(domainObject.identifier);
 | 
			
		||||
            let oldModel = objectUtils.toOldFormat(domainObject);
 | 
			
		||||
 | 
			
		||||
            return this.openmct.$injector.get('instantiate')(oldModel, keyString);
 | 
			
		||||
            createAction.invoke();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -79,9 +79,7 @@
 | 
			
		||||
            <multipane
 | 
			
		||||
                type="vertical"
 | 
			
		||||
            >
 | 
			
		||||
                <pane
 | 
			
		||||
                    id="tree-pane"
 | 
			
		||||
                >
 | 
			
		||||
                <pane>
 | 
			
		||||
                    <mct-tree
 | 
			
		||||
                        ref="mctTree"
 | 
			
		||||
                        :sync-tree-navigation="triggerSync"
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,7 @@
 | 
			
		||||
        ref="mainTree"
 | 
			
		||||
        class="c-tree-and-search__tree c-tree"
 | 
			
		||||
        role="tree"
 | 
			
		||||
        :aria-label="getAriaLabel"
 | 
			
		||||
        aria-expanded="true"
 | 
			
		||||
    >
 | 
			
		||||
 | 
			
		||||
@@ -192,6 +193,9 @@ export default {
 | 
			
		||||
        focusedItems() {
 | 
			
		||||
            return this.activeSearch ? this.searchResultItems : this.treeItems;
 | 
			
		||||
        },
 | 
			
		||||
        getAriaLabel() {
 | 
			
		||||
            return this.isSelectorTree ? "Create Modal Tree" : "Main Tree";
 | 
			
		||||
        },
 | 
			
		||||
        pageThreshold() {
 | 
			
		||||
            return Math.ceil(this.mainTreeHeight / this.itemHeight) + ITEM_BUFFER;
 | 
			
		||||
        },
 | 
			
		||||
@@ -311,7 +315,7 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        targetedPathAnimationEnd() {
 | 
			
		||||
            this.targetedPath = undefined;
 | 
			
		||||
            this.targetedPath = null;
 | 
			
		||||
        },
 | 
			
		||||
        treeItemSelection(item) {
 | 
			
		||||
            this.selectedItem = item;
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
  align-items: flex-start;
 | 
			
		||||
 | 
			
		||||
  > * + * {
 | 
			
		||||
      margin-left: $interiorMargin;
 | 
			
		||||
      margin-left: $interiorMarginSm;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  + .c-recentobjects-listitem {
 | 
			
		||||
@@ -58,7 +58,7 @@
 | 
			
		||||
 | 
			
		||||
  &__type-icon {
 | 
			
		||||
      color: $colorItemTreeIcon;
 | 
			
		||||
      font-size: 2.2em;
 | 
			
		||||
      font-size: 1.25em;
 | 
			
		||||
 | 
			
		||||
      // TEMP: uses object-label component, hide label part
 | 
			
		||||
      .c-object-label__name {
 | 
			
		||||
@@ -72,6 +72,7 @@
 | 
			
		||||
 | 
			
		||||
  &__body {
 | 
			
		||||
      flex: 1 1 auto;
 | 
			
		||||
      padding-top: 2px; // Align with type icon
 | 
			
		||||
 | 
			
		||||
      > * + * {
 | 
			
		||||
          margin-top: $interiorMarginSm;
 | 
			
		||||
@@ -89,7 +90,6 @@
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__tags {
 | 
			
		||||
@@ -102,7 +102,7 @@
 | 
			
		||||
 | 
			
		||||
  &__title {
 | 
			
		||||
      border-radius: $basicCr;
 | 
			
		||||
      color: pullForward($colorBodyFg, 30%);
 | 
			
		||||
      color: $colorItemTreeFg;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      padding: $interiorMarginSm;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -150,16 +150,11 @@ export default {
 | 
			
		||||
            });
 | 
			
		||||
            const selection =
 | 
			
		||||
                    [
 | 
			
		||||
                        {
 | 
			
		||||
                            element: this.openmct.layout.$refs.browseObject.$el,
 | 
			
		||||
                            context: {
 | 
			
		||||
                                item: this.result
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            element: this.$el,
 | 
			
		||||
                            context: {
 | 
			
		||||
                                type: 'plot-points-selection',
 | 
			
		||||
                                item: this.result.targetModels[0],
 | 
			
		||||
                                type: 'plot-annotation-search-result',
 | 
			
		||||
                                targetDetails,
 | 
			
		||||
                                targetDomainObjects,
 | 
			
		||||
                                annotations: [this.result],
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
        targetedPath: {
 | 
			
		||||
            type: String,
 | 
			
		||||
            required: true
 | 
			
		||||
            default: null
 | 
			
		||||
        },
 | 
			
		||||
        selectedItem: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user