Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
7c82aeb4eb | |||
78a3de78b2 | |||
cff2ef7992 | |||
34f25a3e16 | |||
0751d0fed4 | |||
d49cd2510a | |||
31ff6f228a | |||
242fa3cd25 | |||
02fc1690a9 | |||
25eccbed2c | |||
ce31893797 | |||
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 |
@ -89,7 +89,7 @@ Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshot
|
||||
#### Open MCT's implementation
|
||||
|
||||
- Our Snapshot tests receive a `@snapshot` tag.
|
||||
- Snapshots need to be executed within the official Playwright container to ensure we're using the exact rendering platform in CI and locally.
|
||||
- Snapshots need to be executed within the official Playwright container to ensure we're using the exact rendering platform in CI and locally. To do a valid comparison locally:
|
||||
|
||||
```sh
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:[GET THIS VERSION FROM OUR CIRCLECI CONFIG FILE]-focal /bin/bash
|
||||
@ -97,9 +97,24 @@ npm install
|
||||
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
|
||||
```
|
||||
|
||||
### (WIP) Updating Snapshots
|
||||
### Updating Snapshots
|
||||
|
||||
When the `@snapshot` tests fail, they will need to be evaluated to see if the failure is an acceptable change or
|
||||
When the `@snapshot` tests fail, they will need to be evaluated to determine if the failure is an acceptable and desireable or an unintended regression.
|
||||
|
||||
To compare a snapshot, run a test and open the html report with the 'Expected' vs 'Actual' screenshot. If the actual screenshot is preferred, then the source-controlled 'Expected' snapshots will need to be updated with the following scripts.
|
||||
|
||||
MacOS
|
||||
```
|
||||
npm run test:e2e:updatesnapshots
|
||||
```
|
||||
|
||||
Linux/CI
|
||||
```sh
|
||||
// Replace {X.X.X} with the current Playwright version
|
||||
// from our package.json or circleCI configuration file
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
|
||||
npm install
|
||||
npm run test:e2e:updatesnapshots
|
||||
|
||||
## Performance Testing
|
||||
|
||||
|
@ -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
@ -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
|
||||
|
@ -32,7 +32,7 @@ test.use({
|
||||
}
|
||||
});
|
||||
|
||||
test.fixme('ExportAsJSON', () => {
|
||||
test.describe('Autoscale', () => {
|
||||
test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
@ -47,16 +47,32 @@ test.fixme('ExportAsJSON', () => {
|
||||
|
||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
||||
|
||||
// enter edit mode
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await turnOffAutoscale(page);
|
||||
|
||||
// Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
|
||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
||||
await setUserDefinedMinAndMax(page, '-2', '2');
|
||||
|
||||
// save
|
||||
await page.click('button[title="Save"]');
|
||||
await Promise.all([
|
||||
page.locator('li[title = "Save and Finish Editing"]').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// Make sure that after turning off autoscale, the user entered range values are reflexted in the ticks.
|
||||
await testYTicks(page, ['-2.00', '-1.50', '-1.00', '-0.50', '0.00', '0.50', '1.00', '1.50', '2.00']);
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
|
||||
await canvas.hover({trial: true});
|
||||
|
||||
expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' });
|
||||
expect.soft(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' });
|
||||
|
||||
//Alt Drag Start
|
||||
await page.keyboard.down('Alt');
|
||||
@ -76,11 +92,12 @@ test.fixme('ExportAsJSON', () => {
|
||||
await page.keyboard.up('Alt');
|
||||
|
||||
// Ensure the drag worked.
|
||||
await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']);
|
||||
await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00', '3.50']);
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
await canvas.hover({trial: true});
|
||||
|
||||
expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned.png', { animations: 'disabled' });
|
||||
expect.soft(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned.png', { animations: 'disabled' });
|
||||
});
|
||||
});
|
||||
|
||||
@ -152,22 +169,25 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function turnOffAutoscale(page) {
|
||||
// enter edit mode
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
|
||||
// uncheck autoscale
|
||||
await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck();
|
||||
}
|
||||
|
||||
// save
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} min
|
||||
* @param {string} max
|
||||
*/
|
||||
async function setUserDefinedMinAndMax(page, min, max) {
|
||||
// set minimum value
|
||||
const minRangeInput = page.getByRole('listitem').filter({ hasText: 'Minimum Value' }).locator('input[type="number"]');
|
||||
await minRangeInput.click();
|
||||
await minRangeInput.fill(min);
|
||||
|
||||
// set maximum value
|
||||
const maxRangeInput = page.getByRole('listitem').filter({ hasText: 'Maximum Value' }).locator('input[type="number"]');
|
||||
await maxRangeInput.click();
|
||||
await maxRangeInput.fill(max);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -179,7 +199,7 @@ async function testYTicks(page, values) {
|
||||
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
|
||||
|
||||
for (let i = 0, l = values.length; i < l; i += 1) {
|
||||
promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
|
||||
promises.push(expect.soft(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,8 +29,11 @@ const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Overlay Plot', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test('Plot legend color is in sync with plot series color', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Overlay Plot"
|
||||
});
|
||||
@ -56,35 +59,30 @@ test.describe('Overlay Plot', () => {
|
||||
|
||||
expect(color).toBe('rgb(255, 166, 61)');
|
||||
});
|
||||
|
||||
test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Overlay Plot"
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const swgA = await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg a',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const swgB = await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg b',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const swgC = await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg c',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const swgD = await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg d',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const swgE = await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg e',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
@ -92,33 +90,111 @@ test.describe('Overlay Plot', () => {
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
// Expand the elements pool vertically
|
||||
await page.locator('.l-pane__handle').nth(2).hover({ trial: true });
|
||||
await page.locator('.l-pane.l-pane--vertical-handle-before', {
|
||||
hasText: 'Elements'
|
||||
}).locator('.l-pane__handle').hover();
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(0, 100);
|
||||
await page.mouse.up();
|
||||
|
||||
// Drag swg a, c, e into Y Axis 2
|
||||
await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
const yAxis1PropertyGroup = page.locator('[aria-label="Y Axis Properties"]');
|
||||
const yAxis2PropertyGroup = page.locator('[aria-label="Y Axis 2 Properties"]');
|
||||
const yAxis3PropertyGroup = page.locator('[aria-label="Y Axis 3 Properties"]');
|
||||
|
||||
// Drag swg b into Y Axis 3
|
||||
await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]'));
|
||||
// Assert that Y Axis 1 property group is visible only
|
||||
await expect(yAxis1PropertyGroup).toBeVisible();
|
||||
await expect(yAxis2PropertyGroup).toBeHidden();
|
||||
await expect(yAxis3PropertyGroup).toBeHidden();
|
||||
|
||||
// Drag swg a, c, e into Y Axis 2
|
||||
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
await page.locator(`#inspector-elements-tree >> text=${swgC.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
await page.locator(`#inspector-elements-tree >> text=${swgE.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
|
||||
// Assert that Y Axis 1 and Y Axis 2 property groups are visible only
|
||||
await expect(yAxis1PropertyGroup).toBeVisible();
|
||||
await expect(yAxis2PropertyGroup).toBeVisible();
|
||||
await expect(yAxis3PropertyGroup).toBeHidden();
|
||||
|
||||
const yAxis1Group = page.getByLabel("Y Axis 1");
|
||||
const yAxis2Group = page.getByLabel("Y Axis 2");
|
||||
const yAxis3Group = page.getByLabel("Y Axis 3");
|
||||
|
||||
// Drag swg b into Y Axis 3
|
||||
await page.locator(`#inspector-elements-tree >> text=${swgB.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]'));
|
||||
|
||||
// Assert that all Y Axis property groups are visible
|
||||
await expect(yAxis1PropertyGroup).toBeVisible();
|
||||
await expect(yAxis2PropertyGroup).toBeVisible();
|
||||
await expect(yAxis3PropertyGroup).toBeVisible();
|
||||
|
||||
// Verify that the elements are in the correct buckets and in the correct order
|
||||
expect(yAxis1Group.getByRole('listitem', { name: 'swg d' })).toBeTruthy();
|
||||
expect(yAxis1Group.getByRole('listitem').nth(0).getByText('swg d')).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: 'swg e' })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(0).getByText('swg e')).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: 'swg c' })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(1).getByText('swg c')).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: 'swg a' })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(2).getByText('swg a')).toBeTruthy();
|
||||
expect(yAxis3Group.getByRole('listitem', { name: 'swg b' })).toBeTruthy();
|
||||
expect(yAxis3Group.getByRole('listitem').nth(0).getByText('swg b')).toBeTruthy();
|
||||
expect(yAxis1Group.getByRole('listitem', { name: swgD.name })).toBeTruthy();
|
||||
expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: swgE.name })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(0).getByText(swgE.name)).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: swgC.name })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(1).getByText(swgC.name)).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: swgA.name })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(2).getByText(swgA.name)).toBeTruthy();
|
||||
expect(yAxis3Group.getByRole('listitem', { name: swgB.name })).toBeTruthy();
|
||||
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({ page }) => {
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Overlay Plot"
|
||||
});
|
||||
|
||||
const swgA = await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
|
||||
await page.locator('.js-overlay canvas').nth(1);
|
||||
const plotPixelSize = await getCanvasPixelsWithData(page);
|
||||
expect(plotPixelSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getCanvasPixelsWithData(page) {
|
||||
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
|
||||
|
||||
await page.evaluate(() => {
|
||||
// The document canvas is where the plot points and lines are drawn.
|
||||
// The only way to access the canvas is using document (using page.evaluate)
|
||||
let data;
|
||||
let canvas;
|
||||
let ctx;
|
||||
canvas = document.querySelector('.js-overlay canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const imageDataValues = Object.values(data);
|
||||
let plotPixels = [];
|
||||
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
|
||||
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
|
||||
for (let i = 0; i < imageDataValues.length;) {
|
||||
if (imageDataValues[i] > 0) {
|
||||
plotPixels.push({
|
||||
startIndex: i,
|
||||
endIndex: i + 3,
|
||||
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
|
||||
});
|
||||
}
|
||||
|
||||
i = i + 4;
|
||||
|
||||
}
|
||||
|
||||
window.getCanvasValue(plotPixels.length);
|
||||
});
|
||||
|
||||
return getTelemValuePromise;
|
||||
}
|
||||
|
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];
|
||||
}
|
||||
}
|
||||
|
@ -275,7 +275,7 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
|
||||
local: Math.floor(timestamp / delay) * delay,
|
||||
url,
|
||||
sunOrientation: getCompassValues(0, 360),
|
||||
cameraPan: getCompassValues(0, 360),
|
||||
cameraAzimuth: getCompassValues(0, 360),
|
||||
heading: getCompassValues(0, 360),
|
||||
transformations: navCamTransformations,
|
||||
imageDownloadName
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.6-SNAPSHOT",
|
||||
"version": "2.1.6",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
|
@ -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"
|
||||
|
@ -180,6 +180,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
let beforeStartOfBounds;
|
||||
let afterEndOfBounds;
|
||||
let added = [];
|
||||
let addedIndices = [];
|
||||
|
||||
// loop through, sort and dedupe
|
||||
for (let datum of data) {
|
||||
@ -212,6 +213,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
let index = endIndex || startIndex;
|
||||
|
||||
this.boundedTelemetry.splice(index, 0, datum);
|
||||
addedIndices.push(index);
|
||||
added.push(datum);
|
||||
}
|
||||
|
||||
@ -230,7 +232,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this.emit('add', this.boundedTelemetry);
|
||||
}
|
||||
} else {
|
||||
this.emit('add', added);
|
||||
this.emit('add', added, addedIndices);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -330,7 +332,8 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this.boundedTelemetry = added;
|
||||
}
|
||||
|
||||
this.emit('add', added);
|
||||
// Assumption is that added will be of length 1 here, so just send the last index of the boundedTelemetry in the add event
|
||||
this.emit('add', added, [this.boundedTelemetry.length]);
|
||||
}
|
||||
} else {
|
||||
// user bounds change, reset
|
||||
|
@ -32,14 +32,18 @@ class IndependentTimeContext extends TimeContext {
|
||||
this.openmct = openmct;
|
||||
this.unlisteners = [];
|
||||
this.globalTimeContext = globalTimeContext;
|
||||
this.upstreamTimeContext = undefined;
|
||||
// We always start with the global time context.
|
||||
// This upstream context will be undefined when an independent time context is added later.
|
||||
this.upstreamTimeContext = this.globalTimeContext;
|
||||
this.objectPath = objectPath;
|
||||
this.refreshContext = this.refreshContext.bind(this);
|
||||
this.resetContext = this.resetContext.bind(this);
|
||||
this.removeIndependentContext = this.removeIndependentContext.bind(this);
|
||||
|
||||
this.refreshContext();
|
||||
|
||||
this.globalTimeContext.on('refreshContext', this.refreshContext);
|
||||
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
|
||||
}
|
||||
|
||||
bounds(newBounds) {
|
||||
@ -202,16 +206,16 @@ 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) {
|
||||
// If a view has an independent context, don't return an upstream context
|
||||
// Be aware that when a new independent time context is created, we assign the global context as default
|
||||
if (this.hasOwnContext()) {
|
||||
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
|
||||
// we're only interested in parents, not self, so index > 0
|
||||
const itemContext = this.globalTimeContext.independentContexts.get(key);
|
||||
if (index > 0 && itemContext && itemContext.hasOwnContext()) {
|
||||
//upstream time context
|
||||
@ -225,6 +229,43 @@ class IndependentTimeContext extends TimeContext {
|
||||
|
||||
return timeContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time context of a view to follow any upstream time contexts as necessary (defaulting to the global context)
|
||||
* This needs to be separate from refreshContext
|
||||
*/
|
||||
removeIndependentContext(viewKey) {
|
||||
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
|
||||
if (viewKey && key === viewKey) {
|
||||
//this is necessary as the upstream context gets reassigned after this
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
let timeContext = this.globalTimeContext;
|
||||
|
||||
this.objectPath.some((item, index) => {
|
||||
const objectKey = this.openmct.objects.makeKeyString(item.identifier);
|
||||
// we're only interested in any parents, not self, so index > 0
|
||||
const itemContext = this.globalTimeContext.independentContexts.get(objectKey);
|
||||
if (index > 0 && itemContext && itemContext.hasOwnContext()) {
|
||||
//upstream time context
|
||||
timeContext = itemContext;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.upstreamTimeContext = timeContext;
|
||||
|
||||
this.followTimeContext();
|
||||
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.bounds());
|
||||
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
|
||||
this.globalTimeContext.emit('refreshContext', viewKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default IndependentTimeContext;
|
||||
|
@ -149,7 +149,7 @@ class TimeAPI extends GlobalTimeContext {
|
||||
|
||||
return () => {
|
||||
//follow any upstream time context
|
||||
this.emit('refreshContext');
|
||||
this.emit('removeOwnContext', key);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -93,21 +93,82 @@ 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'
|
||||
}
|
||||
}]);
|
||||
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", () => {
|
||||
const domainObjectKey2 = `${domainObjectKey}-2`;
|
||||
const domainObjectKey3 = `${domainObjectKey}-3`;
|
||||
let timeContext = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: domainObjectKey
|
||||
}
|
||||
}]);
|
||||
let destroyTimeContext = api.addIndependentContext('blah', independentBounds);
|
||||
let timeContext2 = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: domainObjectKey2
|
||||
}
|
||||
}]);
|
||||
let timeContext3 = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: domainObjectKey3
|
||||
}
|
||||
}]);
|
||||
// all bounds follow global time context
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
expect(timeContext2.bounds()).toEqual(bounds);
|
||||
expect(timeContext3.bounds()).toEqual(bounds);
|
||||
// only first item has own context
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||
expect(timeContext2.bounds()).toEqual(bounds);
|
||||
expect(timeContext3.bounds()).toEqual(bounds);
|
||||
// first and second item have own context
|
||||
let destroyTimeContext2 = api.addIndependentContext(domainObjectKey2, independentBounds);
|
||||
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||
expect(timeContext2.bounds()).toEqual(independentBounds);
|
||||
expect(timeContext3.bounds()).toEqual(bounds);
|
||||
// all items have own time context
|
||||
let destroyTimeContext3 = api.addIndependentContext(domainObjectKey3, independentBounds);
|
||||
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||
expect(timeContext2.bounds()).toEqual(independentBounds);
|
||||
expect(timeContext3.bounds()).toEqual(independentBounds);
|
||||
//remove own contexts one at a time - should revert to global time context
|
||||
destroyTimeContext();
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
expect(timeContext2.bounds()).toEqual(independentBounds);
|
||||
expect(timeContext3.bounds()).toEqual(independentBounds);
|
||||
destroyTimeContext2();
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
expect(timeContext2.bounds()).toEqual(bounds);
|
||||
expect(timeContext3.bounds()).toEqual(independentBounds);
|
||||
destroyTimeContext3();
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
expect(timeContext2.bounds()).toEqual(bounds);
|
||||
expect(timeContext3.bounds()).toEqual(bounds);
|
||||
});
|
||||
|
||||
it("Allows setting of valid bounds", function () {
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -26,18 +26,23 @@
|
||||
:style="`width: 100%; height: 100%`"
|
||||
>
|
||||
<CompassHUD
|
||||
v-if="showCompassHUD"
|
||||
:sun-heading="sunHeading"
|
||||
:camera-angle-of-view="cameraAngleOfView"
|
||||
:camera-pan="cameraPan"
|
||||
:heading="heading"
|
||||
:camera-azimuth="cameraAzimuth"
|
||||
:transformations="transformations"
|
||||
:has-gimble="hasGimble"
|
||||
:normalized-camera-azimuth="normalizedCameraAzimuth"
|
||||
:sun-heading="sunHeading"
|
||||
/>
|
||||
<CompassRose
|
||||
v-if="showCompassRose"
|
||||
:camera-pan="cameraPan"
|
||||
:camera-angle-of-view="cameraAngleOfView"
|
||||
:heading="heading"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
:sun-heading="sunHeading"
|
||||
:camera-azimuth="cameraAzimuth"
|
||||
:transformations="transformations"
|
||||
:has-gimble="hasGimble"
|
||||
:normalized-camera-azimuth="normalizedCameraAzimuth"
|
||||
:sun-heading="sunHeading"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -45,6 +50,7 @@
|
||||
<script>
|
||||
import CompassHUD from './CompassHUD.vue';
|
||||
import CompassRose from './CompassRose.vue';
|
||||
import { rotate } from './utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -62,11 +68,14 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showCompassHUD() {
|
||||
return this.hasCameraPan && this.cameraAngleOfView > 0;
|
||||
hasGimble() {
|
||||
return this.cameraAzimuth !== undefined;
|
||||
},
|
||||
showCompassRose() {
|
||||
return (this.hasCameraPan || this.hasHeading) && this.cameraAngleOfView > 0;
|
||||
// compass ordinal orientation of camera
|
||||
normalizedCameraAzimuth() {
|
||||
return this.hasGimble
|
||||
? rotate(this.cameraAzimuth)
|
||||
: rotate(this.heading, -this.transformations.rotation || 0);
|
||||
},
|
||||
// horizontal rotation from north in degrees
|
||||
heading() {
|
||||
@ -80,14 +89,11 @@ export default {
|
||||
return this.image.sunOrientation;
|
||||
},
|
||||
// horizontal rotation from north in degrees
|
||||
cameraPan() {
|
||||
cameraAzimuth() {
|
||||
return this.image.cameraPan;
|
||||
},
|
||||
hasCameraPan() {
|
||||
return this.cameraPan !== undefined;
|
||||
},
|
||||
cameraAngleOfView() {
|
||||
return this.transformations?.cameraAngleOfView;
|
||||
return this.transformations.cameraAngleOfView;
|
||||
},
|
||||
transformations() {
|
||||
return this.image.transformations;
|
||||
|
@ -94,17 +94,33 @@ const COMPASS_POINTS = [
|
||||
|
||||
export default {
|
||||
props: {
|
||||
cameraAngleOfView: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
heading: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
cameraAzimuth: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
transformations: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
hasGimble: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
normalizedCameraAzimuth: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
sunHeading: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraAngleOfView: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -130,10 +146,13 @@ export default {
|
||||
left: `${ percentage * 100 }%`
|
||||
};
|
||||
},
|
||||
cameraRotation() {
|
||||
return this.transformations?.rotation;
|
||||
},
|
||||
visibleRange() {
|
||||
return [
|
||||
rotate(this.cameraPan, -this.cameraAngleOfView / 2),
|
||||
rotate(this.cameraPan, this.cameraAngleOfView / 2)
|
||||
rotate(this.normalizedCameraAzimuth, -this.cameraAngleOfView / 2),
|
||||
rotate(this.normalizedCameraAzimuth, this.cameraAngleOfView / 2)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +75,6 @@
|
||||
:style="sunHeadingStyle"
|
||||
/>
|
||||
|
||||
<!-- Camera FOV -->
|
||||
<mask
|
||||
id="mask2"
|
||||
class="c-cr__cam-fov-l-mask"
|
||||
@ -107,55 +106,61 @@
|
||||
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 cameras that gimble. -->
|
||||
<path
|
||||
class="cr-vrover__body"
|
||||
:style="gimbledCameraPanStyle"
|
||||
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"
|
||||
|
||||
<!-- Camera FOV -->
|
||||
<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>
|
||||
|
||||
<!-- NSEW and ticks -->
|
||||
<g
|
||||
class="c-cr__nsew"
|
||||
:style="compassRoseStyle"
|
||||
:style="compassDialStyle"
|
||||
>
|
||||
<g class="c-cr__ticks-major">
|
||||
<path d="M50 3L43 10H57L50 3Z" />
|
||||
@ -254,23 +259,32 @@ import { throttle } from 'lodash';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
cameraAngleOfView: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
heading: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
required: true
|
||||
},
|
||||
sunHeading: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraPan: {
|
||||
cameraAzimuth: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
transformations: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
hasGimble: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
normalizedCameraAzimuth: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
sunHeading: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
sizedImageDimensions: {
|
||||
@ -284,18 +298,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
cameraHeading() {
|
||||
return this.cameraPan ?? this.heading;
|
||||
},
|
||||
cameraAngleOfView() {
|
||||
const cameraAngleOfView = this.transformations?.cameraAngleOfView;
|
||||
|
||||
if (!cameraAngleOfView) {
|
||||
console.warn('No Camera Angle of View provided');
|
||||
}
|
||||
|
||||
return cameraAngleOfView;
|
||||
},
|
||||
camAngleAndPositionStyle() {
|
||||
const translateX = this.transformations?.translateX;
|
||||
const translateY = this.transformations?.translateY;
|
||||
@ -304,18 +306,22 @@ export default {
|
||||
|
||||
return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` };
|
||||
},
|
||||
camGimbalAngleStyle() {
|
||||
const rotation = rotate(this.north, this.heading);
|
||||
gimbledCameraPanStyle() {
|
||||
if (!this.hasGimble) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gimbledCameraPan = rotate(this.normalizedCameraAzimuth, -this.heading);
|
||||
|
||||
return {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
transform: `rotate(${ -gimbledCameraPan }deg)`
|
||||
};
|
||||
},
|
||||
compassRoseStyle() {
|
||||
compassDialStyle() {
|
||||
return { transform: `rotate(${ this.north }deg)` };
|
||||
},
|
||||
north() {
|
||||
return this.lockCompass ? rotate(-this.cameraHeading) : 0;
|
||||
return this.lockCompass ? rotate(-this.normalizedCameraAzimuth) : 0;
|
||||
},
|
||||
cardinalTextRotateN() {
|
||||
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
|
||||
@ -332,14 +338,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;
|
||||
},
|
||||
@ -351,7 +349,7 @@ export default {
|
||||
};
|
||||
},
|
||||
cameraHeadingStyle() {
|
||||
const rotation = rotate(this.north, this.cameraHeading);
|
||||
const rotation = rotate(this.north, this.normalizedCameraAzimuth);
|
||||
|
||||
return {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
|
@ -35,8 +35,15 @@ describe("The Compass component", () => {
|
||||
roll: 90,
|
||||
pitch: 90,
|
||||
cameraTilt: 100,
|
||||
cameraPan: 90,
|
||||
sunAngle: 30
|
||||
cameraAzimuth: 90,
|
||||
sunAngle: 30,
|
||||
transformations: {
|
||||
translateX: 0,
|
||||
translateY: 18,
|
||||
rotation: 0,
|
||||
scale: 0.3,
|
||||
cameraAngleOfView: 70
|
||||
}
|
||||
};
|
||||
let propsData = {
|
||||
naturalAspectRatio: 0.9,
|
||||
@ -44,8 +51,7 @@ describe("The Compass component", () => {
|
||||
sizedImageDimensions: {
|
||||
width: 100,
|
||||
height: 100
|
||||
},
|
||||
compassRoseSizingClasses: '--rose-small --rose-min'
|
||||
}
|
||||
};
|
||||
|
||||
app = new Vue({
|
||||
@ -54,7 +60,6 @@ describe("The Compass component", () => {
|
||||
return propsData;
|
||||
},
|
||||
template: `<Compass
|
||||
:compass-rose-sizing-classes="compassRoseSizingClasses"
|
||||
:image="image"
|
||||
:natural-aspect-ratio="naturalAspectRatio"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
@ -67,7 +72,7 @@ describe("The Compass component", () => {
|
||||
app.$destroy();
|
||||
});
|
||||
|
||||
describe("when a heading value exists on the image", () => {
|
||||
describe("when a heading value and cameraAngleOfView exists on the image", () => {
|
||||
|
||||
it("should display a compass rose", () => {
|
||||
let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS
|
||||
|
@ -94,7 +94,6 @@
|
||||
<Compass
|
||||
v-if="shouldDisplayCompass"
|
||||
:image="focusedImage"
|
||||
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
/>
|
||||
</div>
|
||||
@ -171,7 +170,7 @@
|
||||
>
|
||||
<ImageThumbnail
|
||||
v-for="(image, index) in imageHistory"
|
||||
:key="`${image.thumbnailUrl || image.url}${image.time}`"
|
||||
:key="`${image.thumbnailUrl || image.url}-${image.time}-${index}`"
|
||||
:image="image"
|
||||
:active="focusedImageIndex === index"
|
||||
:selected="focusedImageIndex === index && isPaused"
|
||||
@ -430,9 +429,12 @@ export default {
|
||||
&& imageHeightAndWidth
|
||||
&& this.zoomFactor === 1
|
||||
&& this.imagePanned !== true;
|
||||
const hasCameraConfigurations = this.focusedImage?.transformations !== undefined;
|
||||
const hasHeading = this.focusedImage?.heading !== undefined;
|
||||
const hasCameraAngleOfView = this.focusedImage?.transformations?.cameraAngleOfView > 0;
|
||||
|
||||
return display && hasCameraConfigurations;
|
||||
return display
|
||||
&& hasCameraAngleOfView
|
||||
&& hasHeading;
|
||||
},
|
||||
isSpacecraftPositionFresh() {
|
||||
let isFresh = undefined;
|
||||
@ -582,11 +584,34 @@ export default {
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
focusedImageIndex() {
|
||||
this.trackDuration();
|
||||
this.resetAgeCSS();
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
this.getImageNaturalDimensions();
|
||||
focusedImage: {
|
||||
handler(newImage, oldImage) {
|
||||
const newTime = newImage?.time;
|
||||
const oldTime = oldImage?.time;
|
||||
const newUrl = newImage?.url;
|
||||
const oldUrl = oldImage?.url;
|
||||
|
||||
// Skip if it's all falsy
|
||||
if (!newTime && !oldTime && !newUrl && !oldUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if it's the same image
|
||||
if (newTime === oldTime && newUrl === oldUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update image duration and reset age CSS
|
||||
this.trackDuration();
|
||||
this.resetAgeCSS();
|
||||
|
||||
// Reset image dimensions and calculate new dimensions
|
||||
// on new image load
|
||||
this.getImageNaturalDimensions();
|
||||
|
||||
// Get the related telemetry for the new image
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
}
|
||||
},
|
||||
bounds() {
|
||||
this.scrollHandler();
|
||||
@ -771,6 +796,10 @@ export default {
|
||||
this.layers = layersMetadata;
|
||||
if (this.domainObject.configuration) {
|
||||
const persistedLayers = this.domainObject.configuration.layers;
|
||||
if (!persistedLayers) {
|
||||
return;
|
||||
}
|
||||
|
||||
layersMetadata.forEach((layer) => {
|
||||
const persistedLayer = persistedLayers.find(object => object.name === layer.name);
|
||||
if (persistedLayer) {
|
||||
|
@ -76,9 +76,14 @@ export default {
|
||||
this.telemetryCollection.destroy();
|
||||
},
|
||||
methods: {
|
||||
dataAdded(dataToAdd) {
|
||||
const normalizedDataToAdd = dataToAdd.map(datum => this.normalizeDatum(datum));
|
||||
this.imageHistory = this.imageHistory.concat(normalizedDataToAdd);
|
||||
dataAdded(addedItems, addedItemIndices) {
|
||||
const normalizedDataToAdd = addedItems.map(datum => this.normalizeDatum(datum));
|
||||
let newImageHistory = this.imageHistory.slice();
|
||||
normalizedDataToAdd.forEach(((datum, index) => {
|
||||
newImageHistory.splice(addedItemIndices[index] ?? -1, 0, datum);
|
||||
}));
|
||||
//Assign just once so imageHistory watchers don't get called too often
|
||||
this.imageHistory = newImageHistory;
|
||||
},
|
||||
dataCleared() {
|
||||
this.imageHistory = [];
|
||||
@ -153,9 +158,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
|
||||
delete this.imageContainerWidth;
|
||||
delete this.imageContainerHeight;
|
||||
this.bounds = bounds; // setting bounds for ImageryView watcher
|
||||
},
|
||||
timeSystemChange() {
|
||||
|
@ -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 = '';
|
||||
}
|
||||
},
|
||||
|
@ -64,7 +64,7 @@
|
||||
tabindex="0"
|
||||
>
|
||||
<TextHighlight
|
||||
:text="entryText"
|
||||
:text="formatValidUrls(entry.text)"
|
||||
:highlight="highlightText"
|
||||
:highlight-class="'search-highlight'"
|
||||
/>
|
||||
@ -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>
|
||||
@ -94,12 +94,12 @@
|
||||
class="c-ne__text"
|
||||
contenteditable="false"
|
||||
tabindex="0"
|
||||
v-html="formattedText"
|
||||
v-bind.prop="formattedText"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<div class="c-ne__tags c-tag-holder">
|
||||
<div
|
||||
v-for="(tag, index) in entryTags"
|
||||
:key="index"
|
||||
@ -228,14 +228,17 @@ export default {
|
||||
},
|
||||
selectedEntryId: {
|
||||
type: String,
|
||||
required: true
|
||||
default() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editMode: false,
|
||||
canEdit: true,
|
||||
enableEmbedsWrapperScroll: false
|
||||
enableEmbedsWrapperScroll: false,
|
||||
urlWhitelist: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -247,28 +250,15 @@ export default {
|
||||
},
|
||||
formattedText() {
|
||||
// remove ANY tags
|
||||
let text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA);
|
||||
const text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA);
|
||||
|
||||
if (this.editMode || !this.urlWhitelist) {
|
||||
return text;
|
||||
if (this.editMode || this.urlWhitelist.length === 0) {
|
||||
return { innerText: text };
|
||||
}
|
||||
|
||||
text = text.replace(URL_REGEX, (match) => {
|
||||
const url = new URL(match);
|
||||
const domain = url.hostname;
|
||||
let result = match;
|
||||
let isMatch = this.urlWhitelist.find((partialDomain) => {
|
||||
return domain.endsWith(partialDomain);
|
||||
});
|
||||
const html = this.formatValidUrls(text);
|
||||
|
||||
if (isMatch) {
|
||||
result = `<a class="c-hyperlink" target="_blank" href="${match}">${match}</a>`;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return text;
|
||||
return { innerHTML: html };
|
||||
},
|
||||
isSelectedEntry() {
|
||||
return this.selectedEntryId === this.entry.id;
|
||||
@ -354,6 +344,22 @@ export default {
|
||||
deleteEntry() {
|
||||
this.$emit('deleteEntry', this.entry.id);
|
||||
},
|
||||
formatValidUrls(text) {
|
||||
return text.replace(URL_REGEX, (match) => {
|
||||
const url = new URL(match);
|
||||
const domain = url.hostname;
|
||||
let result = match;
|
||||
let isMatch = this.urlWhitelist.find((partialDomain) => {
|
||||
return domain.endsWith(partialDomain);
|
||||
});
|
||||
|
||||
if (isMatch) {
|
||||
result = `<a class="c-hyperlink" target="_blank" href="${match}">${match}</a>`;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
},
|
||||
manageEmbedLayout() {
|
||||
if (this.$refs.embeds) {
|
||||
const embedsWrapperLength = this.$refs.embedsWrapper.clientWidth;
|
||||
@ -456,7 +462,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 +478,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,
|
||||
|
@ -6,8 +6,7 @@ export const NOTEBOOK_DEFAULT = 'DEFAULT';
|
||||
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
|
||||
export const NOTEBOOK_VIEW_TYPE = 'notebook-vue';
|
||||
export const RESTRICTED_NOTEBOOK_VIEW_TYPE = 'restricted-notebook-vue';
|
||||
export const NOTEBOOK_INSTALLED_KEY = '_NOTEBOOK_PLUGIN_INSTALLED';
|
||||
export const RESTRICTED_NOTEBOOK_INSTALLED_KEY = '_RESTRICTED_NOTEBOOK_PLUGIN_INSTALLED';
|
||||
export const NOTEBOOK_BASE_INSTALLED = '_NOTEBOOK_BASE_FUNCTIONALITY_INSTALLED';
|
||||
|
||||
// these only deals with constants, figured this could skip going into a utils file
|
||||
export function isNotebookOrAnnotationType(domainObject) {
|
||||
|
@ -33,8 +33,7 @@ import {
|
||||
RESTRICTED_NOTEBOOK_TYPE,
|
||||
NOTEBOOK_VIEW_TYPE,
|
||||
RESTRICTED_NOTEBOOK_VIEW_TYPE,
|
||||
NOTEBOOK_INSTALLED_KEY,
|
||||
RESTRICTED_NOTEBOOK_INSTALLED_KEY
|
||||
NOTEBOOK_BASE_INSTALLED
|
||||
} from './notebook-constants';
|
||||
|
||||
import Vue from 'vue';
|
||||
@ -63,7 +62,7 @@ function addLegacyNotebookGetInterceptor(openmct) {
|
||||
|
||||
function installBaseNotebookFunctionality(openmct) {
|
||||
// only need to do this once
|
||||
if (openmct[NOTEBOOK_INSTALLED_KEY] || openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) {
|
||||
if (openmct[NOTEBOOK_BASE_INSTALLED]) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -101,14 +100,12 @@ function installBaseNotebookFunctionality(openmct) {
|
||||
openmct.indicators.add(indicator);
|
||||
|
||||
monkeyPatchObjectAPIForNotebooks(openmct);
|
||||
|
||||
openmct[NOTEBOOK_BASE_INSTALLED] = true;
|
||||
}
|
||||
|
||||
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,17 +119,11 @@ function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {
|
||||
openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);
|
||||
|
||||
installBaseNotebookFunctionality(openmct);
|
||||
|
||||
openmct[NOTEBOOK_INSTALLED_KEY] = true;
|
||||
};
|
||||
}
|
||||
|
||||
function RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist = []) {
|
||||
return function install(openmct) {
|
||||
if (openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const icon = 'icon-notebook-shift-log';
|
||||
const description = 'Create and save timestamped notes with embedded object snapshots with the ability to commit and lock pages.';
|
||||
const snapshotContainer = getSnapshotContainer(openmct);
|
||||
@ -144,8 +135,6 @@ function RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist
|
||||
openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);
|
||||
|
||||
installBaseNotebookFunctionality(openmct);
|
||||
|
||||
openmct[RESTRICTED_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,27 +827,35 @@ 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() {
|
||||
const nearbyAnnotations = [];
|
||||
this.config.series.models.forEach(series => {
|
||||
if (series.closest.annotationsById) {
|
||||
if (series?.closest?.annotationsById) {
|
||||
Object.values(series.closest.annotationsById).forEach(closeAnnotation => {
|
||||
const addedAnnotationAlready = nearbyAnnotations.some(annotation => {
|
||||
return _.isEqual(annotation.targets, closeAnnotation.targets)
|
||||
@ -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() {
|
||||
|
@ -52,6 +52,41 @@ const MARKER_SIZE = 6.0;
|
||||
const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;
|
||||
const ANNOTATION_SIZE = MARKER_SIZE * 3.0;
|
||||
const CLEARANCE = 15;
|
||||
// These attributes are changed in the plot model, but we don't need to react to the changes.
|
||||
const NO_HANDLING_NEEDED_ATTRIBUTES = {
|
||||
label: 'label',
|
||||
values: 'values',
|
||||
format: 'format',
|
||||
color: 'color',
|
||||
name: 'name',
|
||||
unit: 'unit'
|
||||
};
|
||||
// These attributes in turn set one of HANDLED_ATTRIBUTES, so we don't need specific listeners for them - this prevents excessive redraws.
|
||||
const IMPLICIT_HANDLED_ATTRIBUTES = {
|
||||
range: 'range',
|
||||
//series stats update y axis stats
|
||||
stats: 'stats',
|
||||
frozen: 'frozen',
|
||||
autoscale: 'autoscale',
|
||||
autoscalePadding: 'autoscalePadding',
|
||||
logMode: 'logMode',
|
||||
yKey: 'yKey'
|
||||
};
|
||||
// Attribute changes that we are specifically handling with listeners
|
||||
const HANDLED_ATTRIBUTES = {
|
||||
//X and Y Axis attributes
|
||||
key: 'key',
|
||||
displayRange: 'displayRange',
|
||||
//series attributes
|
||||
xKey: 'xKey',
|
||||
interpolate: 'interpolate',
|
||||
markers: 'markers',
|
||||
markerShape: 'markerShape',
|
||||
markerSize: 'markerSize',
|
||||
alarmMarkers: 'alarmMarkers',
|
||||
limitLines: 'limitLines',
|
||||
yAxisId: 'yAxisId'
|
||||
};
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
@ -121,6 +156,7 @@ export default {
|
||||
hiddenYAxisIds() {
|
||||
this.hiddenYAxisIds.forEach(id => {
|
||||
this.resetYOffsetAndSeriesDataForYAxis(id);
|
||||
this.drawLimitLines();
|
||||
});
|
||||
this.scheduleDraw();
|
||||
}
|
||||
@ -137,14 +173,16 @@ export default {
|
||||
this.offset = {
|
||||
[yAxisId]: {}
|
||||
};
|
||||
this.listenTo(this.config.yAxis, 'change:key', this.resetYOffsetAndSeriesDataForYAxis.bind(this, yAxisId), this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw);
|
||||
this.listenTo(this.config.yAxis, `change:${HANDLED_ATTRIBUTES.displayRange}`, this.scheduleDraw);
|
||||
this.listenTo(this.config.yAxis, `change:${HANDLED_ATTRIBUTES.key}`, this.resetYOffsetAndSeriesDataForYAxis.bind(this, yAxisId), this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.redrawIfNotAlreadyHandled);
|
||||
if (this.config.additionalYAxes.length) {
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
const id = yAxis.get('id');
|
||||
this.offset[id] = {};
|
||||
this.listenTo(yAxis, 'change', this.updateLimitsAndDraw);
|
||||
this.listenTo(yAxis, 'change:key', this.resetYOffsetAndSeriesDataForYAxis.bind(this, id), this);
|
||||
this.listenTo(yAxis, `change:${HANDLED_ATTRIBUTES.displayRange}`, this.scheduleDraw);
|
||||
this.listenTo(yAxis, `change:${HANDLED_ATTRIBUTES.key}`, this.resetYOffsetAndSeriesDataForYAxis.bind(this, id), this);
|
||||
this.listenTo(yAxis, 'change', this.redrawIfNotAlreadyHandled);
|
||||
});
|
||||
}
|
||||
|
||||
@ -161,7 +199,8 @@ export default {
|
||||
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
|
||||
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
|
||||
|
||||
this.listenTo(this.config.xAxis, 'change', this.updateLimitsAndDraw);
|
||||
this.listenTo(this.config.xAxis, 'change:displayRange', this.scheduleDraw);
|
||||
this.listenTo(this.config.xAxis, 'change', this.redrawIfNotAlreadyHandled);
|
||||
this.config.series.forEach(this.onSeriesAdd, this);
|
||||
this.$emit('chartLoaded');
|
||||
},
|
||||
@ -190,21 +229,33 @@ export default {
|
||||
this.changeLimitLines(mode, o, series);
|
||||
},
|
||||
onSeriesAdd(series) {
|
||||
this.listenTo(series, 'change:xKey', this.reDraw, this);
|
||||
this.listenTo(series, 'change:interpolate', this.changeInterpolate, this);
|
||||
this.listenTo(series, 'change:markers', this.changeMarkers, this);
|
||||
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
|
||||
this.listenTo(series, 'change:limitLines', this.changeLimitLines, this);
|
||||
this.listenTo(series, 'change:yAxisId', this.resetAxisAndRedraw, this);
|
||||
this.listenTo(series, 'change', this.scheduleDraw);
|
||||
this.listenTo(series, `change:${HANDLED_ATTRIBUTES.xKey}`, this.reDraw, this);
|
||||
this.listenTo(series, `change:${HANDLED_ATTRIBUTES.interpolate}`, this.changeInterpolate, this);
|
||||
this.listenTo(series, `change:${HANDLED_ATTRIBUTES.markers}`, this.changeMarkers, this);
|
||||
this.listenTo(series, `change:${HANDLED_ATTRIBUTES.alarmMarkers}`, this.changeAlarmMarkers, this);
|
||||
this.listenTo(series, `change:${HANDLED_ATTRIBUTES.limitLines}`, this.changeLimitLines, this);
|
||||
this.listenTo(series, `change:${HANDLED_ATTRIBUTES.yAxisId}`, this.resetAxisAndRedraw, this);
|
||||
this.listenTo(series, `change:${HANDLED_ATTRIBUTES.markerShape}`, this.scheduleDraw, this);
|
||||
this.listenTo(series, `change:${HANDLED_ATTRIBUTES.markerSize}`, this.scheduleDraw, this);
|
||||
this.listenTo(series, 'change', this.redrawIfNotAlreadyHandled);
|
||||
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);
|
||||
|
||||
@ -519,6 +570,21 @@ export default {
|
||||
|
||||
return true;
|
||||
},
|
||||
redrawIfNotAlreadyHandled(attribute, value, oldValue) {
|
||||
if (Object.keys(HANDLED_ATTRIBUTES).includes(attribute) && oldValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(IMPLICIT_HANDLED_ATTRIBUTES).includes(attribute) && oldValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(NO_HANDLING_NEEDED_ATTRIBUTES).includes(attribute) && oldValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateLimitsAndDraw();
|
||||
},
|
||||
updateLimitsAndDraw() {
|
||||
this.drawLimitLines();
|
||||
this.scheduleDraw();
|
||||
@ -615,9 +681,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 +701,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 +813,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
|
||||
@ -241,6 +252,7 @@ export default class PlotSeries extends Model {
|
||||
}
|
||||
|
||||
const valueMetadata = this.metadata.value(newKey);
|
||||
//TODO: Should we do this even if there is a persisted config?
|
||||
if (!this.persistedConfig || !this.persistedConfig.interpolate) {
|
||||
if (valueMetadata.format === 'enum') {
|
||||
this.set('interpolate', 'stepAfter');
|
||||
|
@ -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);
|
||||
|
@ -57,7 +57,14 @@ export default class YAxisModel extends Model {
|
||||
this.listenTo(this, 'change:logMode', this.onLogModeChange, this);
|
||||
this.listenTo(this, 'change:frozen', this.toggleFreeze, this);
|
||||
this.listenTo(this, 'change:range', this.updateDisplayRange, this);
|
||||
this.updateDisplayRange(this.get('range'));
|
||||
const range = this.get('range');
|
||||
this.updateDisplayRange(range);
|
||||
//This is an edge case and should not happen
|
||||
const invalidRange = !range || (range?.min === undefined || range?.max === undefined);
|
||||
const invalidAutoScaleOff = (options.model.autoscale === false) && invalidRange;
|
||||
if (invalidAutoScaleOff) {
|
||||
this.set('autoscale', true);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {import('./SeriesCollection').default} seriesCollection
|
||||
@ -250,23 +257,6 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
|
||||
this.set('displayRange', _range);
|
||||
} else {
|
||||
// Otherwise use the last known displayRange as the initial
|
||||
// values for the user-defined range, so that we don't end up
|
||||
// with any error from an undefined user range.
|
||||
|
||||
const _range = this.get('displayRange');
|
||||
|
||||
if (!_range) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.get('logMode')) {
|
||||
_range.min = antisymlog(_range.min, 10);
|
||||
_range.max = antisymlog(_range.max, 10);
|
||||
}
|
||||
|
||||
this.set('range', _range);
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,7 +277,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());
|
||||
});
|
||||
@ -376,11 +367,8 @@ export default class YAxisModel extends Model {
|
||||
autoscale: true,
|
||||
logMode: options.model?.logMode ?? false,
|
||||
autoscalePadding: 0.1,
|
||||
id: options.id
|
||||
|
||||
// 'range' is not specified here, it is undefined at first. When the
|
||||
// user turns off autoscale, the current 'displayRange' is used for
|
||||
// the initial value of 'range'.
|
||||
id: options.id,
|
||||
range: options.model?.range
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
@ -71,7 +73,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="!yAxis.autoscale && yAxis.rangeMin"
|
||||
v-if="!yAxis.autoscale && yAxis.rangeMin !== ''"
|
||||
class="grid-row"
|
||||
>
|
||||
<div
|
||||
@ -81,7 +83,7 @@
|
||||
<div class="grid-cell value">{{ yAxis.rangeMin }}</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="!yAxis.autoscale && yAxis.rangeMax"
|
||||
v-if="!yAxis.autoscale && yAxis.rangeMax !== ''"
|
||||
class="grid-row"
|
||||
>
|
||||
<div
|
||||
@ -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;
|
||||
|
||||
},
|
||||
@ -212,8 +217,8 @@ export default {
|
||||
autoscale: this.config.yAxis.get('autoscale'),
|
||||
logMode: this.config.yAxis.get('logMode'),
|
||||
autoscalePadding: this.config.yAxis.get('autoscalePadding'),
|
||||
rangeMin: range ? range.min : '',
|
||||
rangeMax: range ? range.max : ''
|
||||
rangeMin: range?.min ?? '',
|
||||
rangeMax: range?.max ?? ''
|
||||
});
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
range = yAxis.get('range');
|
||||
@ -225,8 +230,8 @@ export default {
|
||||
autoscale: yAxis.get('autoscale'),
|
||||
logMode: yAxis.get('logMode'),
|
||||
autoscalePadding: yAxis.get('autoscalePadding'),
|
||||
rangeMin: range ? range.min : '',
|
||||
rangeMax: range ? range.max : ''
|
||||
rangeMin: range?.min ?? '',
|
||||
rangeMax: range?.max ?? ''
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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() {
|
||||
@ -150,23 +153,50 @@ export default {
|
||||
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
this.incrementAxisUsageCount(yAxisId);
|
||||
this.$set(this.plotSeries, index, series);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
|
||||
if (this.isStackedPlotObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the series moves to a different yAxis, update the seriesCounts for both yAxes
|
||||
// so we can display the configuration options for all used yAxes
|
||||
this.listenTo(series, 'change:yAxisId', (newYAxisId, oldYAxisId) => {
|
||||
this.incrementAxisUsageCount(newYAxisId);
|
||||
this.decrementAxisUsageCount(oldYAxisId);
|
||||
}, this);
|
||||
},
|
||||
|
||||
removeSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, -1);
|
||||
this.decrementAxisUsageCount(yAxisId);
|
||||
this.plotSeries.splice(index, 1);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
|
||||
if (this.isStackedPlotObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopListening(series, 'change:yAxisId');
|
||||
},
|
||||
|
||||
incrementAxisUsageCount(yAxisId) {
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
},
|
||||
|
||||
decrementAxisUsageCount(yAxisId) {
|
||||
this.updateAxisUsageCount(yAxisId, -1);
|
||||
},
|
||||
|
||||
updateAxisUsageCount(yAxisId, updateCount) {
|
||||
const foundYAxis = this.findYAxisForId(yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;
|
||||
if (!foundYAxis) {
|
||||
throw new Error(`yAxis with id ${yAxisId} not found`);
|
||||
}
|
||||
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;
|
||||
},
|
||||
|
||||
updateSeriesConfigForObject(config) {
|
||||
|
@ -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
|
||||
@ -78,7 +81,7 @@
|
||||
>Minimum Value</div>
|
||||
<div class="grid-cell value">
|
||||
<input
|
||||
v-model="rangeMin"
|
||||
v-model.lazy="rangeMin"
|
||||
class="c-input--flex"
|
||||
type="number"
|
||||
@change="updateForm('range')"
|
||||
@ -91,7 +94,7 @@
|
||||
title="Maximum Y axis value."
|
||||
>Maximum Value</div>
|
||||
<div class="grid-cell value"><input
|
||||
v-model="rangeMax"
|
||||
v-model.lazy="rangeMax"
|
||||
class="c-input--flex"
|
||||
type="number"
|
||||
@change="updateForm('range')"
|
||||
@ -128,6 +131,12 @@ export default {
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.autoscale === false && this.validationErrors.range) {
|
||||
this.autoscale = true;
|
||||
this.updateForm('autoscale');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.getConfig();
|
||||
@ -169,12 +178,9 @@ export default {
|
||||
objectPath: `${prefix}.logMode`
|
||||
},
|
||||
range: {
|
||||
objectPath: `${prefix}.range'`,
|
||||
objectPath: `${prefix}.range`,
|
||||
coerce: function coerceRange(range) {
|
||||
const newRange = {
|
||||
min: -1,
|
||||
max: 1
|
||||
};
|
||||
const newRange = {};
|
||||
|
||||
if (range && typeof range.min !== 'undefined' && range.min !== null) {
|
||||
newRange.min = Number(range.min);
|
||||
@ -219,16 +225,18 @@ export default {
|
||||
this.autoscale = this.yAxis.get('autoscale');
|
||||
this.logMode = this.yAxis.get('logMode');
|
||||
this.autoscalePadding = this.yAxis.get('autoscalePadding');
|
||||
const range = this.yAxis.get('range') ?? this.yAxis.get('displayRange');
|
||||
this.rangeMin = range?.min;
|
||||
this.rangeMax = range?.max;
|
||||
const range = this.yAxis.get('range');
|
||||
if (range && range.min !== undefined && range.max !== undefined) {
|
||||
this.rangeMin = range.min;
|
||||
this.rangeMax = range.max;
|
||||
}
|
||||
},
|
||||
getPrefix() {
|
||||
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;
|
||||
});
|
||||
}
|
||||
@ -308,6 +316,15 @@ export default {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//If autoscale is turned off, we must know what the user defined min and max ranges are
|
||||
if (formKey === 'autoscale' && this.autoscale === false) {
|
||||
const rangeFormField = this.fields.range;
|
||||
this.validationErrors.range = rangeFormField.validate?.({
|
||||
min: this.rangeMin,
|
||||
max: this.rangeMax
|
||||
}, this.yAxis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -664,7 +664,6 @@ mct-plot {
|
||||
border-radius: $smallCr;
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
padding: 1px;
|
||||
|
||||
.plot-series-swatch-and-name,
|
||||
.plot-series-value {
|
||||
|
@ -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],
|
||||
|