Compare commits
25 Commits
feature/is
...
hide-gripp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b117890f27 | ||
|
|
afc37209d2 | ||
|
|
e45b58e2a8 | ||
|
|
8b3487bdbe | ||
|
|
50719b1383 | ||
|
|
cfda067794 | ||
|
|
4847cc8931 | ||
|
|
a9e3eca35c | ||
|
|
cbecd79f71 | ||
|
|
3deb2e3dc2 | ||
|
|
d6e80447ab | ||
|
|
1a4bd0fb55 | ||
|
|
80f89c7609 | ||
|
|
b82649772f | ||
|
|
7f2ed27106 | ||
|
|
57e02db6b5 | ||
|
|
d54335d21c | ||
|
|
e0ed0bb6e2 | ||
|
|
ed3fd8f965 | ||
|
|
e6d59c61d1 | ||
|
|
b74b27c464 | ||
|
|
d35e161701 | ||
|
|
653cb62f9c | ||
|
|
19b3232fa0 | ||
|
|
19892aab53 |
@@ -2,7 +2,7 @@ version: 2.1
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.25.2-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.29.0-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||
|
||||
2
.github/workflows/e2e-couchdb.yml
vendored
2
.github/workflows/e2e-couchdb.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.25.2 install
|
||||
- run: npx playwright@1.29.0 install
|
||||
- run: npm install
|
||||
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||
- run: npm run test:e2e:couchdb
|
||||
|
||||
2
.github/workflows/e2e-pr.yml
vendored
2
.github/workflows/e2e-pr.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.25.2 install
|
||||
- run: npx playwright@1.29.0 install
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: npm install
|
||||
- run: npm run test:e2e:full
|
||||
|
||||
@@ -10,7 +10,7 @@ accept changes from external contributors.
|
||||
|
||||
The short version:
|
||||
|
||||
1. Write your contribution or describe your idea in the form of an [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions)
|
||||
1. Write your contribution or describe your idea in the form of a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [start a GitHub discussion](https://github.com/nasa/openmct/discussions).
|
||||
2. Make sure your contribution meets code, test, and commit message
|
||||
standards as described below.
|
||||
3. Submit a pull request from a topic branch back to `master`. Include a check
|
||||
|
||||
@@ -8,7 +8,7 @@ This document is designed to capture on the What, Why, and How's of writing and
|
||||
|
||||
1. [Getting Started](#getting-started)
|
||||
2. [Types of Testing](#types-of-e2e-testing)
|
||||
3. [Architecture](#architecture)
|
||||
3. [Architecture](#test-architecture-and-ci)
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -276,14 +276,36 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
|
||||
- Leverage `await page.goto('./', { waitUntil: 'networkidle' });`
|
||||
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
|
||||
### How to write a great test (TODO)
|
||||
### How to write a great test (WIP)
|
||||
|
||||
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
|
||||
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
|
||||
|
||||
```js
|
||||
// Fill the "Notes" section with information about the
|
||||
// currently running test and its project.
|
||||
const { testNotes } = page;
|
||||
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
|
||||
await notesInput.fill(testNotes);
|
||||
```
|
||||
|
||||
#### How to write a great visual test (TODO)
|
||||
|
||||
#### How to write a great network test
|
||||
|
||||
- Where possible, it is best to mock out third-party network activity to ensure we are testing application behavior of Open MCT.
|
||||
- It is best to be as specific as possible about the expected network request/response structures in creating your mocks.
|
||||
- Make sure to only mock requests which are relevant to the specific behavior being tested.
|
||||
- Where possible, network requests and responses should be treated in an order-agnostic manner, as the order in which certain requests/responses happen is dynamic and subject to change.
|
||||
|
||||
Some examples of mocking network responses in regards to CouchDB can be found in our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) test file.
|
||||
|
||||
### Best Practices
|
||||
|
||||
For now, our best practices exist as self-tested, living documentation in our [exampleTemplate.e2e.spec.js](./tests/framework/exampleTemplate.e2e.spec.js) file.
|
||||
|
||||
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
|
||||
|
||||
### Tips & Tricks (TODO)
|
||||
|
||||
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
|
||||
@@ -378,3 +400,23 @@ A single e2e test in Open MCT is extended to run:
|
||||
- Tests won't start because 'Error: <http://localhost:8080/># is already used...'
|
||||
This error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal:
|
||||
```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```
|
||||
|
||||
### Upgrading Playwright
|
||||
|
||||
In order to upgrade from one version of Playwright to another, the version should be updated in several places in both `openmct` and `openmct-yamcs` repos. An easy way to identify these locations is to search for the current version in all files and find/replace.
|
||||
|
||||
For reference, all of the locations where the version should be updated are listed below:
|
||||
|
||||
#### **In `openmct`:**
|
||||
|
||||
- `package.json`
|
||||
- Both packages `@playwright/test` and `playwright-core` should be updated to the same target version.
|
||||
- `.circleci/config.yml`
|
||||
- `.github/workflows/e2e-couchdb.yml`
|
||||
- `.github/workflows/e2e-pr.yml`
|
||||
|
||||
#### **In `openmct-yamcs`:**
|
||||
|
||||
- `package.json`
|
||||
- `@playwright/test` should be updated to the target version.
|
||||
- `.github/workflows/yamcs-quickstart-e2e.yml`
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
|
||||
test.describe("CouchDB Status Indicator @couchdb", () => {
|
||||
test.describe("CouchDB Status Indicator with mocked responses @couchdb", () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
//TODO BeforeAll Verify CouchDB Connectivity with APIContext
|
||||
test('Shows green if connected', async ({ page }) => {
|
||||
@@ -71,38 +71,41 @@ test.describe("CouchDB Status Indicator @couchdb", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("CouchDB initialization @couchdb", () => {
|
||||
test.describe("CouchDB initialization with mocked responses @couchdb", () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
|
||||
// Store any relevant PUT requests that happen on the page
|
||||
const createMineFolderRequests = [];
|
||||
page.on('request', req => {
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (req.method() === 'PUT' && req.url().endsWith('openmct/mine')) {
|
||||
createMineFolderRequests.push(req);
|
||||
}
|
||||
});
|
||||
const mockedMissingObjectResponsefromCouchDB = {
|
||||
status: 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
};
|
||||
|
||||
// Override the first request to GET openmct/mine to return a 404
|
||||
await page.route('**/openmct/mine', route => {
|
||||
route.fulfill({
|
||||
status: 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
// Override the first request to GET openmct/mine to return a 404.
|
||||
// This simulates the case of starting Open MCT with a fresh database
|
||||
// and no "My Items" folder created yet.
|
||||
await page.route('**/mine', route => {
|
||||
route.fulfill(mockedMissingObjectResponsefromCouchDB);
|
||||
}, { times: 1 });
|
||||
|
||||
// Go to baseURL
|
||||
// Set up promise to verify that a PUT request to create "My Items"
|
||||
// folder was made.
|
||||
const putMineFolderRequest = page.waitForRequest(req =>
|
||||
req.url().endsWith('/mine')
|
||||
&& req.method() === 'PUT');
|
||||
|
||||
// Set up promise to verify that a GET request to retrieve "My Items"
|
||||
// folder was made.
|
||||
const getMineFolderRequest = page.waitForRequest(req =>
|
||||
req.url().endsWith('/mine')
|
||||
&& req.method() === 'GET');
|
||||
|
||||
// Go to baseURL.
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Verify that error banner is displayed
|
||||
const bannerMessage = await page.locator('.c-message-banner__message').innerText();
|
||||
expect(bannerMessage).toEqual('Failed to retrieve object mine');
|
||||
|
||||
// Verify that a PUT request to create "My Items" folder was made
|
||||
await expect.poll(() => createMineFolderRequests.length, {
|
||||
message: 'Verify that PUT request to create "mine" folder was made',
|
||||
timeout: 1000
|
||||
}).toBeGreaterThanOrEqual(1);
|
||||
// Wait for both requests to resolve.
|
||||
await Promise.all([
|
||||
putMineFolderRequest,
|
||||
getMineFolderRequest
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
This test suite is dedicated to tests which verify form functionality in isolation
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const genUuid = require('uuid').v4;
|
||||
const path = require('path');
|
||||
|
||||
const TEST_FOLDER = 'test folder';
|
||||
@@ -128,6 +129,108 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
timeout: 1000
|
||||
}).toEqual(1);
|
||||
});
|
||||
test('Can create an object after a conflict error @couchdb @2p', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5982'
|
||||
});
|
||||
|
||||
const page2 = await page.context().newPage();
|
||||
|
||||
// Both pages: Go to baseURL
|
||||
await Promise.all([
|
||||
page.goto('./', { waitUntil: 'networkidle' }),
|
||||
page2.goto('./', { waitUntil: 'networkidle' })
|
||||
]);
|
||||
|
||||
// Both pages: Click the Create button
|
||||
await Promise.all([
|
||||
page.click('button:has-text("Create")'),
|
||||
page2.click('button:has-text("Create")')
|
||||
]);
|
||||
|
||||
// Both pages: Click "Clock" in the Create menu
|
||||
await Promise.all([
|
||||
page.click(`li[role='menuitem']:text("Clock")`),
|
||||
page2.click(`li[role='menuitem']:text("Clock")`)
|
||||
]);
|
||||
|
||||
// Generate unique names for both objects
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
const nameInput2 = page2.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
|
||||
// Both pages: Fill in the 'Name' form field.
|
||||
await Promise.all([
|
||||
nameInput.fill(""),
|
||||
nameInput.fill(`Clock:${genUuid()}`),
|
||||
nameInput2.fill(""),
|
||||
nameInput2.fill(`Clock:${genUuid()}`)
|
||||
]);
|
||||
|
||||
// Both pages: Fill the "Notes" section with information about the
|
||||
// currently running test and its project.
|
||||
const testNotes = page.testNotes;
|
||||
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
|
||||
const notesInput2 = page2.locator('form[name="mctForm"] #notes-textarea');
|
||||
await Promise.all([
|
||||
notesInput.fill(testNotes),
|
||||
notesInput2.fill(testNotes)
|
||||
]);
|
||||
|
||||
// Page 2: Click "OK" to create the domain object and wait for navigation.
|
||||
// This will update the composition of the parent folder, setting the
|
||||
// conditions for a conflict error from the first page.
|
||||
await Promise.all([
|
||||
page2.waitForLoadState(),
|
||||
page2.click('[aria-label="Save"]'),
|
||||
// Wait for Save Banner to appear
|
||||
page2.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// Close Page 2, we're done with it.
|
||||
await page2.close();
|
||||
|
||||
// Page 1: Click "OK" to create the domain object and wait for navigation.
|
||||
// This will trigger a conflict error upon attempting to update
|
||||
// the composition of the parent folder.
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
page.click('[aria-label="Save"]'),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// Page 1: Verify that the conflict has occurred and an error notification is displayed.
|
||||
await expect(page.locator('.c-message-banner__message', {
|
||||
hasText: "Conflict detected while saving mine"
|
||||
})).toBeVisible();
|
||||
|
||||
// Page 1: Start logging console errors from this point on
|
||||
let errors = [];
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
// Page 1: Try to create a clock with the page that received the conflict.
|
||||
const clockAfterConflict = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock'
|
||||
});
|
||||
|
||||
// Page 1: Wait for save progress dialog to appear/disappear
|
||||
await page.locator('.c-message-banner__message', {
|
||||
hasText: 'Do not navigate away from this page or close this browser tab while this message is displayed.',
|
||||
state: 'visible'
|
||||
}).waitFor({ state: 'hidden' });
|
||||
|
||||
// Page 1: Navigate to 'My Items' and verify that the second clock was created
|
||||
await page.goto('./#/browse/mine');
|
||||
await expect(page.locator(`.c-grid-item__name[title="${clockAfterConflict.name}"]`)).toBeVisible();
|
||||
|
||||
// Verify no console errors occurred
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Form Correctness by Object Type', () => {
|
||||
|
||||
@@ -156,7 +156,7 @@ async function turnOffAutoscale(page) {
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
|
||||
// uncheck autoscale
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck();
|
||||
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();
|
||||
|
||||
@@ -205,7 +205,8 @@ async function enableEditMode(page) {
|
||||
*/
|
||||
async function enableLogMode(page) {
|
||||
// turn on log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
|
||||
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').check();
|
||||
// await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,7 +214,7 @@ async function enableLogMode(page) {
|
||||
*/
|
||||
async function disableLogMode(page) {
|
||||
// turn off log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
|
||||
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
116
e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js
Normal file
116
e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/*****************************************************************************
|
||||
* 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('Overlay Plot', () => {
|
||||
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"
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
// navigate to plot series color palette
|
||||
await page.click('.l-browse-bar__actions__edit');
|
||||
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
|
||||
await page.locator('.c-click-swatch--menu').click();
|
||||
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();
|
||||
|
||||
// gets color for swatch located in legend
|
||||
const element = await page.waitForSelector('.plot-series-color-swatch');
|
||||
const color = await element.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-color');
|
||||
});
|
||||
|
||||
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, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg a',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg b',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg c',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg d',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg e',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.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 page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
|
||||
await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
|
||||
await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
|
||||
await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group"]').nth(2));
|
||||
const elementsTree = await page.locator('#inspector-elements-tree').allInnerTexts();
|
||||
expect(elementsTree.join('').split('\n')).toEqual([
|
||||
"Y Axis 1",
|
||||
"swg d",
|
||||
"Y Axis 2",
|
||||
"swg e",
|
||||
"swg c",
|
||||
"swg a",
|
||||
"Y Axis 3",
|
||||
"swg b"
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const { createDomainObjectWithDefaults} = require('../../../../appActions');
|
||||
|
||||
test.describe('Plot Integrity Testing @unstable', () => {
|
||||
let sineWaveGeneratorObject;
|
||||
@@ -40,7 +40,6 @@ test.describe('Plot Integrity Testing @unstable', () => {
|
||||
test('Plots do not re-request data when a plot is clicked', async ({ page }) => {
|
||||
//Navigate to Sine Wave Generator
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
//Capture the number of plots points and store as const name numberOfPlotPoints
|
||||
//Click on the plot canvas
|
||||
await page.locator('canvas').nth(1).click();
|
||||
//No request was made to get historical data
|
||||
@@ -51,4 +50,90 @@ test.describe('Plot Integrity Testing @unstable', () => {
|
||||
});
|
||||
expect(createMineFolderRequests.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('Plot is rendered when infinity values exist', async ({ page }) => {
|
||||
// Edit Plot
|
||||
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
|
||||
|
||||
//Get pixel data from Canvas
|
||||
const plotPixelSize = await getCanvasPixelsWithData(page);
|
||||
expect(plotPixelSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This function edits a sine wave generator with the default options and enables the infinity values option.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('../../../../appActions').CreateObjectInfo} sineWaveGeneratorObject
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the edited domain object.
|
||||
*/
|
||||
async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) {
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
// Edit LAD table
|
||||
await page.locator('[title="More options"]').click();
|
||||
await page.locator('[title="Edit properties of this object."]').click();
|
||||
// Modify the infinity option to true
|
||||
const infinityInput = page.locator('[aria-label="Include Infinity Values"]');
|
||||
await infinityInput.click();
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
page.click('[aria-label="Save"]'),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// FIXME: Changes to SWG properties should be reflected on save, but they're not?
|
||||
// Thus, navigate away and back to the object.
|
||||
await page.goto('./#/browse/mine');
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
|
||||
await page.locator('c-progress-bar c-telemetry-table__progress-bar').waitFor({
|
||||
state: 'hidden'
|
||||
});
|
||||
|
||||
// FIXME: The progress bar disappears on series data load, not on plot render,
|
||||
// so wait for a half a second before evaluating the canvas.
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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('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;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ define([
|
||||
dataRateInHz: 1,
|
||||
randomness: 0,
|
||||
phase: 0,
|
||||
loadDelay: 0
|
||||
loadDelay: 0,
|
||||
infinityValues: false
|
||||
};
|
||||
|
||||
function GeneratorProvider(openmct) {
|
||||
@@ -56,7 +57,8 @@ define([
|
||||
'dataRateInHz',
|
||||
'randomness',
|
||||
'phase',
|
||||
'loadDelay'
|
||||
'loadDelay',
|
||||
'infinityValues'
|
||||
];
|
||||
|
||||
request = request || {};
|
||||
|
||||
@@ -76,10 +76,10 @@
|
||||
name: data.name,
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
|
||||
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues),
|
||||
wavelengths: wavelengths(),
|
||||
intensities: intensities(),
|
||||
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
|
||||
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues)
|
||||
}
|
||||
});
|
||||
nextStep += step;
|
||||
@@ -117,6 +117,7 @@
|
||||
var phase = request.phase;
|
||||
var randomness = request.randomness;
|
||||
var loadDelay = Math.max(request.loadDelay, 0);
|
||||
var infinityValues = request.infinityValues;
|
||||
|
||||
var step = 1000 / dataRateInHz;
|
||||
var nextStep = start - (start % step) + step;
|
||||
@@ -127,10 +128,10 @@
|
||||
data.push({
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
|
||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues),
|
||||
wavelengths: wavelengths(),
|
||||
intensities: intensities(),
|
||||
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
|
||||
cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,12 +156,20 @@
|
||||
});
|
||||
}
|
||||
|
||||
function cos(timestamp, period, amplitude, offset, phase, randomness) {
|
||||
function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
|
||||
if (infinityValues && Math.random() > 0.5) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
return amplitude
|
||||
* Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
||||
}
|
||||
|
||||
function sin(timestamp, period, amplitude, offset, phase, randomness) {
|
||||
function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
|
||||
if (infinityValues && Math.random() > 0.5) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
return amplitude
|
||||
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
||||
}
|
||||
|
||||
@@ -143,6 +143,16 @@ define([
|
||||
"telemetry",
|
||||
"loadDelay"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Include Infinity Values",
|
||||
control: "toggleSwitch",
|
||||
cssClass: "l-input",
|
||||
key: "infinityValues",
|
||||
property: [
|
||||
"telemetry",
|
||||
"infinityValues"
|
||||
]
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
@@ -153,7 +163,8 @@ define([
|
||||
dataRateInHz: 1,
|
||||
phase: 0,
|
||||
randomness: 0,
|
||||
loadDelay: 0
|
||||
loadDelay: 0,
|
||||
infinityValues: false
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
20
package.json
20
package.json
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.4-SNAPSHOT",
|
||||
"version": "2.1.5-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@percy/cli": "1.16.0",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.25.2",
|
||||
"@types/jasmine": "4.3.0",
|
||||
"@playwright/test": "1.29.0",
|
||||
"@types/jasmine": "4.3.1",
|
||||
"@types/lodash": "4.14.191",
|
||||
"babel-loader": "9.0.0",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"codecov": "3.8.3",
|
||||
"comma-separated-values": "3.6.4",
|
||||
@@ -19,7 +19,7 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.28.0",
|
||||
"eslint": "8.30.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.11.2",
|
||||
"eslint-plugin-vue": "9.8.0",
|
||||
@@ -42,22 +42,22 @@
|
||||
"karma-webpack": "5.0.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"mini-css-extract-plugin": "2.7.2",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.38",
|
||||
"moment-timezone": "0.5.40",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"playwright-core": "1.25.2",
|
||||
"playwright-core": "1.29.0",
|
||||
"plotly.js-basic-dist": "2.14.0",
|
||||
"plotly.js-gl2d-dist": "2.14.0",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.56.1",
|
||||
"sass-loader": "13.0.2",
|
||||
"sinon": "14.0.1",
|
||||
"sinon": "15.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "4.9.3",
|
||||
"typescript": "4.9.4",
|
||||
"uuid": "9.0.0",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "9.1.0",
|
||||
|
||||
@@ -73,6 +73,10 @@ export default class Editor extends EventEmitter {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.openmct.objects.getActiveTransaction();
|
||||
if (!transaction) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
transaction.cancel()
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<ToggleSwitch
|
||||
id="switchId"
|
||||
:checked="isChecked"
|
||||
:name="model.name"
|
||||
@change="toggleCheckBox"
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -410,9 +410,19 @@ export default class ObjectAPI {
|
||||
}
|
||||
}
|
||||
|
||||
return result.catch((error) => {
|
||||
return result.catch(async (error) => {
|
||||
if (error instanceof this.errors.Conflict) {
|
||||
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
|
||||
|
||||
// Synchronized objects will resolve their own conflicts, so
|
||||
// bypass the refresh here and throw the error.
|
||||
if (!this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
|
||||
if (this.isTransactionActive()) {
|
||||
this.endTransaction();
|
||||
}
|
||||
|
||||
await this.refresh(domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
||||
@@ -73,19 +73,21 @@ export default class CreateAction extends PropertiesAction {
|
||||
title: 'Saving'
|
||||
});
|
||||
|
||||
const success = await this.openmct.objects.save(this.domainObject);
|
||||
if (success) {
|
||||
try {
|
||||
await this.openmct.objects.save(this.domainObject);
|
||||
const compositionCollection = await this.openmct.composition.get(parentDomainObject);
|
||||
compositionCollection.add(this.domainObject);
|
||||
|
||||
this._navigateAndEdit(this.domainObject, parentDomainObjectPath);
|
||||
|
||||
this.openmct.notifications.info('Save successful');
|
||||
} else {
|
||||
this.openmct.notifications.error('Error saving objects');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.openmct.notifications.error(`Error saving objects: ${err}`);
|
||||
} finally {
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
tabindex="0"
|
||||
class="c-imagery"
|
||||
@keyup="arrowUpHandler"
|
||||
@keydown="arrowDownHandler"
|
||||
@keydown.prevent="arrowDownHandler"
|
||||
@mouseover="focusElement"
|
||||
>
|
||||
<div
|
||||
@@ -147,7 +147,7 @@
|
||||
v-if="!isFixed"
|
||||
class="c-button icon-pause pause-play"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@click="paused(!isPaused)"
|
||||
@click="handlePauseButton(!isPaused)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,6 +165,9 @@
|
||||
<div
|
||||
ref="thumbsWrapper"
|
||||
class="c-imagery__thumbs-scroll-area"
|
||||
:class="[{
|
||||
'animate-scroll': animateThumbScroll
|
||||
}]"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<ImageThumbnail
|
||||
@@ -182,7 +185,7 @@
|
||||
<button
|
||||
class="c-imagery__auto-scroll-resume-button c-icon-button icon-play"
|
||||
title="Resume automatic scrolling of image thumbnails"
|
||||
@click="scrollToRight('reset')"
|
||||
@click="scrollToRight"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,6 +195,7 @@
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import Vue from 'vue';
|
||||
|
||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
@@ -289,7 +293,8 @@ export default {
|
||||
pan: undefined,
|
||||
animateZoom: true,
|
||||
imagePanned: false,
|
||||
forceShowThumbnails: false
|
||||
forceShowThumbnails: false,
|
||||
animateThumbScroll: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -393,6 +398,12 @@ export default {
|
||||
|
||||
return disabled;
|
||||
},
|
||||
isComposedInLayout() {
|
||||
return (
|
||||
this.currentView?.objectPath
|
||||
&& !this.openmct.router.isNavigatedObject(this.currentView.objectPath)
|
||||
);
|
||||
},
|
||||
focusedImage() {
|
||||
return this.imageHistory[this.focusedImageIndex];
|
||||
},
|
||||
@@ -542,7 +553,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
imageHistory: {
|
||||
handler(newHistory, _oldHistory) {
|
||||
async handler(newHistory, oldHistory) {
|
||||
const newSize = newHistory.length;
|
||||
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
|
||||
if (this.focusedImageTimestamp !== undefined) {
|
||||
@@ -570,10 +581,13 @@ export default {
|
||||
|
||||
if (!this.isPaused) {
|
||||
this.setFocusedImage(imageIndex);
|
||||
this.scrollToRight();
|
||||
} else {
|
||||
this.scrollToFocused();
|
||||
}
|
||||
|
||||
await this.scrollHandler();
|
||||
if (oldHistory?.length > 0) {
|
||||
this.animateThumbScroll = true;
|
||||
}
|
||||
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
@@ -584,7 +598,7 @@ export default {
|
||||
this.getImageNaturalDimensions();
|
||||
},
|
||||
bounds() {
|
||||
this.scrollToFocused();
|
||||
this.scrollHandler();
|
||||
},
|
||||
isFixed(newValue) {
|
||||
const isRealTime = !newValue;
|
||||
@@ -848,6 +862,13 @@ export default {
|
||||
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
|
||||
this.autoScroll = !disableScroll;
|
||||
},
|
||||
handlePauseButton(newState) {
|
||||
this.paused(newState);
|
||||
if (newState) {
|
||||
// need to set the focused index or the paused focus will drift
|
||||
this.thumbnailClicked(this.focusedImageIndex);
|
||||
}
|
||||
},
|
||||
paused(state) {
|
||||
this.isPaused = Boolean(state);
|
||||
|
||||
@@ -855,7 +876,7 @@ export default {
|
||||
this.previousFocusedImage = null;
|
||||
this.setFocusedImage(this.nextImageIndex);
|
||||
this.autoScroll = true;
|
||||
this.scrollToRight();
|
||||
this.scrollHandler();
|
||||
}
|
||||
},
|
||||
scrollToFocused() {
|
||||
@@ -865,28 +886,43 @@ export default {
|
||||
}
|
||||
|
||||
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
|
||||
|
||||
if (domThumb) {
|
||||
domThumb.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
}
|
||||
},
|
||||
scrollToRight(type) {
|
||||
if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
|
||||
if (!domThumb) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollWidth = this.$refs.thumbsWrapper.scrollWidth || 0;
|
||||
// separate scrollTo function had to be implemented since scrollIntoView
|
||||
// caused undesirable behavior in layouts
|
||||
// and could not simply be scoped to the parent element
|
||||
if (this.isComposedInLayout) {
|
||||
const wrapperWidth = this.$refs.thumbsWrapper.clientWidth ?? 0;
|
||||
this.$refs.thumbsWrapper.scrollLeft = (
|
||||
domThumb.offsetLeft - (wrapperWidth - domThumb.clientWidth) / 2);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
domThumb.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
},
|
||||
async scrollToRight() {
|
||||
|
||||
const scrollWidth = this.$refs?.thumbsWrapper?.scrollWidth ?? 0;
|
||||
if (!scrollWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
|
||||
});
|
||||
await Vue.nextTick();
|
||||
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
|
||||
},
|
||||
scrollHandler() {
|
||||
if (this.isPaused) {
|
||||
return this.scrollToFocused();
|
||||
} else if (this.autoScroll) {
|
||||
return this.scrollToRight();
|
||||
}
|
||||
},
|
||||
matchIndexOfPreviousImage(previous, imageHistory) {
|
||||
// match logic uses a composite of url and time to account
|
||||
@@ -1087,7 +1123,7 @@ export default {
|
||||
this.setSizedImageDimensions();
|
||||
this.setImageViewport();
|
||||
this.calculateViewHeight();
|
||||
this.scrollToFocused();
|
||||
this.scrollHandler();
|
||||
},
|
||||
setSizedImageDimensions() {
|
||||
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
|
||||
@@ -1122,9 +1158,7 @@ export default {
|
||||
this.handleThumbWindowResizeEnded();
|
||||
},
|
||||
handleThumbWindowResizeEnded() {
|
||||
if (!this.isPaused) {
|
||||
this.scrollToRight('reset');
|
||||
}
|
||||
this.scrollHandler();
|
||||
|
||||
this.calculateViewHeight();
|
||||
|
||||
@@ -1137,7 +1171,6 @@ export default {
|
||||
},
|
||||
wheelZoom(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.$refs.imageControls.wheelZoom(e);
|
||||
},
|
||||
startPan(e) {
|
||||
|
||||
@@ -194,6 +194,9 @@
|
||||
overflow-y: hidden;
|
||||
margin-bottom: 1px;
|
||||
padding-bottom: $interiorMarginSm;
|
||||
&.animate-scroll {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
&__auto-scroll-resume-button {
|
||||
|
||||
@@ -481,19 +481,16 @@ describe("The Imagery View Layouts", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
|
||||
Vue.nextTick(() => {
|
||||
// use spyon to spy the scroll function
|
||||
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
|
||||
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
||||
Vue.nextTick(() => {
|
||||
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
|
||||
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it ('scrollToRight is called when clicking on auto scroll button', async () => {
|
||||
await Vue.nextTick();
|
||||
// use spyon to spy the scroll function
|
||||
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollHandler');
|
||||
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
||||
await Vue.nextTick();
|
||||
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
|
||||
expect(imageryView._getInstance().$refs.ImageryContainer.scrollHandler);
|
||||
});
|
||||
xit('should change the image zoom factor when using the zoom buttons', async (done) => {
|
||||
xit('should change the image zoom factor when using the zoom buttons', async () => {
|
||||
await Vue.nextTick();
|
||||
let imageSizeBefore;
|
||||
let imageSizeAfter;
|
||||
@@ -512,7 +509,6 @@ describe("The Imagery View Layouts", () => {
|
||||
imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
|
||||
expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);
|
||||
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
|
||||
done();
|
||||
});
|
||||
xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {
|
||||
await Vue.nextTick();
|
||||
|
||||
@@ -234,7 +234,8 @@ class CouchObjectProvider {
|
||||
#handleResponseCode(status, json, fetchOptions) {
|
||||
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
|
||||
if (status === CouchObjectProvider.HTTP_CONFLICT) {
|
||||
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
|
||||
const objectName = JSON.parse(fetchOptions.body)?.model?.name;
|
||||
throw new this.openmct.objects.errors.Conflict(`Conflict persisting "${objectName}"`);
|
||||
} else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {
|
||||
if (!json.error || !json.reason) {
|
||||
throw new Error(`CouchDB Error ${status}`);
|
||||
|
||||
@@ -34,23 +34,26 @@
|
||||
@legendHoverChanged="legendHoverChanged"
|
||||
/>
|
||||
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
|
||||
<y-axis
|
||||
v-if="seriesModels.length > 0"
|
||||
:tick-width="tickWidth"
|
||||
:single-series="seriesModels.length === 1"
|
||||
:has-same-range-value="hasSameRangeValue"
|
||||
:series-model="seriesModels[0]"
|
||||
:style="{
|
||||
left: (plotWidth - tickWidth) + 'px'
|
||||
}"
|
||||
@yKeyChanged="setYAxisKey"
|
||||
@tickWidthChanged="onTickWidthChange"
|
||||
/>
|
||||
<div
|
||||
v-if="seriesModels.length"
|
||||
class="u-contents"
|
||||
>
|
||||
<y-axis
|
||||
v-for="(yAxis, index) in yAxesIds"
|
||||
:id="yAxis.id"
|
||||
:key="`yAxis-${index}`"
|
||||
:multiple-left-axes="multipleLeftAxes"
|
||||
:position="yAxis.id > 2 ? 'right' : 'left'"
|
||||
:class="{'plot-yaxis-right': yAxis.id > 2}"
|
||||
:tick-width="yAxis.tickWidth"
|
||||
:plot-left-tick-width="yAxis.id > 2 ? yAxis.tickWidth: plotLeftTickWidth"
|
||||
@yKeyChanged="setYAxisKey"
|
||||
@tickWidthChanged="onTickWidthChange"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="gl-plot-wrapper-display-area-and-x-axis"
|
||||
:style="{
|
||||
left: (plotWidth + 20) + 'px'
|
||||
}"
|
||||
:style="xAxisStyle"
|
||||
>
|
||||
|
||||
<div class="gl-plot-display-area has-local-controls has-cursor-guides">
|
||||
@@ -69,9 +72,12 @@
|
||||
/>
|
||||
|
||||
<mct-ticks
|
||||
v-for="(yAxis, index) in yAxesIds"
|
||||
v-show="gridLines"
|
||||
:key="`yAxis-gridlines-${index}`"
|
||||
:axis-type="'yAxis'"
|
||||
:position="'bottom'"
|
||||
:axis-id="yAxis.id"
|
||||
@plotTickWidth="onTickWidthChange"
|
||||
/>
|
||||
|
||||
@@ -214,6 +220,7 @@ import YAxis from "./axis/YAxis.vue";
|
||||
import _ from "lodash";
|
||||
|
||||
const OFFSET_THRESHOLD = 10;
|
||||
const AXES_PADDING = 20;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -269,7 +276,6 @@ export default {
|
||||
altPressed: false,
|
||||
highlights: [],
|
||||
lockHighlightPoint: false,
|
||||
tickWidth: 0,
|
||||
yKeyOptions: [],
|
||||
yAxisLabel: '',
|
||||
rectangles: [],
|
||||
@@ -284,12 +290,31 @@ export default {
|
||||
isTimeOutOfSync: false,
|
||||
showLimitLineLabels: this.limitLineLabels,
|
||||
isFrozenOnMouseDown: false,
|
||||
hasSameRangeValue: true,
|
||||
cursorGuide: this.initCursorGuide,
|
||||
gridLines: this.initGridLines
|
||||
gridLines: this.initGridLines,
|
||||
yAxes: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
xAxisStyle() {
|
||||
const rightAxis = this.yAxesIds.find(yAxis => yAxis.id > 2);
|
||||
const leftOffset = this.multipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING;
|
||||
let style = {
|
||||
left: `${this.plotLeftTickWidth + leftOffset}px`
|
||||
};
|
||||
|
||||
if (rightAxis) {
|
||||
style.right = `${rightAxis.tickWidth + AXES_PADDING}px`;
|
||||
}
|
||||
|
||||
return style;
|
||||
},
|
||||
yAxesIds() {
|
||||
return this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
|
||||
},
|
||||
multipleLeftAxes() {
|
||||
return this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1;
|
||||
},
|
||||
isNestedWithinAStackedPlot() {
|
||||
const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path));
|
||||
|
||||
@@ -312,8 +337,17 @@ export default {
|
||||
return 'plot-legend-collapsed';
|
||||
}
|
||||
},
|
||||
plotWidth() {
|
||||
return this.plotTickWidth || this.tickWidth;
|
||||
plotLeftTickWidth() {
|
||||
let leftTickWidth = 0;
|
||||
this.yAxes.forEach((yAxis) => {
|
||||
if (yAxis.id > 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
leftTickWidth = leftTickWidth + yAxis.tickWidth;
|
||||
});
|
||||
|
||||
return this.plotTickWidth || leftTickWidth;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -342,6 +376,20 @@ export default {
|
||||
|
||||
this.config = this.getConfig();
|
||||
this.legend = this.config.legend;
|
||||
this.yAxes = [{
|
||||
id: this.config.yAxis.id,
|
||||
seriesCount: 0,
|
||||
tickWidth: 0
|
||||
}];
|
||||
if (this.config.additionalYAxes) {
|
||||
this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => {
|
||||
return {
|
||||
id: yAxis.id,
|
||||
seriesCount: 0,
|
||||
tickWidth: 0
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.isNestedWithinAStackedPlot) {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
@@ -383,8 +431,10 @@ export default {
|
||||
},
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||
this.followTimeContext();
|
||||
|
||||
},
|
||||
followTimeContext() {
|
||||
this.updateDisplayBounds(this.timeContext.bounds());
|
||||
@@ -417,12 +467,13 @@ export default {
|
||||
return config;
|
||||
},
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
this.$set(this.seriesModels, index, series);
|
||||
this.listenTo(series, 'change:xKey', (xKey) => {
|
||||
this.setDisplayRange(series, xKey);
|
||||
}, this);
|
||||
this.listenTo(series, 'change:yKey', () => {
|
||||
this.checkSameRangeValue();
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
|
||||
@@ -430,20 +481,21 @@ export default {
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
|
||||
this.checkSameRangeValue();
|
||||
this.loadSeriesData(series);
|
||||
},
|
||||
|
||||
checkSameRangeValue() {
|
||||
this.hasSameRangeValue = this.seriesModels.every((model) => {
|
||||
return model.get('yKey') === this.seriesModels[0].get('yKey');
|
||||
});
|
||||
removeSeries(plotSeries, index) {
|
||||
const yAxisId = plotSeries.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, -1);
|
||||
this.seriesModels.splice(index, 1);
|
||||
this.stopListening(plotSeries);
|
||||
},
|
||||
|
||||
removeSeries(plotSeries, index) {
|
||||
this.seriesModels.splice(index, 1);
|
||||
this.checkSameRangeValue();
|
||||
this.stopListening(plotSeries);
|
||||
updateAxisUsageCount(yAxisId, updateCountBy) {
|
||||
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy;
|
||||
}
|
||||
},
|
||||
|
||||
loadSeriesData(series) {
|
||||
@@ -673,6 +725,7 @@ export default {
|
||||
|
||||
// Setup canvas etc.
|
||||
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
|
||||
//TODO: handle yScale, zoom/pan for all yAxes
|
||||
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
|
||||
|
||||
this.pan = undefined;
|
||||
@@ -690,6 +743,9 @@ export default {
|
||||
|
||||
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
|
||||
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange, this);
|
||||
});
|
||||
},
|
||||
|
||||
onXAxisChange(displayBounds) {
|
||||
@@ -704,20 +760,24 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
onTickWidthChange(width, fromDifferentObject) {
|
||||
if (fromDifferentObject) {
|
||||
onTickWidthChange(data, fromDifferentObject) {
|
||||
const {width, yAxisId} = data;
|
||||
if (yAxisId) {
|
||||
const index = this.yAxes.findIndex(yAxis => yAxis.id === yAxisId);
|
||||
if (fromDifferentObject) {
|
||||
// Always accept tick width if it comes from a different object.
|
||||
this.tickWidth = width;
|
||||
} else {
|
||||
this.yAxes[index].tickWidth = width;
|
||||
} else {
|
||||
// Otherwise, only accept tick with if it's larger.
|
||||
const newWidth = Math.max(width, this.tickWidth);
|
||||
if (newWidth !== this.tickWidth) {
|
||||
this.tickWidth = newWidth;
|
||||
const newWidth = Math.max(width, this.yAxes[index].tickWidth);
|
||||
if (newWidth !== this.yAxes[index].tickWidth) {
|
||||
this.yAxes[index].tickWidth = newWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.$emit('plotTickWidth', this.tickWidth, id);
|
||||
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id);
|
||||
}
|
||||
},
|
||||
|
||||
trackMousePosition(event) {
|
||||
@@ -1108,8 +1168,9 @@ export default {
|
||||
this.userViewportChangeEnd();
|
||||
},
|
||||
|
||||
setYAxisKey(yKey) {
|
||||
this.config.series.models[0].set('yKey', yKey);
|
||||
setYAxisKey(yKey, yAxisId) {
|
||||
const seriesForYAxis = this.config.series.models.filter((model => model.get('yAxisId') === yAxisId));
|
||||
seriesForYAxis.forEach(model => model.set('yKey', yKey));
|
||||
},
|
||||
|
||||
pause() {
|
||||
|
||||
@@ -103,6 +103,12 @@ export default {
|
||||
return 6;
|
||||
}
|
||||
},
|
||||
axisId: {
|
||||
type: Number,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
position: {
|
||||
required: true,
|
||||
type: String,
|
||||
@@ -145,7 +151,15 @@ export default {
|
||||
throw new Error('config is missing');
|
||||
}
|
||||
|
||||
return config[this.axisType];
|
||||
if (this.axisType === 'yAxis') {
|
||||
if (this.axisId && this.axisId !== config.yAxis.id) {
|
||||
return config.additionalYAxes.find(axis => axis.id === this.axisId);
|
||||
} else {
|
||||
return config.yAxis;
|
||||
}
|
||||
} else {
|
||||
return config[this.axisType];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determine whether ticks should be regenerated for a given range.
|
||||
@@ -258,7 +272,10 @@ export default {
|
||||
}, 0));
|
||||
|
||||
this.tickWidth = tickWidth;
|
||||
this.$emit('plotTickWidth', tickWidth);
|
||||
this.$emit('plotTickWidth', {
|
||||
width: tickWidth,
|
||||
yAxisId: this.axisType === 'yAxis' ? this.axisId : ''
|
||||
});
|
||||
this.shouldCheckWidth = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,8 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="gl-plot-axis-area gl-plot-y has-local-controls"
|
||||
:style="{
|
||||
width: (tickWidth + 20) + 'px'
|
||||
}"
|
||||
class="gl-plot-axis-area gl-plot-y has-local-controls js-plot-y-axis"
|
||||
:style="yAxisStyle"
|
||||
>
|
||||
|
||||
<div
|
||||
@@ -52,6 +50,7 @@
|
||||
</select>
|
||||
|
||||
<mct-ticks
|
||||
:axis-id="id"
|
||||
:axis-type="'yAxis'"
|
||||
class="gl-plot-ticks"
|
||||
:position="'top'"
|
||||
@@ -63,6 +62,10 @@
|
||||
<script>
|
||||
import MctTicks from "../MctTicks.vue";
|
||||
import configStore from "../configuration/ConfigStore";
|
||||
import eventHelpers from "../lib/eventHelpers";
|
||||
|
||||
const AXIS_PADDING = 20;
|
||||
const AXIS_OFFSET = 5;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -70,22 +73,10 @@ export default {
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
singleSeries: {
|
||||
type: Boolean,
|
||||
id: {
|
||||
type: Number,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
hasSameRangeValue: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
seriesModel: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
tickWidth: {
|
||||
@@ -93,37 +84,132 @@ export default {
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
plotLeftTickWidth: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
multipleLeftAxes: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
position: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'left';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
this.seriesModels = [];
|
||||
|
||||
return {
|
||||
yAxisLabel: 'none',
|
||||
loaded: false
|
||||
loaded: false,
|
||||
yKeyOptions: [],
|
||||
hasSameRangeValue: true,
|
||||
singleSeries: true,
|
||||
mainYAxisId: null,
|
||||
hasAdditionalYAxes: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canShowYAxisLabel() {
|
||||
return this.singleSeries === true || this.hasSameRangeValue === true;
|
||||
},
|
||||
yAxisStyle() {
|
||||
let style = {
|
||||
width: `${this.tickWidth + AXIS_PADDING}px`
|
||||
};
|
||||
const multipleAxesPadding = this.multipleLeftAxes ? 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 = `${ this.plotLeftTickWidth - this.tickWidth - multipleAxesPadding - AXIS_OFFSET }px`;
|
||||
style['border-right'] = `1px solid`;
|
||||
} else {
|
||||
style.left = `${this.plotLeftTickWidth - this.tickWidth + multipleAxesPadding}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.yAxis = this.getYAxisFromConfig();
|
||||
eventHelpers.extend(this);
|
||||
this.initAxisAndSeriesConfig();
|
||||
this.loaded = true;
|
||||
this.setUpYAxisOptions();
|
||||
},
|
||||
methods: {
|
||||
getYAxisFromConfig() {
|
||||
initAxisAndSeriesConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
if (config) {
|
||||
return config.yAxis;
|
||||
this.mainYAxisId = config.yAxis.id;
|
||||
this.hasAdditionalYAxes = config?.additionalYAxes.length;
|
||||
if (this.id && this.id !== this.mainYAxisId) {
|
||||
this.yAxis = config.additionalYAxes.find(yAxis => yAxis.id === this.id);
|
||||
} else {
|
||||
this.yAxis = config.yAxis;
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
this.listenTo(this.config.series, 'reorder', this.addOrRemoveSeries, this);
|
||||
|
||||
this.config.series.models.forEach(this.addSeries, this);
|
||||
}
|
||||
},
|
||||
addOrRemoveSeries(series) {
|
||||
const yAxisId = this.series.get('yAxisId');
|
||||
if (yAxisId === this.id) {
|
||||
this.addSeries(series);
|
||||
} else {
|
||||
this.removeSeries(series);
|
||||
}
|
||||
},
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), series.get('identifier')));
|
||||
|
||||
if (yAxisId === this.id && seriesIndex < 0) {
|
||||
this.seriesModels.push(series);
|
||||
this.checkRangeValueAndSingleSeries();
|
||||
this.setUpYAxisOptions();
|
||||
}
|
||||
},
|
||||
removeSeries(plotSeries) {
|
||||
const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), plotSeries.get('identifier')));
|
||||
if (seriesIndex > -1) {
|
||||
this.seriesModels.splice(seriesIndex, 1);
|
||||
this.checkRangeValueAndSingleSeries();
|
||||
this.setUpYAxisOptions();
|
||||
}
|
||||
},
|
||||
checkRangeValueAndSingleSeries() {
|
||||
this.hasSameRangeValue = this.seriesModels.every((model) => {
|
||||
return model.get('yKey') === this.seriesModels[0].get('yKey');
|
||||
});
|
||||
this.singleSeries = this.seriesModels.length === 1;
|
||||
},
|
||||
setUpYAxisOptions() {
|
||||
this.yKeyOptions = [];
|
||||
if (!this.seriesModels.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.seriesModel.metadata) {
|
||||
this.yKeyOptions = this.seriesModel.metadata
|
||||
const seriesModel = this.seriesModels[0];
|
||||
if (seriesModel.metadata) {
|
||||
this.yKeyOptions = seriesModel.metadata
|
||||
.valuesForHints(['range'])
|
||||
.map(function (o) {
|
||||
return {
|
||||
@@ -135,22 +221,22 @@ export default {
|
||||
|
||||
// set yAxisLabel if none is set yet
|
||||
if (this.yAxisLabel === 'none') {
|
||||
let yKey = this.seriesModel.model.yKey;
|
||||
let yKeyModel = this.yKeyOptions.filter(o => o.key === yKey)[0];
|
||||
|
||||
this.yAxisLabel = yKeyModel ? yKeyModel.name : '';
|
||||
this.yAxisLabel = this.yAxis.get('label');
|
||||
}
|
||||
},
|
||||
toggleYAxisLabel() {
|
||||
let yAxisObject = this.yKeyOptions.filter(o => o.name === this.yAxisLabel)[0];
|
||||
|
||||
if (yAxisObject) {
|
||||
this.$emit('yKeyChanged', yAxisObject.key);
|
||||
this.$emit('yKeyChanged', yAxisObject.key, this.id);
|
||||
this.yAxis.set('label', this.yAxisLabel);
|
||||
}
|
||||
},
|
||||
onTickWidthChange(width) {
|
||||
this.$emit('tickWidthChanged', width);
|
||||
onTickWidthChange(data) {
|
||||
this.$emit('tickWidthChanged', {
|
||||
width: data.width,
|
||||
yAxisId: this.id
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,7 +98,21 @@ export default {
|
||||
this.limitLines = [];
|
||||
this.pointSets = [];
|
||||
this.alarmSets = [];
|
||||
this.offset = {};
|
||||
const yAxisId = this.config.yAxis.get('id');
|
||||
this.offset = {
|
||||
[yAxisId]: {}
|
||||
};
|
||||
this.listenTo(this.config.yAxis, 'change:key', this.resetOffsetAndSeriesDataForYAxis.bind(this, yAxisId), this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw);
|
||||
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(this.config.yAxis, 'change:key', this.resetOffsetAndSeriesDataForYAxis.bind(this, id), this);
|
||||
});
|
||||
}
|
||||
|
||||
this.seriesElements = new WeakMap();
|
||||
this.seriesLimits = new WeakMap();
|
||||
|
||||
@@ -111,8 +125,7 @@ export default {
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
|
||||
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
|
||||
this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw);
|
||||
|
||||
this.listenTo(this.config.xAxis, 'change', this.updateLimitsAndDraw);
|
||||
this.config.series.forEach(this.onSeriesAdd, this);
|
||||
this.$emit('chartLoaded');
|
||||
@@ -224,25 +237,31 @@ export default {
|
||||
this.limitLines.forEach(line => line.destroy());
|
||||
DrawLoader.releaseDrawAPI(this.drawAPI);
|
||||
},
|
||||
clearOffset() {
|
||||
delete this.offset.x;
|
||||
delete this.offset.y;
|
||||
delete this.offset.xVal;
|
||||
delete this.offset.yVal;
|
||||
delete this.offset.xKey;
|
||||
delete this.offset.yKey;
|
||||
this.lines.forEach(function (line) {
|
||||
resetOffsetAndSeriesDataForYAxis(yAxisId) {
|
||||
delete this.offset[yAxisId].x;
|
||||
delete this.offset[yAxisId].y;
|
||||
delete this.offset[yAxisId].xVal;
|
||||
delete this.offset[yAxisId].yVal;
|
||||
delete this.offset[yAxisId].xKey;
|
||||
delete this.offset[yAxisId].yKey;
|
||||
|
||||
const lines = this.lines.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
lines.forEach(function (line) {
|
||||
line.reset();
|
||||
});
|
||||
this.limitLines.forEach(function (line) {
|
||||
const limitLines = this.limitLines.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
limitLines.forEach(function (line) {
|
||||
line.reset();
|
||||
});
|
||||
this.pointSets.forEach(function (pointSet) {
|
||||
const pointSets = this.pointSets.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
pointSets.forEach(function (pointSet) {
|
||||
pointSet.reset();
|
||||
});
|
||||
},
|
||||
setOffset(offsetPoint, index, series) {
|
||||
if (this.offset.x && this.offset.y) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
if (this.offset[yAxisId].x && this.offset[yAxisId].y) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -251,19 +270,20 @@ export default {
|
||||
y: series.getYVal(offsetPoint)
|
||||
};
|
||||
|
||||
this.offset.x = function (x) {
|
||||
this.offset[yAxisId].x = function (x) {
|
||||
return x - offsets.x;
|
||||
}.bind(this);
|
||||
this.offset.y = function (y) {
|
||||
this.offset[yAxisId].y = function (y) {
|
||||
return y - offsets.y;
|
||||
}.bind(this);
|
||||
this.offset.xVal = function (point, pSeries) {
|
||||
return this.offset.x(pSeries.getXVal(point));
|
||||
this.offset[yAxisId].xVal = function (point, pSeries) {
|
||||
return this.offset[yAxisId].x(pSeries.getXVal(point));
|
||||
}.bind(this);
|
||||
this.offset.yVal = function (point, pSeries) {
|
||||
return this.offset.y(pSeries.getYVal(point));
|
||||
this.offset[yAxisId].yVal = function (point, pSeries) {
|
||||
return this.offset[yAxisId].y(pSeries.getYVal(point));
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
initializeCanvas(canvas, overlay) {
|
||||
this.canvas = canvas;
|
||||
this.overlay = overlay;
|
||||
@@ -311,11 +331,15 @@ export default {
|
||||
this.clearLimitLines(series);
|
||||
},
|
||||
lineForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
if (series.get('interpolate') === 'linear') {
|
||||
return new MCTChartLineLinear(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
|
||||
@@ -323,33 +347,45 @@ export default {
|
||||
return new MCTChartLineStepAfter(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
},
|
||||
limitLineForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
return new MCTChartAlarmLineSet(
|
||||
series,
|
||||
this,
|
||||
this.offset,
|
||||
offset,
|
||||
this.openmct.time.bounds()
|
||||
);
|
||||
},
|
||||
pointSetForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
if (series.get('markers')) {
|
||||
return new MCTChartPointSet(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
},
|
||||
alarmPointSetForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
if (series.get('alarmMarkers')) {
|
||||
return new MCTChartAlarmPointSet(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -410,8 +446,8 @@ export default {
|
||||
this.seriesLimits.delete(series);
|
||||
}
|
||||
},
|
||||
canDraw() {
|
||||
if (!this.offset.x || !this.offset.y) {
|
||||
canDraw(yAxisId) {
|
||||
if (!this.offset[yAxisId] || !this.offset[yAxisId].x || !this.offset[yAxisId].y) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -434,16 +470,31 @@ export default {
|
||||
}
|
||||
|
||||
this.drawAPI.clear();
|
||||
if (this.canDraw()) {
|
||||
this.updateViewport();
|
||||
this.drawSeries();
|
||||
this.drawRectangles();
|
||||
this.drawHighlights();
|
||||
}
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
//There has to be at least one yAxis
|
||||
const yAxisIds = [mainYAxisId].concat(this.config.additionalYAxes.map(yAxis => yAxis.get('id')));
|
||||
// Repeat drawing for all yAxes
|
||||
yAxisIds.forEach((id) => {
|
||||
if (this.canDraw(id)) {
|
||||
this.updateViewport(id);
|
||||
this.drawSeries(id);
|
||||
this.drawRectangles(id);
|
||||
this.drawHighlights(id);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateViewport() {
|
||||
updateViewport(yAxisId) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
let yRange;
|
||||
if (yAxisId === mainYAxisId) {
|
||||
yRange = this.config.yAxis.get('displayRange');
|
||||
} else {
|
||||
if (this.config.additionalYAxes.length) {
|
||||
const yAxisForId = this.config.additionalYAxes.find(yAxis => yAxis.get('id') === yAxisId);
|
||||
yRange = yAxisForId.get('displayRange');
|
||||
}
|
||||
}
|
||||
|
||||
if (!xRange || !yRange) {
|
||||
return;
|
||||
@@ -454,9 +505,10 @@ export default {
|
||||
yRange.max - yRange.min
|
||||
];
|
||||
|
||||
const origin = [
|
||||
this.offset.x(xRange.min),
|
||||
this.offset.y(yRange.min)
|
||||
let origin;
|
||||
origin = [
|
||||
this.offset[yAxisId].x(xRange.min),
|
||||
this.offset[yAxisId].y(yRange.min)
|
||||
];
|
||||
|
||||
this.drawAPI.setDimensions(
|
||||
@@ -464,38 +516,66 @@ export default {
|
||||
origin
|
||||
);
|
||||
},
|
||||
drawSeries() {
|
||||
this.lines.forEach(this.drawLine, this);
|
||||
this.pointSets.forEach(this.drawPoints, this);
|
||||
this.alarmSets.forEach(this.drawAlarmPoints, this);
|
||||
matchByYAxisId(id, item) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
let matchesId = false;
|
||||
|
||||
const series = item.series;
|
||||
if (series) {
|
||||
const seriesYAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
|
||||
matchesId = seriesYAxisId === id;
|
||||
}
|
||||
|
||||
return matchesId;
|
||||
},
|
||||
drawSeries(id) {
|
||||
const lines = this.lines.filter(this.matchByYAxisId.bind(this, id));
|
||||
lines.forEach(this.drawLine, this);
|
||||
const pointSets = this.pointSets.filter(this.matchByYAxisId.bind(this, id));
|
||||
pointSets.forEach(this.drawPoints, this);
|
||||
const alarmSets = this.alarmSets.filter(this.matchByYAxisId.bind(this, id));
|
||||
alarmSets.forEach(this.drawAlarmPoints, this);
|
||||
},
|
||||
drawLimitLines() {
|
||||
if (this.canDraw()) {
|
||||
this.updateViewport();
|
||||
|
||||
if (!this.drawAPI.origin) {
|
||||
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) => {
|
||||
const showLabels = this.showLabels(limit.seriesKey);
|
||||
if (showLabels) {
|
||||
const overlap = this.getLimitOverlap(limit, limitPointOverlap);
|
||||
limitPointOverlap.push(overlap);
|
||||
let limitLabelEl = this.getLimitLabel(limit, overlap);
|
||||
limitContainerEl.appendChild(limitLabelEl);
|
||||
}
|
||||
|
||||
let limitEl = this.getLimitElement(limit);
|
||||
limitContainerEl.appendChild(limitEl);
|
||||
|
||||
}, this);
|
||||
});
|
||||
this.config.series.models.forEach(series => {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.drawLimitLinesForSeries(yAxisId, series);
|
||||
});
|
||||
},
|
||||
drawLimitLinesForSeries(yAxisId, series) {
|
||||
if (!this.canDraw(yAxisId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateViewport(yAxisId);
|
||||
|
||||
if (!this.drawAPI.origin) {
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showLabels = this.showLabels(limit.seriesKey);
|
||||
if (showLabels) {
|
||||
const overlap = this.getLimitOverlap(limit, limitPointOverlap);
|
||||
limitPointOverlap.push(overlap);
|
||||
let limitLabelEl = this.getLimitLabel(limit, overlap);
|
||||
limitContainerEl.appendChild(limitLabelEl);
|
||||
}
|
||||
|
||||
let limitEl = this.getLimitElement(limit);
|
||||
limitContainerEl.appendChild(limitEl);
|
||||
|
||||
}, this);
|
||||
});
|
||||
},
|
||||
showLabels(seriesKey) {
|
||||
return this.showLimitLineLabels.seriesKey
|
||||
@@ -577,22 +657,25 @@ export default {
|
||||
);
|
||||
},
|
||||
drawLine(chartElement, disconnected) {
|
||||
this.drawAPI.drawLine(
|
||||
chartElement.getBuffer(),
|
||||
chartElement.color().asRGBAArray(),
|
||||
chartElement.count,
|
||||
disconnected
|
||||
);
|
||||
},
|
||||
drawHighlights() {
|
||||
if (this.highlights && this.highlights.length) {
|
||||
this.highlights.forEach(this.drawHighlight, this);
|
||||
if (chartElement) {
|
||||
this.drawAPI.drawLine(
|
||||
chartElement.getBuffer(),
|
||||
chartElement.color().asRGBAArray(),
|
||||
chartElement.count,
|
||||
disconnected
|
||||
);
|
||||
}
|
||||
},
|
||||
drawHighlight(highlight) {
|
||||
drawHighlights(yAxisId) {
|
||||
if (this.highlights && this.highlights.length) {
|
||||
const highlights = this.highlights.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
highlights.forEach(this.drawHighlight.bind(this, yAxisId), this);
|
||||
}
|
||||
},
|
||||
drawHighlight(yAxisId, highlight) {
|
||||
const points = new Float32Array([
|
||||
this.offset.xVal(highlight.point, highlight.series),
|
||||
this.offset.yVal(highlight.point, highlight.series)
|
||||
this.offset[yAxisId].xVal(highlight.point, highlight.series),
|
||||
this.offset[yAxisId].yVal(highlight.point, highlight.series)
|
||||
]);
|
||||
|
||||
const color = highlight.series.get('color').asRGBAArray();
|
||||
@@ -601,20 +684,21 @@ export default {
|
||||
|
||||
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
|
||||
},
|
||||
drawRectangles() {
|
||||
drawRectangles(yAxisId) {
|
||||
if (this.rectangles) {
|
||||
this.rectangles.forEach(this.drawRectangle, this);
|
||||
const rectangles = this.rectangles.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
rectangles.forEach(this.drawRectangle.bind(this, yAxisId), this);
|
||||
}
|
||||
},
|
||||
drawRectangle(rect) {
|
||||
drawRectangle(yAxisId, rect) {
|
||||
this.drawAPI.drawSquare(
|
||||
[
|
||||
this.offset.x(rect.start.x),
|
||||
this.offset.y(rect.start.y)
|
||||
this.offset[yAxisId].x(rect.start.x),
|
||||
this.offset[yAxisId].y(rect.start.y)
|
||||
],
|
||||
[
|
||||
this.offset.x(rect.end.x),
|
||||
this.offset.y(rect.end.y)
|
||||
this.offset[yAxisId].x(rect.end.x),
|
||||
this.offset[yAxisId].y(rect.end.y)
|
||||
],
|
||||
rect.color
|
||||
);
|
||||
|
||||
@@ -27,6 +27,10 @@ import XAxisModel from "./XAxisModel";
|
||||
import YAxisModel from "./YAxisModel";
|
||||
import LegendModel from "./LegendModel";
|
||||
|
||||
const MAX_Y_AXES = 3;
|
||||
const MAIN_Y_AXES_ID = 1;
|
||||
const MAX_ADDITIONAL_AXES = MAX_Y_AXES - 1;
|
||||
|
||||
/**
|
||||
* PlotConfiguration model stores the configuration of a plot and some
|
||||
* limited state. The indiidual parts of the plot configuration model
|
||||
@@ -58,8 +62,35 @@ export default class PlotConfigurationModel extends Model {
|
||||
this.yAxis = new YAxisModel({
|
||||
model: options.model.yAxis,
|
||||
plot: this,
|
||||
openmct: options.openmct
|
||||
openmct: options.openmct,
|
||||
id: options.model.yAxis.id || MAIN_Y_AXES_ID
|
||||
});
|
||||
//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];
|
||||
this.additionalYAxes.push(new YAxisModel({
|
||||
model: yAxis,
|
||||
plot: this,
|
||||
openmct: options.openmct,
|
||||
id: yAxis.id || (MAIN_Y_AXES_ID + yAxisCount + 1)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 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({
|
||||
model: options.model.legend,
|
||||
plot: this,
|
||||
@@ -81,6 +112,9 @@ export default class PlotConfigurationModel extends Model {
|
||||
}
|
||||
|
||||
this.yAxis.listenToSeriesCollection(this.series);
|
||||
this.additionalYAxes.forEach(yAxis => {
|
||||
yAxis.listenToSeriesCollection(this.series);
|
||||
});
|
||||
this.legend.listenToSeriesCollection(this.series);
|
||||
|
||||
this.listenTo(this, 'destroy', this.onDestroy, this);
|
||||
@@ -145,6 +179,7 @@ export default class PlotConfigurationModel extends Model {
|
||||
domainObject: options.domainObject,
|
||||
xAxis: {},
|
||||
yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}),
|
||||
additionalYAxes: _.cloneDeep(options.domainObject.configuration?.additionalYAxes ?? []),
|
||||
legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@ export default class PlotSeries extends Model {
|
||||
// Model.apply(this, arguments);
|
||||
this.onXKeyChange(this.get('xKey'));
|
||||
this.onYKeyChange(this.get('yKey'));
|
||||
|
||||
this.unPlottableValues = [undefined, Infinity, -Infinity];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +118,8 @@ export default class PlotSeries extends Model {
|
||||
markerShape: 'point',
|
||||
markerSize: 2.0,
|
||||
alarmMarkers: true,
|
||||
limitLines: false
|
||||
limitLines: false,
|
||||
yAxisId: options.model.yAxisId || 1
|
||||
};
|
||||
}
|
||||
|
||||
@@ -342,6 +345,10 @@ export default class PlotSeries extends Model {
|
||||
let stats = this.get('stats');
|
||||
let changed = false;
|
||||
if (!stats) {
|
||||
if ([Infinity, -Infinity].includes(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
stats = {
|
||||
minValue: value,
|
||||
minPoint: point,
|
||||
@@ -350,13 +357,13 @@ export default class PlotSeries extends Model {
|
||||
};
|
||||
changed = true;
|
||||
} else {
|
||||
if (stats.maxValue < value) {
|
||||
if (stats.maxValue < value && value !== Infinity) {
|
||||
stats.maxValue = value;
|
||||
stats.maxPoint = point;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (stats.minValue > value) {
|
||||
if (stats.minValue > value && value !== -Infinity) {
|
||||
stats.minValue = value;
|
||||
stats.minPoint = point;
|
||||
changed = true;
|
||||
@@ -419,7 +426,7 @@ export default class PlotSeries extends Model {
|
||||
* @private
|
||||
*/
|
||||
isValueInvalid(val) {
|
||||
return Number.isNaN(val) || val === undefined;
|
||||
return Number.isNaN(val) || this.unPlottableValues.includes(val);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -135,18 +135,44 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
}
|
||||
resetStats() {
|
||||
//TODO: do we need the series id here?
|
||||
this.unset('stats');
|
||||
this.seriesCollection.forEach(series => {
|
||||
this.getSeriesForYAxis(this.seriesCollection).forEach(series => {
|
||||
if (series.has('stats')) {
|
||||
this.updateStats(series.get('stats'));
|
||||
}
|
||||
});
|
||||
}
|
||||
getSeriesForYAxis(seriesCollection) {
|
||||
return seriesCollection.filter(series => {
|
||||
const seriesYAxisId = series.get('yAxisId') || 1;
|
||||
|
||||
return seriesYAxisId === this.id;
|
||||
});
|
||||
}
|
||||
|
||||
getYAxisForId(id) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
let yAxis;
|
||||
if (this.id === 1) {
|
||||
yAxis = plotModel.configuration?.yAxis;
|
||||
} else {
|
||||
if (plotModel.configuration?.additionalYAxes) {
|
||||
yAxis = plotModel.configuration.additionalYAxes.find(additionalYAxis => additionalYAxis.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
return yAxis;
|
||||
}
|
||||
/**
|
||||
* @param {import('./PlotSeries').default} series
|
||||
*/
|
||||
trackSeries(series) {
|
||||
this.listenTo(series, 'change:stats', seriesStats => {
|
||||
if (series.get('yAxisId') !== this.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!seriesStats) {
|
||||
this.resetStats();
|
||||
} else {
|
||||
@@ -154,6 +180,10 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
});
|
||||
this.listenTo(series, 'change:yKey', () => {
|
||||
if (series.get('yAxisId') !== this.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
});
|
||||
}
|
||||
@@ -252,14 +282,40 @@ export default class YAxisModel extends Model {
|
||||
// Update the series collection labels and formatting
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given series collection, get the metadata of the current yKey for each series.
|
||||
* Then return first available value of the given property from the metadata.
|
||||
* @param {import('./SeriesCollection').default} series
|
||||
* @param {String} property
|
||||
*/
|
||||
getMetadataValueByProperty(series, property) {
|
||||
return series.map(s => (s.metadata ? s.metadata.value(s.get('yKey'))[property] : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
}
|
||||
/**
|
||||
* Update yAxis format, values, and label from known series.
|
||||
* @param {import('./SeriesCollection').default} seriesCollection
|
||||
*/
|
||||
updateFromSeries(seriesCollection) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
const label = plotModel.configuration?.yAxis?.label;
|
||||
const sampleSeries = seriesCollection.first();
|
||||
const seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection);
|
||||
if (!seriesForThisYAxis.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yAxis = this.getYAxisForId(this.id);
|
||||
const label = yAxis?.label;
|
||||
const sampleSeries = seriesForThisYAxis[0];
|
||||
if (!sampleSeries || !sampleSeries.metadata) {
|
||||
if (!label) {
|
||||
this.unset('label');
|
||||
@@ -279,41 +335,17 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
|
||||
this.set('values', yMetadata.values);
|
||||
|
||||
if (!label) {
|
||||
const labelName = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).name : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name');
|
||||
if (labelName) {
|
||||
this.set('label', labelName);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const labelUnits = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).units : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
//if the name is not available, set the units as the label
|
||||
const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units');
|
||||
if (labelUnits) {
|
||||
this.set('label', labelUnits);
|
||||
|
||||
@@ -331,7 +363,8 @@ export default class YAxisModel extends Model {
|
||||
frozen: false,
|
||||
autoscale: true,
|
||||
logMode: options.model?.logMode ?? false,
|
||||
autoscalePadding: 0.1
|
||||
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
|
||||
|
||||
@@ -36,20 +36,21 @@
|
||||
/>
|
||||
</ul>
|
||||
<div
|
||||
v-if="plotSeries.length"
|
||||
v-if="plotSeries.length && !isStackedPlotObject"
|
||||
class="grid-properties"
|
||||
>
|
||||
<ul
|
||||
v-if="!isStackedPlotObject"
|
||||
v-for="(yAxis, index) in yAxesWithSeries"
|
||||
:key="`yAxis-${index}`"
|
||||
class="l-inspector-part js-yaxis-properties"
|
||||
>
|
||||
<h2 title="Y axis settings for this object">Y Axis</h2>
|
||||
<h2 title="Y axis settings for this object">Y Axis {{ yAxis.id }}</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Manually override how the Y axis is labeled."
|
||||
>Label</div>
|
||||
<div class="grid-cell value">{{ label ? label : "Not defined" }}</div>
|
||||
<div class="grid-cell value">{{ yAxis.label ? yAxis.label : "Not defined" }}</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
@@ -57,7 +58,7 @@
|
||||
title="Enable log mode."
|
||||
>Log mode</div>
|
||||
<div class="grid-cell value">
|
||||
{{ logMode ? "Enabled" : "Disabled" }}
|
||||
{{ yAxis.logMode ? "Enabled" : "Disabled" }}
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
@@ -66,32 +67,36 @@
|
||||
title="Automatically scale the Y axis to keep all values in view."
|
||||
>Auto scale</div>
|
||||
<div class="grid-cell value">
|
||||
{{ autoscale ? "Enabled: " + autoscalePadding : "Disabled" }}
|
||||
{{ yAxis.autoscale ? "Enabled: " + yAxis.autoscalePadding : "Disabled" }}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="!autoscale && rangeMin"
|
||||
v-if="!yAxis.autoscale && yAxis.rangeMin"
|
||||
class="grid-row"
|
||||
>
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Minimum Y axis value."
|
||||
>Minimum value</div>
|
||||
<div class="grid-cell value">{{ rangeMin }}</div>
|
||||
<div class="grid-cell value">{{ yAxis.rangeMin }}</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="!autoscale && rangeMax"
|
||||
v-if="!yAxis.autoscale && yAxis.rangeMax"
|
||||
class="grid-row"
|
||||
>
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Maximum Y axis value."
|
||||
>Maximum value</div>
|
||||
<div class="grid-cell value">{{ rangeMax }}</div>
|
||||
<div class="grid-cell value">{{ yAxis.rangeMax }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
v-if="plotSeries.length && (isStackedPlotObject || !isNestedWithinAStackedPlot)"
|
||||
class="grid-properties"
|
||||
>
|
||||
<ul
|
||||
v-if="isStackedPlotObject || !isNestedWithinAStackedPlot"
|
||||
class="l-inspector-part js-legend-properties"
|
||||
>
|
||||
<h2 title="Legend settings for this object">Legend</h2>
|
||||
@@ -157,12 +162,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
config: {},
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
position: '',
|
||||
hideLegendWhenSmall: '',
|
||||
expandByDefault: '',
|
||||
@@ -173,7 +172,8 @@ export default {
|
||||
showMaximumWhenExpanded: '',
|
||||
showUnitsWhenExpanded: '',
|
||||
loaded: false,
|
||||
plotSeries: []
|
||||
plotSeries: [],
|
||||
yAxes: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -182,13 +182,18 @@ export default {
|
||||
},
|
||||
isStackedPlotObject() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
yAxesWithSeries() {
|
||||
return this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.config = this.getConfig();
|
||||
this.initYAxesConfiguration();
|
||||
|
||||
this.registerListeners();
|
||||
this.initConfiguration();
|
||||
this.initLegendConfiguration();
|
||||
this.loaded = true;
|
||||
|
||||
},
|
||||
@@ -196,18 +201,38 @@ export default {
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
initConfiguration() {
|
||||
initYAxesConfiguration() {
|
||||
if (this.config) {
|
||||
this.label = this.config.yAxis.get('label');
|
||||
this.autoscale = this.config.yAxis.get('autoscale');
|
||||
this.logMode = this.config.yAxis.get('logMode');
|
||||
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
|
||||
const range = this.config.yAxis.get('range');
|
||||
if (range) {
|
||||
this.rangeMin = range.min;
|
||||
this.rangeMax = range.max;
|
||||
}
|
||||
let range = this.config.yAxis.get('range');
|
||||
|
||||
this.yAxes.push({
|
||||
id: this.config.yAxis.id,
|
||||
seriesCount: 0,
|
||||
label: this.config.yAxis.get('label'),
|
||||
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 : ''
|
||||
});
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
range = yAxis.get('range');
|
||||
|
||||
this.yAxes.push({
|
||||
id: yAxis.id,
|
||||
seriesCount: 0,
|
||||
label: yAxis.get('label'),
|
||||
autoscale: yAxis.get('autoscale'),
|
||||
logMode: yAxis.get('logMode'),
|
||||
autoscalePadding: yAxis.get('autoscalePadding'),
|
||||
rangeMin: range ? range.min : '',
|
||||
rangeMax: range ? range.max : ''
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
initLegendConfiguration() {
|
||||
if (this.config) {
|
||||
this.position = this.config.legend.get('position');
|
||||
this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
|
||||
this.expandByDefault = this.config.legend.get('expandByDefault');
|
||||
@@ -229,18 +254,44 @@ export default {
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
}
|
||||
},
|
||||
|
||||
setYAxisLabel(yAxisId) {
|
||||
const found = this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
if (found && found.seriesCount > 0) {
|
||||
const mainYAxisId = this.config.yAxis.id;
|
||||
if (mainYAxisId === yAxisId) {
|
||||
found.label = this.config.yAxis.get('label');
|
||||
} else {
|
||||
const additionalYAxis = this.config.additionalYAxes.find(axis => axis.id === yAxisId);
|
||||
if (additionalYAxis) {
|
||||
found.label = additionalYAxis.get('label');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
this.$set(this.plotSeries, index, series);
|
||||
this.initConfiguration();
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
resetAllSeries() {
|
||||
this.plotSeries = [];
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
removeSeries(plotSeries, index) {
|
||||
const yAxisId = plotSeries.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, -1);
|
||||
this.plotSeries.splice(index, 1);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
updateAxisUsageCount(yAxisId, updateCount) {
|
||||
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,8 +40,10 @@
|
||||
</li>
|
||||
</ul>
|
||||
<y-axis-form
|
||||
v-if="plotSeries.length && !isStackedPlotObject"
|
||||
class="grid-properties"
|
||||
v-for="(yAxisId, index) in yAxesIds"
|
||||
:id="yAxisId.id"
|
||||
:key="`yAxis-${index}`"
|
||||
class="grid-properties js-yaxis-grid-properties"
|
||||
:y-axis="config.yAxis"
|
||||
@seriesUpdated="updateSeriesConfigForObject"
|
||||
/>
|
||||
@@ -76,6 +78,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
config: {},
|
||||
yAxes: [],
|
||||
plotSeries: [],
|
||||
loaded: false
|
||||
};
|
||||
@@ -86,11 +89,27 @@ export default {
|
||||
},
|
||||
isStackedPlotObject() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
yAxesIds() {
|
||||
return !this.isStackedPlotObject && this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
|
||||
}
|
||||
},
|
||||
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
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
this.registerListeners();
|
||||
this.loaded = true;
|
||||
},
|
||||
@@ -107,16 +126,47 @@ export default {
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
},
|
||||
|
||||
findYAxisForId(yAxisId) {
|
||||
return this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
},
|
||||
|
||||
setYAxisLabel(yAxisId) {
|
||||
const found = this.findYAxisForId(yAxisId);
|
||||
if (found && found.seriesCount > 0) {
|
||||
const mainYAxisId = this.config.yAxis.id;
|
||||
if (mainYAxisId === yAxisId) {
|
||||
found.label = this.config.yAxis.get('label');
|
||||
} else {
|
||||
const additionalYAxis = this.config.additionalYAxes.find(axis => axis.id === yAxisId);
|
||||
if (additionalYAxis) {
|
||||
found.label = additionalYAxis.get('label');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
this.$set(this.plotSeries, index, series);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
resetAllSeries() {
|
||||
this.plotSeries = [];
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
removeSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, -1);
|
||||
this.plotSeries.splice(index, 1);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
updateAxisUsageCount(yAxisId, updateCount) {
|
||||
const foundYAxis = this.findYAxisForId(yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;
|
||||
}
|
||||
},
|
||||
|
||||
updateSeriesConfigForObject(config) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="loaded">
|
||||
<ul class="l-inspector-part">
|
||||
<h2>Y Axis</h2>
|
||||
<h2>Y Axis {{ id }}</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
@@ -25,6 +25,7 @@
|
||||
<!-- eslint-disable-next-line vue/html-self-closing -->
|
||||
<input
|
||||
v-model="logMode"
|
||||
class="js-log-mode-input"
|
||||
type="checkbox"
|
||||
@change="updateForm('logMode')"
|
||||
/>
|
||||
@@ -103,52 +104,72 @@
|
||||
<script>
|
||||
import { objectPath } from "./formUtil";
|
||||
import _ from "lodash";
|
||||
import eventHelpers from "../../lib/eventHelpers";
|
||||
import configStore from "../../configuration/ConfigStore";
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
yAxis: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
yAxis: null,
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
validationErrors: {}
|
||||
validationErrors: {},
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initialize();
|
||||
eventHelpers.extend(this);
|
||||
this.getConfig();
|
||||
this.loaded = true;
|
||||
this.initFields();
|
||||
this.initFormValues();
|
||||
},
|
||||
methods: {
|
||||
initialize: function () {
|
||||
getConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
const config = configStore.get(configId);
|
||||
if (config) {
|
||||
const mainYAxisId = config.yAxis.id;
|
||||
this.isAdditionalYAxis = this.id !== mainYAxisId;
|
||||
if (this.isAdditionalYAxis) {
|
||||
this.additionalYAxes = config.additionalYAxes;
|
||||
this.yAxis = config.additionalYAxes.find(yAxis => yAxis.id === this.id);
|
||||
} else {
|
||||
this.yAxis = config.yAxis;
|
||||
}
|
||||
}
|
||||
},
|
||||
initFields() {
|
||||
const prefix = `configuration.${this.getPrefix()}`;
|
||||
this.fields = {
|
||||
label: {
|
||||
objectPath: 'configuration.yAxis.label'
|
||||
objectPath: `${prefix}.label`
|
||||
},
|
||||
autoscale: {
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.yAxis.autoscale'
|
||||
objectPath: `${prefix}.autoscale`
|
||||
},
|
||||
autoscalePadding: {
|
||||
coerce: Number,
|
||||
objectPath: 'configuration.yAxis.autoscalePadding'
|
||||
objectPath: `${prefix}.autoscalePadding`
|
||||
},
|
||||
logMode: {
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.yAxis.logMode'
|
||||
objectPath: `${prefix}.logMode`
|
||||
},
|
||||
range: {
|
||||
objectPath: 'configuration.yAxis.range',
|
||||
objectPath: `${prefix}.range'`,
|
||||
coerce: function coerceRange(range) {
|
||||
const newRange = {
|
||||
min: -1,
|
||||
@@ -202,6 +223,25 @@ export default {
|
||||
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) => {
|
||||
return yAxis.id === this.id;
|
||||
});
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
prefix = `additionalYAxes[${index}]`;
|
||||
}
|
||||
|
||||
return prefix;
|
||||
},
|
||||
updateForm(formKey) {
|
||||
let newVal;
|
||||
if (formKey === 'range') {
|
||||
@@ -231,18 +271,42 @@ export default {
|
||||
this.yAxis.set(formKey, newVal);
|
||||
// Then we mutate the domain object configuration to persist the settings
|
||||
if (path) {
|
||||
if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
|
||||
this.$emit('seriesUpdated', {
|
||||
identifier: this.domainObject.identifier,
|
||||
path: `yAxis.${formKey}`,
|
||||
value: newVal
|
||||
});
|
||||
if (this.isAdditionalYAxis) {
|
||||
if (this.domainObject.configuration && this.domainObject.configuration.series) {
|
||||
//update the id
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
`configuration.${this.getPrefix()}.id`,
|
||||
this.id
|
||||
);
|
||||
//update the yAxes values
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
newVal
|
||||
);
|
||||
} else {
|
||||
this.$emit('seriesUpdated', {
|
||||
identifier: this.domainObject.identifier,
|
||||
path: `${this.getPrefix()}.${formKey}`,
|
||||
id: this.id,
|
||||
value: newVal
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
newVal
|
||||
);
|
||||
if (this.domainObject.configuration && this.domainObject.configuration.series) {
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
newVal
|
||||
);
|
||||
} else {
|
||||
this.$emit('seriesUpdated', {
|
||||
identifier: this.domainObject.identifier,
|
||||
path: `${this.getPrefix()}.${formKey}`,
|
||||
value: newVal
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
504
src/plugins/plot/overlayPlot/pluginSpec.js
Normal file
504
src/plugins/plot/overlayPlot/pluginSpec.js
Normal file
@@ -0,0 +1,504 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing";
|
||||
import PlotVuePlugin from "../plugin";
|
||||
import Vue from "vue";
|
||||
import Plot from "../Plot.vue";
|
||||
import configStore from "../configuration/ConfigStore";
|
||||
import EventEmitter from "EventEmitter";
|
||||
import PlotOptions from "../inspector/PlotOptions.vue";
|
||||
|
||||
describe("the plugin", function () {
|
||||
let element;
|
||||
let child;
|
||||
let openmct;
|
||||
let telemetryPromise;
|
||||
let telemetryPromiseResolve;
|
||||
let mockObjectPath;
|
||||
let overlayPlotObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-plot"
|
||||
},
|
||||
type: "telemetry.plot.overlay",
|
||||
name: "Test Overlay Plot",
|
||||
composition: [],
|
||||
configuration: {
|
||||
series: []
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach((done) => {
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock folder',
|
||||
type: 'fake-folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'mock parent folder',
|
||||
type: 'time-strip',
|
||||
identifier: {
|
||||
key: 'mock-parent-folder',
|
||||
namespace: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
const testTelemetry = [
|
||||
{
|
||||
'utc': 1,
|
||||
'some-key': 'some-value 1',
|
||||
'some-other-key': 'some-other-value 1',
|
||||
'some-key2': 'some-value2 1',
|
||||
'some-other-key2': 'some-other-value2 1'
|
||||
},
|
||||
{
|
||||
'utc': 2,
|
||||
'some-key': 'some-value 2',
|
||||
'some-other-key': 'some-other-value 2',
|
||||
'some-key2': 'some-value2 2',
|
||||
'some-other-key2': 'some-other-value2 2'
|
||||
},
|
||||
{
|
||||
'utc': 3,
|
||||
'some-key': 'some-value 3',
|
||||
'some-other-key': 'some-other-value 3',
|
||||
'some-key2': 'some-value2 2',
|
||||
'some-other-key2': 'some-other-value2 2'
|
||||
}
|
||||
];
|
||||
|
||||
const timeSystem = {
|
||||
timeSystemKey: 'utc',
|
||||
bounds: {
|
||||
start: 0,
|
||||
end: 4
|
||||
}
|
||||
};
|
||||
|
||||
openmct = createOpenMct(timeSystem);
|
||||
|
||||
telemetryPromise = new Promise((resolve) => {
|
||||
telemetryPromiseResolve = resolve;
|
||||
});
|
||||
|
||||
spyOn(openmct.telemetry, 'request').and.callFake(() => {
|
||||
telemetryPromiseResolve(testTelemetry);
|
||||
|
||||
return telemetryPromise;
|
||||
});
|
||||
|
||||
openmct.install(new PlotVuePlugin());
|
||||
|
||||
element = document.createElement("div");
|
||||
element.style.width = "640px";
|
||||
element.style.height = "480px";
|
||||
child = document.createElement("div");
|
||||
child.style.width = "640px";
|
||||
child.style.height = "480px";
|
||||
element.appendChild(child);
|
||||
document.body.appendChild(element);
|
||||
|
||||
spyOn(window, 'ResizeObserver').and.returnValue({
|
||||
observe() {},
|
||||
unobserve() {},
|
||||
disconnect() {}
|
||||
});
|
||||
|
||||
openmct.types.addType("test-object", {
|
||||
creatable: true
|
||||
});
|
||||
|
||||
spyOnBuiltins(["requestAnimationFrame"]);
|
||||
window.requestAnimationFrame.and.callFake((callBack) => {
|
||||
callBack();
|
||||
});
|
||||
|
||||
openmct.router.path = [overlayPlotObject];
|
||||
openmct.on("start", done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
openmct.time.timeSystem('utc', {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
configStore.deleteAll();
|
||||
resetApplicationState(openmct).then(done).catch(done);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
openmct.router.path = null;
|
||||
});
|
||||
|
||||
describe("the plot views", () => {
|
||||
it("provides an overlay plot view for objects with telemetry", () => {
|
||||
const testTelemetryObject = {
|
||||
id: "test-object",
|
||||
type: "telemetry.plot.overlay",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "some-key"
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
|
||||
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay");
|
||||
expect(plotView).toBeDefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("The overlay plot view with multiple axes", () => {
|
||||
let testTelemetryObject;
|
||||
let testTelemetryObject2;
|
||||
let config;
|
||||
let component;
|
||||
let mockComposition;
|
||||
|
||||
afterAll(() => {
|
||||
component.$destroy();
|
||||
openmct.router.path = null;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
testTelemetryObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-object"
|
||||
},
|
||||
type: "test-object",
|
||||
name: "Test Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "utc",
|
||||
format: "utc",
|
||||
name: "Time",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-key",
|
||||
name: "Some attribute",
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-other-key",
|
||||
name: "Another attribute",
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
testTelemetryObject2 = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-object2"
|
||||
},
|
||||
type: "test-object",
|
||||
name: "Test Object2",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "utc",
|
||||
format: "utc",
|
||||
name: "Time",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-key2",
|
||||
name: "Some attribute2",
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-other-key2",
|
||||
name: "Another attribute2",
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
overlayPlotObject.composition = [
|
||||
{
|
||||
identifier: testTelemetryObject.identifier
|
||||
},
|
||||
{
|
||||
identifier: testTelemetryObject2.identifier
|
||||
}
|
||||
];
|
||||
overlayPlotObject.configuration.series = [
|
||||
{
|
||||
identifier: testTelemetryObject.identifier,
|
||||
yAxisId: 1
|
||||
},
|
||||
{
|
||||
identifier: testTelemetryObject2.identifier,
|
||||
yAxisId: 3
|
||||
}
|
||||
];
|
||||
overlayPlotObject.configuration.additionalYAxes = [
|
||||
{
|
||||
label: 'Test Object Label',
|
||||
id: 2
|
||||
},
|
||||
{
|
||||
label: 'Test Object 2 Label',
|
||||
id: 3
|
||||
}
|
||||
];
|
||||
mockComposition = new EventEmitter();
|
||||
mockComposition.load = () => {
|
||||
mockComposition.emit('add', testTelemetryObject);
|
||||
mockComposition.emit('add', testTelemetryObject2);
|
||||
|
||||
return [testTelemetryObject, testTelemetryObject2];
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
||||
|
||||
let viewContainer = document.createElement("div");
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
el: viewContainer,
|
||||
components: {
|
||||
Plot
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: overlayPlotObject,
|
||||
composition: openmct.composition.get(overlayPlotObject),
|
||||
path: [overlayPlotObject]
|
||||
},
|
||||
template: '<plot ref="plotComponent"></plot>'
|
||||
});
|
||||
|
||||
return telemetryPromise
|
||||
.then(Vue.nextTick())
|
||||
.then(() => {
|
||||
const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier);
|
||||
config = configStore.get(configId);
|
||||
});
|
||||
});
|
||||
|
||||
it("Renders multiple Y-axis for the telemetry objects", (done) => {
|
||||
config.yAxis.set('displayRange', {
|
||||
min: 10,
|
||||
max: 20
|
||||
});
|
||||
Vue.nextTick(() => {
|
||||
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper");
|
||||
expect(yAxisElement.length).toBe(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('the inspector view', () => {
|
||||
let inspectorComponent;
|
||||
let viewComponentObject;
|
||||
let selection;
|
||||
beforeEach((done) => {
|
||||
selection = [
|
||||
[
|
||||
{
|
||||
context: {
|
||||
item: {
|
||||
id: overlayPlotObject.identifier.key,
|
||||
identifier: overlayPlotObject.identifier,
|
||||
type: overlayPlotObject.type,
|
||||
configuration: overlayPlotObject.configuration,
|
||||
composition: overlayPlotObject.composition
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
let viewContainer = document.createElement('div');
|
||||
child.append(viewContainer);
|
||||
inspectorComponent = new Vue({
|
||||
el: viewContainer,
|
||||
components: {
|
||||
PlotOptions
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: selection[0][0].context.item,
|
||||
path: [selection[0][0].context.item]
|
||||
},
|
||||
template: '<plot-options/>'
|
||||
});
|
||||
|
||||
Vue.nextTick(() => {
|
||||
viewComponentObject = inspectorComponent.$root.$children[0];
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
openmct.router.path = null;
|
||||
});
|
||||
|
||||
describe('in edit mode', () => {
|
||||
let editOptionsEl;
|
||||
|
||||
beforeEach((done) => {
|
||||
viewComponentObject.setEditState(true);
|
||||
Vue.nextTick(() => {
|
||||
editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows multiple yAxis options', () => {
|
||||
const yAxisProperties = editOptionsEl.querySelectorAll(".js-yaxis-grid-properties .l-inspector-part h2");
|
||||
expect(yAxisProperties.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('saves yAxis options', () => {
|
||||
//toggle log mode and save
|
||||
config.additionalYAxes[1].set('displayRange', {
|
||||
min: 10,
|
||||
max: 20
|
||||
});
|
||||
const yAxisProperties = editOptionsEl.querySelectorAll(".js-log-mode-input");
|
||||
const clickEvent = createMouseEvent("click");
|
||||
yAxisProperties[1].dispatchEvent(clickEvent);
|
||||
|
||||
expect(config.additionalYAxes[1].get('logMode')).toEqual(true);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("The overlay plot view with single axes", () => {
|
||||
let testTelemetryObject;
|
||||
let config;
|
||||
let component;
|
||||
let mockComposition;
|
||||
|
||||
afterAll(() => {
|
||||
component.$destroy();
|
||||
openmct.router.path = null;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
testTelemetryObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-object"
|
||||
},
|
||||
type: "test-object",
|
||||
name: "Test Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "utc",
|
||||
format: "utc",
|
||||
name: "Time",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-key",
|
||||
name: "Some attribute",
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-other-key",
|
||||
name: "Another attribute",
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
overlayPlotObject.composition = [
|
||||
{
|
||||
identifier: testTelemetryObject.identifier
|
||||
}
|
||||
];
|
||||
overlayPlotObject.configuration.series = [
|
||||
{
|
||||
identifier: testTelemetryObject.identifier
|
||||
}
|
||||
];
|
||||
mockComposition = new EventEmitter();
|
||||
mockComposition.load = () => {
|
||||
mockComposition.emit('add', testTelemetryObject);
|
||||
|
||||
return [testTelemetryObject];
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
||||
|
||||
let viewContainer = document.createElement("div");
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
el: viewContainer,
|
||||
components: {
|
||||
Plot
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: overlayPlotObject,
|
||||
composition: openmct.composition.get(overlayPlotObject),
|
||||
path: [overlayPlotObject]
|
||||
},
|
||||
template: '<plot ref="plotComponent"></plot>'
|
||||
});
|
||||
|
||||
return telemetryPromise
|
||||
.then(Vue.nextTick())
|
||||
.then(() => {
|
||||
const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier);
|
||||
config = configStore.get(configId);
|
||||
});
|
||||
});
|
||||
|
||||
it("Renders single Y-axis for the telemetry object", (done) => {
|
||||
config.yAxis.set('displayRange', {
|
||||
min: 10,
|
||||
max: 20
|
||||
});
|
||||
Vue.nextTick(() => {
|
||||
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper");
|
||||
expect(yAxisElement.length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,8 @@ import EventEmitter from "EventEmitter";
|
||||
import PlotOptions from "./inspector/PlotOptions.vue";
|
||||
import PlotConfigurationModel from "./configuration/PlotConfigurationModel";
|
||||
|
||||
const TEST_KEY_ID = 'test-key';
|
||||
|
||||
describe("the plugin", function () {
|
||||
let element;
|
||||
let child;
|
||||
@@ -404,6 +406,20 @@ describe("the plugin", function () {
|
||||
expect(options[1].value).toBe("Another attribute");
|
||||
});
|
||||
|
||||
it("Updates the Y-axis label when changed", () => {
|
||||
const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
|
||||
const config = configStore.get(configId);
|
||||
const yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y")[0].__vue__;
|
||||
config.yAxis.seriesCollection.models.forEach((plotSeries) => {
|
||||
expect(plotSeries.model.yKey).toBe('some-key');
|
||||
});
|
||||
|
||||
yAxisElement.$emit('yKeyChanged', TEST_KEY_ID, 1);
|
||||
config.yAxis.seriesCollection.models.forEach((plotSeries) => {
|
||||
expect(plotSeries.model.yKey).toBe(TEST_KEY_ID);
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the pause and play controls', () => {
|
||||
let pauseEl = element.querySelectorAll(".c-button-set .icon-pause");
|
||||
let playEl = element.querySelectorAll(".c-button-set .icon-arrow-right");
|
||||
|
||||
@@ -62,8 +62,8 @@ export default class RemoteClock extends DefaultClock {
|
||||
}
|
||||
|
||||
start() {
|
||||
this.openmct.time.on('timeSystem', this._timeSystemChange);
|
||||
this.openmct.objects.get(this.identifier).then((domainObject) => {
|
||||
this.openmct.time.on('timeSystem', this._timeSystemChange);
|
||||
this.timeTelemetryObject = domainObject;
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
this._timeSystemChange();
|
||||
|
||||
@@ -71,7 +71,10 @@ describe("the RemoteClock plugin", () => {
|
||||
parse: (datum) => datum.key
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
let objectPromise;
|
||||
let requestPromise;
|
||||
|
||||
beforeEach(() => {
|
||||
openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID));
|
||||
|
||||
let clocks = openmct.time.getAllClocks();
|
||||
@@ -89,7 +92,9 @@ describe("the RemoteClock plugin", () => {
|
||||
spyOn(metadata, 'value').and.callThrough();
|
||||
|
||||
let requestPromiseResolve;
|
||||
let requestPromise = new Promise((resolve) => {
|
||||
let objectPromiseResolve;
|
||||
|
||||
requestPromise = new Promise((resolve) => {
|
||||
requestPromiseResolve = resolve;
|
||||
});
|
||||
spyOn(openmct.telemetry, 'request').and.callFake(() => {
|
||||
@@ -98,8 +103,7 @@ describe("the RemoteClock plugin", () => {
|
||||
return requestPromise;
|
||||
});
|
||||
|
||||
let objectPromiseResolve;
|
||||
let objectPromise = new Promise((resolve) => {
|
||||
objectPromise = new Promise((resolve) => {
|
||||
objectPromiseResolve = resolve;
|
||||
});
|
||||
spyOn(openmct.objects, 'get').and.callFake(() => {
|
||||
@@ -112,39 +116,48 @@ describe("the RemoteClock plugin", () => {
|
||||
start: OFFSET_START,
|
||||
end: OFFSET_END
|
||||
});
|
||||
|
||||
await Promise.all([objectPromiseResolve, requestPromise]);
|
||||
});
|
||||
|
||||
it('is available and sets up initial values and listeners', () => {
|
||||
expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);
|
||||
expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);
|
||||
expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);
|
||||
expect(remoteClock._timeSystemChange).toHaveBeenCalled();
|
||||
it("Does not throw error if time system is changed before remote clock initialized", () => {
|
||||
expect(() => openmct.time.timeSystem('utc')).not.toThrow();
|
||||
});
|
||||
|
||||
it('will request/store the object based on the identifier passed in', () => {
|
||||
expect(remoteClock.timeTelemetryObject).toEqual(object);
|
||||
describe('once resolved', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all([objectPromise, requestPromise]);
|
||||
});
|
||||
|
||||
it('is available and sets up initial values and listeners', () => {
|
||||
expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);
|
||||
expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);
|
||||
expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);
|
||||
expect(remoteClock._timeSystemChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('will request/store the object based on the identifier passed in', () => {
|
||||
expect(remoteClock.timeTelemetryObject).toEqual(object);
|
||||
});
|
||||
|
||||
it('will request metadata and set up formatters', () => {
|
||||
expect(remoteClock.metadata).toEqual(metadata);
|
||||
expect(metadata.value).toHaveBeenCalled();
|
||||
expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);
|
||||
});
|
||||
|
||||
it('will request the latest datum for the object it received and process the datum returned', () => {
|
||||
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
|
||||
expect(boundsCallback).toHaveBeenCalledWith({
|
||||
start: TIME_VALUE + OFFSET_START,
|
||||
end: TIME_VALUE + OFFSET_END
|
||||
}, true);
|
||||
});
|
||||
|
||||
it('will set up subscriptions correctly', () => {
|
||||
expect(remoteClock._unsubscribe).toBeDefined();
|
||||
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum);
|
||||
});
|
||||
});
|
||||
|
||||
it('will request metadata and set up formatters', () => {
|
||||
expect(remoteClock.metadata).toEqual(metadata);
|
||||
expect(metadata.value).toHaveBeenCalled();
|
||||
expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);
|
||||
});
|
||||
|
||||
it('will request the latest datum for the object it received and process the datum returned', () => {
|
||||
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
|
||||
expect(boundsCallback).toHaveBeenCalledWith({
|
||||
start: TIME_VALUE + OFFSET_START,
|
||||
end: TIME_VALUE + OFFSET_END
|
||||
}, true);
|
||||
});
|
||||
|
||||
it('will set up subscriptions correctly', () => {
|
||||
expect(remoteClock._unsubscribe).toBeDefined();
|
||||
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -178,7 +178,7 @@ define([
|
||||
if (this.paused) {
|
||||
this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));
|
||||
} else {
|
||||
this.tableRows.addRows(telemetryRows, 'add');
|
||||
this.tableRows.addRows(telemetryRows);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -229,7 +229,7 @@ define([
|
||||
});
|
||||
});
|
||||
|
||||
this.tableRows.addRows(allRows, 'filter');
|
||||
this.tableRows.clearRowsFromTableAndFilter(allRows);
|
||||
}
|
||||
|
||||
updateFilters(updatedFilters) {
|
||||
|
||||
@@ -61,30 +61,39 @@ define(
|
||||
this.emit('remove', removed);
|
||||
}
|
||||
|
||||
addRows(rows, type = 'add') {
|
||||
if (this.sortOptions === undefined) {
|
||||
throw 'Please specify sort options';
|
||||
}
|
||||
|
||||
let isFilterTriggeredReset = type === 'filter';
|
||||
let anyActiveFilters = Object.keys(this.columnFilters).length > 0;
|
||||
let rowsToAdd = !anyActiveFilters ? rows : rows.filter(this.matchesFilters, this);
|
||||
|
||||
// if type is filter, then it's a reset of all rows,
|
||||
// need to wipe current rows
|
||||
if (isFilterTriggeredReset) {
|
||||
this.rows = [];
|
||||
}
|
||||
addRows(rows) {
|
||||
let rowsToAdd = this.filterRows(rows);
|
||||
|
||||
this.sortAndMergeRows(rowsToAdd);
|
||||
|
||||
// we emit filter no matter what to trigger
|
||||
// an update of visible rows
|
||||
if (rowsToAdd.length > 0 || isFilterTriggeredReset) {
|
||||
this.emit(type, rowsToAdd);
|
||||
if (rowsToAdd.length > 0) {
|
||||
this.emit('add', rowsToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
clearRowsFromTableAndFilter(rows) {
|
||||
|
||||
let rowsToAdd = this.filterRows(rows);
|
||||
// Reset of all rows, need to wipe current rows
|
||||
this.rows = [];
|
||||
|
||||
this.sortAndMergeRows(rowsToAdd);
|
||||
|
||||
// We emit filter and update of visible rows
|
||||
this.emit('filter', rowsToAdd);
|
||||
}
|
||||
|
||||
filterRows(rows) {
|
||||
|
||||
if (Object.keys(this.columnFilters).length > 0) {
|
||||
return rows.filter(this.matchesFilters, this);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
sortAndMergeRows(rows) {
|
||||
const sortedRowsToAdd = this.sortCollection(rows);
|
||||
|
||||
|
||||
@@ -593,6 +593,8 @@ mct-plot {
|
||||
.plot-legend-left .gl-plot-legend { margin-right: $interiorMargin; }
|
||||
.plot-legend-right .gl-plot-legend { margin-left: $interiorMargin; }
|
||||
|
||||
.gl-plot .plot-yaxis-right.gl-plot-y { margin-left: 100%; }
|
||||
|
||||
.gl-plot,
|
||||
.c-plot {
|
||||
&.plot-legend-collapsed .plot-wrapper-expanded-legend { display: none; }
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
:checked="checked"
|
||||
@change="onUserSelect($event)"
|
||||
>
|
||||
<span class="c-toggle-switch__slider"></span>
|
||||
<span
|
||||
class="c-toggle-switch__slider"
|
||||
role="switch"
|
||||
:aria-label="name"
|
||||
></span>
|
||||
</label>
|
||||
<div
|
||||
v-if="label && label.length"
|
||||
@@ -32,6 +36,11 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
checked: Boolean
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
draggable="true"
|
||||
@dragstart="emitDragStartEvent"
|
||||
@dragenter="onDragenter"
|
||||
@dragover="onDragover"
|
||||
@dragover.prevent
|
||||
@dragleave="onDragleave"
|
||||
@drop="emitDropEvent"
|
||||
>
|
||||
@@ -38,6 +38,7 @@
|
||||
}"
|
||||
>
|
||||
<span
|
||||
v-if="showGrippy"
|
||||
class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"
|
||||
></span>
|
||||
<object-label
|
||||
@@ -81,6 +82,10 @@ export default {
|
||||
},
|
||||
allowDrop: {
|
||||
type: Boolean
|
||||
},
|
||||
showGrippy: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -93,11 +98,8 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onDragover(event) {
|
||||
event.preventDefault();
|
||||
},
|
||||
emitDropEvent(event) {
|
||||
this.$emit('drop-custom', this.index);
|
||||
this.$emit('drop-custom', event);
|
||||
this.hover = false;
|
||||
},
|
||||
emitDragStartEvent(event) {
|
||||
|
||||
101
src/ui/inspector/ElementItemGroup.vue
Normal file
101
src/ui/inspector/ElementItemGroup.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
/*****************************************************************************
|
||||
* 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-elements-pool__group"
|
||||
:class="{
|
||||
'hover': hover
|
||||
}"
|
||||
:allow-drop="allowDrop"
|
||||
@dragover.prevent
|
||||
@dragenter="onDragEnter"
|
||||
@dragleave.stop="onDragLeave"
|
||||
@drop="emitDrop"
|
||||
>
|
||||
<ul>
|
||||
<div>
|
||||
<span class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"></span>
|
||||
<div
|
||||
class="c-tree__item__type-icon c-object-label__type-icon"
|
||||
>
|
||||
<span
|
||||
class="is-status__indicator"
|
||||
></span>
|
||||
</div>
|
||||
<div
|
||||
class="c-tree__item__name c-object-label__name"
|
||||
aria-label="Element Item Group"
|
||||
>
|
||||
{{ label }}
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
parentObject: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: () => {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
allowDrop: {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragCounter: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hover() {
|
||||
return this.dragCounter > 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emitDrop(event) {
|
||||
this.dragCounter = 0;
|
||||
this.$emit('drop-group', event);
|
||||
},
|
||||
onDragEnter(event) {
|
||||
this.dragCounter++;
|
||||
},
|
||||
onDragLeave(event) {
|
||||
this.dragCounter--;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -65,8 +65,8 @@ import ElementItem from './ElementItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'Search': Search,
|
||||
'ElementItem': ElementItem
|
||||
Search,
|
||||
ElementItem
|
||||
},
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
|
||||
@@ -56,7 +56,12 @@
|
||||
handle="before"
|
||||
label="Elements"
|
||||
>
|
||||
<elements-pool />
|
||||
<plot-elements-pool
|
||||
v-if="isOverlayPlot"
|
||||
/>
|
||||
<elements-pool
|
||||
v-else
|
||||
/>
|
||||
</pane>
|
||||
</multipane>
|
||||
<multipane
|
||||
@@ -83,6 +88,7 @@
|
||||
import multipane from '../layout/multipane.vue';
|
||||
import pane from '../layout/pane.vue';
|
||||
import ElementsPool from './ElementsPool.vue';
|
||||
import PlotElementsPool from './PlotElementsPool.vue';
|
||||
import Location from './Location.vue';
|
||||
import Properties from './details/Properties.vue';
|
||||
import ObjectName from './ObjectName.vue';
|
||||
@@ -99,6 +105,7 @@ export default {
|
||||
multipane,
|
||||
pane,
|
||||
ElementsPool,
|
||||
PlotElementsPool,
|
||||
Properties,
|
||||
ObjectName,
|
||||
Location,
|
||||
@@ -118,6 +125,7 @@ export default {
|
||||
return {
|
||||
hasComposition: false,
|
||||
showStyles: false,
|
||||
isOverlayPlot: false,
|
||||
tabbedViews: [{
|
||||
key: '__properties',
|
||||
name: 'Properties'
|
||||
@@ -151,6 +159,7 @@ export default {
|
||||
let 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';
|
||||
}
|
||||
},
|
||||
refreshTabs(selection) {
|
||||
|
||||
330
src/ui/inspector/PlotElementsPool.vue
Normal file
330
src/ui/inspector/PlotElementsPool.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
/*****************************************************************************
|
||||
* 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-elements-pool">
|
||||
<Search
|
||||
class="c-elements-pool__search"
|
||||
:value="currentSearch"
|
||||
@input="applySearch"
|
||||
@clear="applySearch"
|
||||
/>
|
||||
<div
|
||||
class="c-elements-pool__elements"
|
||||
>
|
||||
<ul
|
||||
v-if="hasElements"
|
||||
id="inspector-elements-tree"
|
||||
class="c-tree c-elements-pool__tree"
|
||||
>
|
||||
<element-item-group
|
||||
v-for="(yAxis, index) in yAxes"
|
||||
:key="`element-group-yaxis-${yAxis.id}`"
|
||||
:parent-object="parentObject"
|
||||
:allow-drop="allowDrop"
|
||||
:label="`Y Axis ${yAxis.id}`"
|
||||
@drop-group="moveTo($event, 0, yAxis.id)"
|
||||
>
|
||||
<li
|
||||
class="js-first-place"
|
||||
@drop="moveTo($event, 0, yAxis.id)"
|
||||
></li>
|
||||
<element-item
|
||||
v-for="(element, elemIndex) in yAxis.elements"
|
||||
:key="element.identifier.key"
|
||||
:index="elemIndex"
|
||||
:element-object="element"
|
||||
:parent-object="parentObject"
|
||||
:allow-drop="allowDrop"
|
||||
:show-grippy="false"
|
||||
@dragstart-custom="moveFrom($event, yAxis.id)"
|
||||
@drop-custom="moveTo($event, index, yAxis.id)"
|
||||
/>
|
||||
<li
|
||||
v-if="yAxis.elements.length > 0"
|
||||
class="js-last-place"
|
||||
@drop="moveTo($event, yAxis.elements.length, yAxis.id)"
|
||||
></li>
|
||||
</element-item-group>
|
||||
</ul>
|
||||
<div
|
||||
v-if="!hasElements"
|
||||
>
|
||||
No contained elements
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import Search from '../components/search.vue';
|
||||
import ElementItem from './ElementItem.vue';
|
||||
import ElementItemGroup from './ElementItemGroup.vue';
|
||||
import configStore from '../../plugins/plot/configuration/ConfigStore';
|
||||
|
||||
const Y_AXIS_1 = 1;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Search,
|
||||
ElementItemGroup,
|
||||
ElementItem
|
||||
},
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
yAxes: [],
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
parentObject: undefined,
|
||||
currentSearch: '',
|
||||
selection: [],
|
||||
contextClickTracker: {},
|
||||
allowDrop: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasElements() {
|
||||
for (const yAxis of this.yAxes) {
|
||||
if (yAxis.elements.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const selection = this.openmct.selection.get();
|
||||
if (selection && selection.length > 0) {
|
||||
this.showSelection(selection);
|
||||
}
|
||||
|
||||
this.openmct.selection.on('change', this.showSelection);
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
},
|
||||
destroyed() {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
this.openmct.selection.off('change', this.showSelection);
|
||||
|
||||
this.unlistenComposition();
|
||||
},
|
||||
methods: {
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
this.showSelection(this.openmct.selection.get());
|
||||
},
|
||||
showSelection(selection) {
|
||||
if (_.isEqual(this.selection, selection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selection = selection;
|
||||
this.elementsCache = {};
|
||||
this.listeners = [];
|
||||
this.parentObject = selection && selection[0] && selection[0][0].context.item;
|
||||
|
||||
this.unlistenComposition();
|
||||
|
||||
if (this.parentObject) {
|
||||
this.setYAxisIds();
|
||||
this.composition = this.openmct.composition.get(this.parentObject);
|
||||
|
||||
if (this.composition) {
|
||||
this.composition.load();
|
||||
this.registerCompositionListeners();
|
||||
}
|
||||
}
|
||||
},
|
||||
unlistenComposition() {
|
||||
if (this.compositionUnlistener) {
|
||||
this.compositionUnlistener();
|
||||
}
|
||||
},
|
||||
registerCompositionListeners() {
|
||||
this.composition.on('add', this.addElement);
|
||||
this.composition.on('remove', this.removeElement);
|
||||
this.composition.on('reorder', this.reorderElements);
|
||||
|
||||
this.compositionUnlistener = () => {
|
||||
this.composition.off('add', this.addElement);
|
||||
this.composition.off('remove', this.removeElement);
|
||||
this.composition.off('reorder', this.reorderElements);
|
||||
delete this.compositionUnlistener;
|
||||
};
|
||||
},
|
||||
setYAxisIds() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.parentObject.identifier);
|
||||
this.config = configStore.get(configId);
|
||||
this.yAxes.push({
|
||||
id: this.config.yAxis.id,
|
||||
elements: this.parentObject.configuration.series.filter(
|
||||
series => series.yAxisId === this.config.yAxis.id
|
||||
)
|
||||
});
|
||||
if (this.config.additionalYAxes) {
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
this.yAxes.push({
|
||||
id: yAxis.id,
|
||||
elements: this.parentObject.configuration.series.filter(
|
||||
series => series.yAxisId === yAxis.id
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
addElement(element) {
|
||||
// Get the index of the corresponding element in the series list
|
||||
const seriesIndex = this.parentObject.configuration.series.findIndex(
|
||||
series => this.openmct.objects.areIdsEqual(series.identifier, element.identifier)
|
||||
);
|
||||
const keyString = this.openmct.objects.makeKeyString(element.identifier);
|
||||
|
||||
const wasDraggedOntoPlot = this.parentObject.configuration.series[seriesIndex].yAxisId === undefined;
|
||||
const yAxisId = wasDraggedOntoPlot
|
||||
? Y_AXIS_1
|
||||
: this.parentObject.configuration.series[seriesIndex].yAxisId;
|
||||
|
||||
if (wasDraggedOntoPlot) {
|
||||
const insertIndex = this.yAxes[0].elements.length;
|
||||
// Insert the element at the end of the first YAxis bucket
|
||||
this.composition.reorder(seriesIndex, insertIndex);
|
||||
}
|
||||
|
||||
// Store the element in the cache and set its yAxisId
|
||||
this.elementsCache[keyString] = JSON.parse(JSON.stringify(element));
|
||||
if (this.elementsCache[keyString].yAxisId !== yAxisId) {
|
||||
// Mutate the YAxisId on the domainObject itself
|
||||
this.updateCacheAndMutate(element, yAxisId);
|
||||
}
|
||||
|
||||
this.applySearch(this.currentSearch);
|
||||
},
|
||||
reorderElements() {
|
||||
this.applySearch(this.currentSearch);
|
||||
},
|
||||
removeElement(identifier) {
|
||||
const keyString = this.openmct.objects.makeKeyString(identifier);
|
||||
delete this.elementsCache[keyString];
|
||||
this.applySearch(this.currentSearch);
|
||||
},
|
||||
applySearch(input) {
|
||||
this.currentSearch = input;
|
||||
this.yAxes.forEach(yAxis => {
|
||||
yAxis.elements = this.filterForSearchAndAxis(input, yAxis.id);
|
||||
});
|
||||
},
|
||||
filterForSearchAndAxis(input, yAxisId) {
|
||||
return this.parentObject.composition.map((id) =>
|
||||
this.elementsCache[this.openmct.objects.makeKeyString(id)]
|
||||
).filter((element) => {
|
||||
return element !== undefined
|
||||
&& element.name.toLowerCase().search(input) !== -1
|
||||
&& element.yAxisId === yAxisId;
|
||||
});
|
||||
},
|
||||
moveFrom(elementIndex, groupIndex) {
|
||||
this.allowDrop = true;
|
||||
this.moveFromIndex = elementIndex;
|
||||
this.moveFromYAxisId = groupIndex;
|
||||
},
|
||||
moveTo(event, moveToIndex, moveToYAxisId) {
|
||||
// FIXME: If the user starts the drag by clicking outside of the <object-label/> element,
|
||||
// domain object information will not be set on the dataTransfer data. To prevent errors,
|
||||
// we simply short-circuit here if the data is not set.
|
||||
const serializedDomainObject = event.dataTransfer.getData('openmct/composable-domain-object');
|
||||
if (!serializedDomainObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const domainObject = JSON.parse(serializedDomainObject);
|
||||
this.updateCacheAndMutate(domainObject, moveToYAxisId);
|
||||
|
||||
const moveFromIndex = this.moveFromIndex;
|
||||
|
||||
this.moveAndReorderElement(moveFromIndex, moveToIndex, moveToYAxisId);
|
||||
},
|
||||
updateCacheAndMutate(domainObject, yAxisId) {
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const index = this.parentObject.configuration.series.findIndex(
|
||||
series => series.identifier.key === domainObject.identifier.key
|
||||
);
|
||||
|
||||
// Handle the case of dragging an element directly into the Elements Pool
|
||||
if (!this.elementsCache[keyString]) {
|
||||
// Update the series list locally so our CompositionAdd handler can
|
||||
// take care of the rest.
|
||||
this.parentObject.configuration.series.push({
|
||||
identifier: domainObject.identifier,
|
||||
yAxisId
|
||||
});
|
||||
this.composition.add(domainObject);
|
||||
this.elementsCache[keyString] = JSON.parse(JSON.stringify(domainObject));
|
||||
}
|
||||
|
||||
this.elementsCache[keyString].yAxisId = yAxisId;
|
||||
const shouldMutate = this.parentObject.configuration.series?.[index]?.yAxisId !== yAxisId;
|
||||
if (shouldMutate) {
|
||||
this.openmct.objects.mutate(
|
||||
this.parentObject,
|
||||
`configuration.series[${index}].yAxisId`,
|
||||
yAxisId
|
||||
);
|
||||
}
|
||||
},
|
||||
moveAndReorderElement(moveFromIndex, moveToIndex, moveToYAxisId) {
|
||||
if (!this.allowDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the corresponding indexes of the from/to yAxes in the yAxes list
|
||||
const moveFromYAxisIndex = this.yAxes.findIndex(yAxis => yAxis.id === this.moveFromYAxisId);
|
||||
const moveToYAxisIndex = this.yAxes.findIndex(yAxis => yAxis.id === moveToYAxisId);
|
||||
|
||||
// Calculate the actual indexes of the elements in the composition array
|
||||
// based on which bucket and index they are being moved from/to.
|
||||
// Then, trigger a composition reorder.
|
||||
for (let yAxisId = 0; yAxisId < moveFromYAxisIndex; yAxisId++) {
|
||||
const lesserYAxisBucketLength = this.yAxes[yAxisId].elements.length;
|
||||
// Add the lengths of preceding buckets to calculate the actual 'from' index
|
||||
moveFromIndex = moveFromIndex + lesserYAxisBucketLength;
|
||||
}
|
||||
|
||||
for (let yAxisId = 0; yAxisId < moveToYAxisIndex; yAxisId++) {
|
||||
const greaterYAxisBucketLength = this.yAxes[yAxisId].elements.length;
|
||||
// Add the lengths of subsequent buckets to calculate the actual 'to' index
|
||||
moveToIndex = moveToIndex + greaterYAxisBucketLength;
|
||||
}
|
||||
|
||||
// Adjust the index by 1 if we're moving from one bucket to another
|
||||
if (this.moveFromYAxisId !== moveToYAxisId && moveToIndex > 0) {
|
||||
moveToIndex--;
|
||||
}
|
||||
|
||||
// Reorder the composition array according to the calculated indexes
|
||||
this.composition.reorder(moveFromIndex, moveToIndex);
|
||||
|
||||
this.allowDrop = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -21,6 +21,11 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__group {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__elements {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
@@ -335,6 +335,7 @@ export default {
|
||||
dialog.dismiss();
|
||||
this.openmct.notifications.error('Error saving objects');
|
||||
console.error(error);
|
||||
this.openmct.editor.cancel();
|
||||
});
|
||||
},
|
||||
saveAndContinueEditing() {
|
||||
|
||||
@@ -174,8 +174,7 @@ export default {
|
||||
itemOffset: 0,
|
||||
activeSearch: false,
|
||||
mainTreeTopMargin: undefined,
|
||||
selectedItem: {},
|
||||
observers: {}
|
||||
selectedItem: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -277,10 +276,13 @@ export default {
|
||||
this.treeResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
this.destroyObservers(this.observers);
|
||||
this.destroyObservers();
|
||||
this.destroyMutables();
|
||||
},
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.observers = {};
|
||||
this.mutables = {};
|
||||
this.isLoading = true;
|
||||
this.getSavedOpenItems();
|
||||
this.treeResizeObserver = new ResizeObserver(this.handleTreeResize);
|
||||
@@ -355,8 +357,15 @@ export default {
|
||||
}
|
||||
|
||||
this.treeItems = this.treeItems.filter((checkItem) => {
|
||||
return checkItem.navigationPath === path
|
||||
|| !checkItem.navigationPath.includes(path);
|
||||
if (checkItem.navigationPath !== path
|
||||
&& checkItem.navigationPath.includes(path)) {
|
||||
this.destroyObserverByPath(checkItem.navigationPath);
|
||||
this.destroyMutableByPath(checkItem.navigationPath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
this.openTreeItems.splice(pathIndex, 1);
|
||||
this.removeCompositionListenerFor(path);
|
||||
@@ -436,7 +445,17 @@ export default {
|
||||
|
||||
}, Promise.resolve()).then(() => {
|
||||
if (this.isSelectorTree) {
|
||||
this.treeItemSelection(this.getTreeItemByPath(navigationPath));
|
||||
// If item is missing due to error in object creation,
|
||||
// walk up the navigationPath until we find an item
|
||||
let item = this.getTreeItemByPath(navigationPath);
|
||||
while (!item) {
|
||||
const startIndex = 0;
|
||||
const endIndex = navigationPath.lastIndexOf('/');
|
||||
navigationPath = navigationPath.substring(startIndex, endIndex);
|
||||
item = this.getTreeItemByPath(navigationPath);
|
||||
}
|
||||
|
||||
this.treeItemSelection(item);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -537,7 +556,7 @@ export default {
|
||||
composition = sortedComposition;
|
||||
}
|
||||
|
||||
if (parentObjectPath.length) {
|
||||
if (parentObjectPath.length && !this.isSelectorTree) {
|
||||
let navigationPath = this.buildNavigationPath(parentObjectPath);
|
||||
|
||||
if (this.compositionCollections[navigationPath]) {
|
||||
@@ -556,7 +575,15 @@ export default {
|
||||
}
|
||||
|
||||
return composition.map((object) => {
|
||||
this.addTreeItemObserver(object, parentObjectPath);
|
||||
// Only add observers and mutables if this is NOT a selector tree
|
||||
if (!this.isSelectorTree) {
|
||||
if (this.openmct.objects.supportsMutation(object.identifier)) {
|
||||
object = this.openmct.objects.toMutable(object);
|
||||
this.addMutable(object, parentObjectPath);
|
||||
}
|
||||
|
||||
this.addTreeItemObserver(object, parentObjectPath);
|
||||
}
|
||||
|
||||
return this.buildTreeItem(object, parentObjectPath);
|
||||
});
|
||||
@@ -574,6 +601,15 @@ export default {
|
||||
navigationPath
|
||||
};
|
||||
},
|
||||
addMutable(mutableDomainObject, parentObjectPath) {
|
||||
const objectPath = [mutableDomainObject].concat(parentObjectPath);
|
||||
const navigationPath = this.buildNavigationPath(objectPath);
|
||||
|
||||
// If the mutable already exists, destroy it.
|
||||
this.destroyMutableByPath(navigationPath);
|
||||
|
||||
this.mutables[navigationPath] = () => this.openmct.objects.destroyMutable(mutableDomainObject);
|
||||
},
|
||||
addTreeItemObserver(domainObject, parentObjectPath) {
|
||||
const objectPath = [domainObject].concat(parentObjectPath);
|
||||
const navigationPath = this.buildNavigationPath(objectPath);
|
||||
@@ -588,30 +624,6 @@ export default {
|
||||
this.sortTreeItems.bind(this, parentObjectPath)
|
||||
);
|
||||
},
|
||||
async updateTreeItems(parentObjectPath) {
|
||||
let children;
|
||||
|
||||
if (parentObjectPath.length) {
|
||||
const parentItem = this.treeItems.find(item => item.objectPath === parentObjectPath);
|
||||
const descendants = this.getChildrenInTreeFor(parentItem, true);
|
||||
const parentIndex = this.treeItems.map(e => e.object).indexOf(parentObjectPath[0]);
|
||||
|
||||
children = await this.loadAndBuildTreeItemsFor(parentItem.object, parentItem.objectPath);
|
||||
|
||||
this.treeItems.splice(parentIndex + 1, descendants.length, ...children);
|
||||
} else {
|
||||
const root = await this.openmct.objects.get('ROOT');
|
||||
children = await this.loadAndBuildTreeItemsFor(root, []);
|
||||
|
||||
this.treeItems = [...children];
|
||||
}
|
||||
|
||||
for (let item of children) {
|
||||
if (this.isTreeItemOpen(item)) {
|
||||
this.openTreeItem(item);
|
||||
}
|
||||
}
|
||||
},
|
||||
sortTreeItems(parentObjectPath) {
|
||||
const navigationPath = this.buildNavigationPath(parentObjectPath);
|
||||
const parentItem = this.getTreeItemByPath(navigationPath);
|
||||
@@ -662,6 +674,10 @@ export default {
|
||||
const descendants = this.getChildrenInTreeFor(parentItem, true);
|
||||
const directDescendants = this.getChildrenInTreeFor(parentItem);
|
||||
|
||||
if (domainObject.isMutable) {
|
||||
this.addMutable(domainObject, parentItem.objectPath);
|
||||
}
|
||||
|
||||
this.addTreeItemObserver(domainObject, parentItem.objectPath);
|
||||
|
||||
if (directDescendants.length === 0) {
|
||||
@@ -692,13 +708,15 @@ export default {
|
||||
},
|
||||
compositionRemoveHandler(navigationPath) {
|
||||
return (identifier) => {
|
||||
let removeKeyString = this.openmct.objects.makeKeyString(identifier);
|
||||
let parentItem = this.getTreeItemByPath(navigationPath);
|
||||
let directDescendants = this.getChildrenInTreeFor(parentItem);
|
||||
let removeItem = directDescendants.find(item => item.id === removeKeyString);
|
||||
const removeKeyString = this.openmct.objects.makeKeyString(identifier);
|
||||
const parentItem = this.getTreeItemByPath(navigationPath);
|
||||
const directDescendants = this.getChildrenInTreeFor(parentItem);
|
||||
const removeItem = directDescendants.find(item => item.id === removeKeyString);
|
||||
|
||||
// Remove the item from the tree, unobserve it, and clean up any mutables
|
||||
this.removeItemFromTree(removeItem);
|
||||
this.removeItemFromObservers(removeItem);
|
||||
this.destroyObserverByPath(removeItem.navigationPath);
|
||||
this.destroyMutableByPath(removeItem.navigationPath);
|
||||
};
|
||||
},
|
||||
removeCompositionListenerFor(navigationPath) {
|
||||
@@ -720,13 +738,6 @@ export default {
|
||||
const removeIndex = this.getTreeItemIndex(item.navigationPath);
|
||||
this.treeItems.splice(removeIndex, 1);
|
||||
},
|
||||
removeItemFromObservers(item) {
|
||||
if (this.observers[item.id]) {
|
||||
this.observers[item.id]();
|
||||
|
||||
delete this.observers[item.id];
|
||||
}
|
||||
},
|
||||
addItemToTreeBefore(addItem, beforeItem) {
|
||||
const addIndex = this.getTreeItemIndex(beforeItem.navigationPath);
|
||||
|
||||
@@ -792,12 +803,17 @@ export default {
|
||||
|
||||
for (const result of results) {
|
||||
if (!abortSignal.aborted) {
|
||||
// Don't show deleted objects in search results
|
||||
if (result.location === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
resultPromises.push(this.openmct.objects.getOriginalPath(result.identifier).then((objectPath) => {
|
||||
// removing the item itself, as the path we pass to buildTreeItem is a parent path
|
||||
objectPath.shift();
|
||||
|
||||
// if root, remove, we're not using in object path for tree
|
||||
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
|
||||
const lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
|
||||
if (lastObject && lastObject.type === 'root') {
|
||||
objectPath.pop();
|
||||
}
|
||||
@@ -959,13 +975,46 @@ export default {
|
||||
handleTreeResize() {
|
||||
this.calculateHeights();
|
||||
},
|
||||
destroyObservers(observers) {
|
||||
Object.entries(observers).forEach(([keyString, unobserve]) => {
|
||||
if (typeof unobserve === 'function') {
|
||||
/**
|
||||
* Destroy an observer for the given navigationPath.
|
||||
*/
|
||||
destroyObserverByPath(navigationPath) {
|
||||
if (this.observers[navigationPath]) {
|
||||
this.observers[navigationPath]();
|
||||
delete this.observers[navigationPath];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Destroy all observers.
|
||||
*/
|
||||
destroyObservers() {
|
||||
Object.entries(this.observers).forEach(([key, unobserve]) => {
|
||||
if (unobserve) {
|
||||
unobserve();
|
||||
}
|
||||
|
||||
delete observers[keyString];
|
||||
delete this.observers[key];
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Destroy a mutable for the given navigationPath.
|
||||
*/
|
||||
destroyMutableByPath(navigationPath) {
|
||||
if (this.mutables[navigationPath]) {
|
||||
this.mutables[navigationPath]();
|
||||
delete this.mutables[navigationPath];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Destroy all mutables.
|
||||
*/
|
||||
destroyMutables() {
|
||||
Object.entries(this.mutables).forEach(([key, destroyMutable]) => {
|
||||
if (destroyMutable) {
|
||||
destroyMutable();
|
||||
}
|
||||
|
||||
delete this.mutables[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user