mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 10:44:21 +00:00
Compare commits
86 Commits
v2.0.4
...
operator-s
Author | SHA1 | Date | |
---|---|---|---|
a181faff4e | |||
10c26ac39a | |||
6521b888d6 | |||
7d3d68277b | |||
85fce3c456 | |||
8d577a8958 | |||
a34190f44c | |||
9c8ee09960 | |||
9568da9d5f | |||
2aa3b810ba | |||
1cdbb34e21 | |||
f6f8db3142 | |||
95299336d0 | |||
4619ab23f5 | |||
8bcbb776c5 | |||
ab93b61530 | |||
35e2d2c643 | |||
3663f22807 | |||
b8ff5c7f33 | |||
d58111c0ce | |||
9ede023cfa | |||
308e621b5d | |||
e6b5870234 | |||
be8e9ba605 | |||
0d6359bc58 | |||
60d759f76c | |||
28e3d2e066 | |||
934285b3fb | |||
fe7b368cbd | |||
03e7d912be | |||
f8838e5a6e | |||
aabde3df80 | |||
c51cf72fa7 | |||
72cbd3dd3b | |||
d93ed2b69c | |||
22e45848af | |||
09da373d1c | |||
c94bab7964 | |||
63363ccbb9 | |||
bdfc99ed03 | |||
458e9211e9 | |||
5399e06370 | |||
e76fed4005 | |||
f4af7aa2f4 | |||
ad081c0db7 | |||
32b1ccff5b | |||
b8d9e41c01 | |||
815e7d169c | |||
343ea86f48 | |||
98e0fc5bfb | |||
58387e0902 | |||
0a0826f87e | |||
e063442d8c | |||
6a5823ab5c | |||
fb2cbc72ba | |||
c85e6904a3 | |||
0493e5ae3c | |||
24f13b6249 | |||
6918d7d3d9 | |||
221fb4d6bf | |||
814d8a8380 | |||
5035403507 | |||
1fc082afe5 | |||
257742b45b | |||
44edec4f04 | |||
441e24a78a | |||
7ec02258e8 | |||
45c66b758b | |||
e197d39ce0 | |||
ab4d0dd37f | |||
c089a4760d | |||
b77a4066f2 | |||
20d7e80502 | |||
d63fec51a7 | |||
c8a4ff09f8 | |||
2550d93b1e | |||
7bcf136f43 | |||
245d61adda | |||
18b05b6faf | |||
b950031eae | |||
a72aa10d7b | |||
c5ff94ad7a | |||
e75cc35cb7 | |||
d30c4fcb53 | |||
f216bd8769 | |||
79a430278e |
@ -2,7 +2,7 @@ version: 2.1
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.19.2-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.21.1-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
parameters:
|
||||
@ -64,7 +64,7 @@ commands:
|
||||
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
|
||||
orbs:
|
||||
node: circleci/node@4.9.0
|
||||
browser-tools: circleci/browser-tools@1.2.3
|
||||
browser-tools: circleci/browser-tools@1.3.0
|
||||
jobs:
|
||||
npm-audit:
|
||||
parameters:
|
||||
|
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -40,6 +40,7 @@ assignees: ''
|
||||
- [ ] Is there a workaround available?
|
||||
- [ ] Does this impact a critical component?
|
||||
- [ ] Is this just a visual bug with no functional impact?
|
||||
- [ ] Does this block the execution of e2e tests?
|
||||
|
||||
#### Additional Information
|
||||
<!--- Include any screenshots, gifs, or logs which will expedite triage -->
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -16,7 +16,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
|
||||
* [ ] Unit tests included and/or updated with changes?
|
||||
* [ ] Command line build passes?
|
||||
* [ ] Has this been smoke tested?
|
||||
* [ ] Testing instructions included in associated issue?
|
||||
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
|
||||
|
||||
### Reviewer Checklist
|
||||
|
||||
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -32,12 +32,12 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
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.19.2 install
|
||||
- run: npx playwright@1.21.1 install
|
||||
- run: npm install
|
||||
- run: npm run test:e2e:full
|
||||
- name: Archive test results
|
||||
|
2
.github/workflows/e2e-visual.yml
vendored
2
.github/workflows/e2e-visual.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.19.2 install
|
||||
- run: npx playwright@1.21.1 install
|
||||
- run: npm install
|
||||
- name: Run the e2e visual tests
|
||||
run: npm run test:e2e:visual
|
||||
|
27
e2e/fixtures.js
Normal file
27
e2e/fixtures.js
Normal file
@ -0,0 +1,27 @@
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
// This file extends the base functionality of the playwright test framework
|
||||
const base = require('@playwright/test');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
exports.test = base.test.extend({
|
||||
page: async ({ baseURL, page }, use) => {
|
||||
const messages = [];
|
||||
page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`));
|
||||
await use(page);
|
||||
await expect.soft(messages.toString()).not.toContain('[error]');
|
||||
},
|
||||
browser: async ({ playwright, browser }, use, workerInfo) => {
|
||||
// Use browserless if configured
|
||||
if (workerInfo.project.name.match(/browserless/)) {
|
||||
const vBrowser = await playwright.chromium.connectOverCDP({
|
||||
endpointURL: 'ws://localhost:3003'
|
||||
});
|
||||
await use(vBrowser);
|
||||
} else {
|
||||
// Use Local Browser for testing.
|
||||
await use(browser);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -2,13 +2,14 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { devices } = require('@playwright/test');
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 2,
|
||||
retries: 1,
|
||||
testDir: 'tests',
|
||||
timeout: 90 * 1000,
|
||||
timeout: 60 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
port: 8080,
|
||||
@ -28,12 +29,12 @@ const config = {
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
...devices['Desktop Chrome']
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
|
@ -2,6 +2,7 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { devices } = require('@playwright/test');
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
@ -29,12 +30,12 @@ const config = {
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
...devices['Desktop Chrome']
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
|
79
e2e/tests/api/forms/forms.e2e.spec.js
Normal file
79
e2e/tests/api/forms/forms.e2e.spec.js
Normal file
@ -0,0 +1,79 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify form functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const TEST_FOLDER = 'test folder';
|
||||
|
||||
test.describe('forms set', () => {
|
||||
test('New folder form has title as required field', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.click('button:has-text("Create")');
|
||||
// Click :nth-match(:text("Folder"), 2)
|
||||
await page.click(':nth-match(:text("Folder"), 2)');
|
||||
// Click text=Properties Title Notes >> input[type="text"]
|
||||
await page.click('text=Properties Title Notes >> input[type="text"]');
|
||||
// Fill text=Properties Title Notes >> input[type="text"]
|
||||
await page.fill('text=Properties Title Notes >> input[type="text"]', '');
|
||||
// Press Tab
|
||||
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
|
||||
// Click text=OK Cancel
|
||||
await page.click('text=OK', { force: true });
|
||||
|
||||
const okButton = page.locator('text=OK');
|
||||
|
||||
await expect(okButton).toBeDisabled();
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
|
||||
|
||||
// Click text=Properties Title Notes >> input[type="text"]
|
||||
await page.click('text=Properties Title Notes >> input[type="text"]');
|
||||
// Fill text=Properties Title Notes >> input[type="text"]
|
||||
await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER);
|
||||
// Press Tab
|
||||
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
|
||||
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER);
|
||||
});
|
||||
test.fixme('Create all object types and verify correctness', async ({ page }) => {
|
||||
//Create the following Domain Objects with their unique Object Types
|
||||
// Sine Wave Generator (number object)
|
||||
// Timer Object
|
||||
// Plan View Object
|
||||
// Clock Object
|
||||
// Hyperlink
|
||||
});
|
||||
});
|
@ -24,7 +24,8 @@
|
||||
This test suite is dedicated to tests which verify branding related components.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Branding tests', () => {
|
||||
test('About Modal launches with basic branding properties', async ({ page }) => {
|
||||
@ -57,6 +58,6 @@ test.describe('Branding tests', () => {
|
||||
page.waitForEvent('popup'),
|
||||
page.locator('text=click here for third party licensing information').click()
|
||||
]);
|
||||
expect(page2.waitForURL('**\/licenses**')).toBeTruthy();
|
||||
expect(page2.waitForURL('**/licenses**')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -24,7 +24,8 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Example Event Generator Operations', () => {
|
||||
test('Can create example event generator with a name', async ({ page }) => {
|
||||
|
@ -24,7 +24,8 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Sine Wave Generator', () => {
|
||||
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => {
|
||||
|
@ -24,19 +24,119 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Move item tests', () => {
|
||||
test.fixme('Create a basic object and verify that it can be moved to another Folder', async ({ page }) => {
|
||||
//Create and save Folder
|
||||
//Create and save Domain Object
|
||||
//Verify that the newly created domain object can be moved to Folder from Step 1.
|
||||
//Verify that newly moved object appears in the correct point in Tree
|
||||
//Verify that newly moved object appears correctly in Inspector panel
|
||||
test('Create a basic object and verify that it can be moved to another folder', async ({ page }) => {
|
||||
// Go to Open MCT
|
||||
await page.goto('/');
|
||||
|
||||
// Create a new folder in the root my items folder
|
||||
let folder1 = "Folder1";
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li.icon-folder').click();
|
||||
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// Create another folder with a new name at default location, which is currently inside Folder 1
|
||||
let folder2 = "Folder2";
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li.icon-folder').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// Move Folder 2 from Folder 1 to My Items
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click();
|
||||
|
||||
await page.locator(`a:has-text("${folder2}")`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator('form[name="mctForm"] >> text=My Items').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Expect that Folder 2 is in My Items, the root folder
|
||||
expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy();
|
||||
});
|
||||
test.fixme('Create a basic object and verify that it cannot be moved to object without Composition Provider', async ({ page }) => {
|
||||
//Create and save Telemetry Object
|
||||
//Create and save Domain Object
|
||||
//Verify that the newly created domain object cannot be moved to Telemetry Object from step 1.
|
||||
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page }) => {
|
||||
// Go to Open MCT
|
||||
await page.goto('/');
|
||||
|
||||
// Create Telemetry Table
|
||||
let telemetryTable = 'Test Telemetry Table';
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Telemetry Table")').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Finish editing and save Telemetry Table
|
||||
await page.locator('.c-button--menu.c-button--major.icon-save').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create New Folder Basic Domain Object
|
||||
let folder = 'Test Folder';
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Folder")').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled = await okButton.isDisabled();
|
||||
expect.soft(okButtonStateDisabled).toBeTruthy();
|
||||
|
||||
// Continue test regardless of assertion and create it in My Items
|
||||
await page.locator('form[name="mctForm"] >> text=My Items').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Open My Items
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
|
||||
// Select Folder Object and select Move from context menu
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`a:has-text("${folder}")`).click()
|
||||
]);
|
||||
await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object after creation
|
||||
await page.locator('text=Location Open MCT My Items >> span').nth(3).click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled2 = await okButton2.isDisabled();
|
||||
expect(okButtonStateDisabled2).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -24,7 +24,8 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
|
||||
// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651
|
||||
|
@ -24,7 +24,10 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test.fixme('Create a basic object and verify that it can be exported as JSON from Tree', async ({ page }) => {
|
||||
|
@ -24,7 +24,10 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {
|
||||
|
@ -24,7 +24,8 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Clock Generator', () => {
|
||||
|
||||
|
@ -21,45 +21,164 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
|
||||
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
|
||||
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Condition Set Operations', () => {
|
||||
test('Create new button `condition set` creates new condition object', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
let conditionSetUrl;
|
||||
let getConditionSetIdentifierFromUrl;
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click text=Condition Set
|
||||
await page.click('text=Condition Set');
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=OK
|
||||
// Click text=Condition Set
|
||||
await page.click('text=Condition Set');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
|
||||
|
||||
//Set object identifier from url
|
||||
conditionSetUrl = await page.url();
|
||||
console.log('conditionSetUrl ' + conditionSetUrl);
|
||||
|
||||
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
|
||||
console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
|
||||
|
||||
});
|
||||
|
||||
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
//Load localStorage for subsequent tests
|
||||
test.use({ storageState: './e2e/tests/recycled_storage.json' });
|
||||
|
||||
//Begin suite of tests again localStorage
|
||||
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
|
||||
//Navigate to baseURL with injected localStorage
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
|
||||
page.click('text=OK')
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
//Re-verify after reload
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
|
||||
|
||||
});
|
||||
test.fixme('condition set object properties exist', async ({ page }) => {
|
||||
//Go to object created in step one
|
||||
//Verify the Condition Set properties persist on Save
|
||||
//Verify the Condition Set properties persist on page.reload()
|
||||
});
|
||||
test.fixme('condition set object can be modified', async ({ page }) => {
|
||||
//Go to object created in step one
|
||||
test('condition set object can be modified on @localStorage', async ({ page }) => {
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Update the Condition Set properties
|
||||
//Verify the Condition Set properties persist on Save
|
||||
//Verify the Condition Set properties persist on page.reload()
|
||||
// Click Edit Button
|
||||
await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
|
||||
|
||||
//Edit Condition Set Name from main view
|
||||
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
|
||||
await page.locator('text=Renamed Condition Set').first().press('Enter');
|
||||
// Click Save Button
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click Save and Finish Editing Option
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
//Verify Main section reflects updated Name Property
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
|
||||
|
||||
// Verify Inspector properties
|
||||
// Verify Inspector has updated Name property
|
||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
|
||||
// Verify Inspector Details has updated Name property
|
||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
||||
|
||||
// Verify Tree reflects updated Name proprety
|
||||
// Expand Tree
|
||||
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
|
||||
// Verify Condition Set Object is renamed in Tree
|
||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
// Verify Search Tree reflects renamed Name property
|
||||
await page.locator('input[type="search"]').fill('Renamed');
|
||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
//Verify Main section reflects updated Name Property
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
|
||||
|
||||
// Verify Inspector properties
|
||||
// Verify Inspector has updated Name property
|
||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
|
||||
// Verify Inspector Details has updated Name property
|
||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
||||
|
||||
// Verify Tree reflects updated Name proprety
|
||||
// Expand Tree
|
||||
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
|
||||
// Verify Condition Set Object is renamed in Tree
|
||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
// Verify Search Tree reflects renamed Name property
|
||||
await page.locator('input[type="search"]').fill('Renamed');
|
||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
});
|
||||
test.fixme('condition set object can be deleted', async ({ page }) => {
|
||||
//Go to object created in step one
|
||||
//Verify that Condition Set object can be deleted
|
||||
//Verify the Condition Set object does not exist in Tree
|
||||
//Verify the Condition Set object does not exist with direct navigation to object's URL
|
||||
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Expect Unnamed Condition Set to be visible in Main View
|
||||
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
|
||||
|
||||
// Search for Unnamed Condition Set
|
||||
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
|
||||
// Right Click to Open Actions Menu
|
||||
await page.locator('a:has-text("Unnamed Condition Set")').click({
|
||||
button: 'right'
|
||||
});
|
||||
// Click Remove Action
|
||||
await page.locator('text=Remove').click();
|
||||
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
//Expect Unnamed Condition Set to be removed in Main View
|
||||
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
|
||||
|
||||
await page.locator('.c-search__clear-input').click();
|
||||
// Search for Unnamed Condition Set
|
||||
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
|
||||
// Expect Unnamed Condition Set to be removed
|
||||
await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
|
||||
|
||||
//Feature?
|
||||
//Domain Object is still available by direct URL after delete
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -24,13 +24,15 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
|
||||
but only assume that example imagery is present.
|
||||
*/
|
||||
/* globals process */
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Example Imagery', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on('console', msg => console.log(msg.text()))
|
||||
page.on('console', msg => console.log(msg.text()));
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
@ -40,12 +42,18 @@ test.describe('Example Imagery', () => {
|
||||
// Click text=Example Imagery
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
||||
await page.click('form[name="mctForm"] a:has-text("My Items")');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
|
||||
page.click('text=OK')
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
});
|
||||
|
||||
@ -77,9 +85,11 @@ test.describe('Example Imagery', () => {
|
||||
|
||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
|
||||
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
|
||||
// zoom in
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
await bgImageLocator.hover();
|
||||
@ -91,40 +101,49 @@ test.describe('Example Imagery', () => {
|
||||
// center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
//Get Diagnostic info about process environment
|
||||
console.log('process.platform is ' + process.platform);
|
||||
const getUA = await page.evaluate(() => navigator.userAgent);
|
||||
console.log('navigator.userAgent ' + getUA);
|
||||
// Pan Imagery Hints
|
||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
||||
expect(expectedAltText).toEqual(imageryHintsText);
|
||||
|
||||
// pan right
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
||||
|
||||
// pan left
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
||||
|
||||
// pan up
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
|
||||
|
||||
// pan down
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
||||
|
||||
@ -156,20 +175,26 @@ test.describe('Example Imagery', () => {
|
||||
|
||||
test('Can use the reset button to reset the image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
|
||||
const zoomInBtn = await page.locator('.t-btn-zoom-in');
|
||||
const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
|
||||
const initialBoundingBox = await bgImageLocator.boundingBox();
|
||||
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
|
||||
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
await zoomResetBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
|
||||
const resetBoundingBox = await bgImageLocator.boundingBox();
|
||||
@ -180,38 +205,160 @@ test.describe('Example Imagery', () => {
|
||||
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
|
||||
});
|
||||
|
||||
//test('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
//test('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
//test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
//test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
//test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
//test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test('Using the zoom features does not pause telemetry', async ({ page }) => {
|
||||
const bgImageLocator = page.locator(backgroundImageSelector);
|
||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
|
||||
// open the time conductor drop down
|
||||
await page.locator('.c-conductor__controls button.c-mode-button').click();
|
||||
// Click local clock
|
||||
await page.locator('.icon-clock >> text=Local Clock').click();
|
||||
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in');
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
|
||||
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Display layout', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
// The following test case will cover these scenarios
|
||||
// ('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
// ('Can use alt+drag to move around image once zoomed in');
|
||||
// ('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
// ('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
// ('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
test('Example Imagery in Display layout', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Clear and set Image load delay (milliseconds)
|
||||
await page.click('input[type="number"]', {clickCount: 3});
|
||||
await page.type('input[type="number"]', "20");
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
// Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
|
||||
// Click previous image button
|
||||
const previousImageButton = await page.locator('.c-nav--prev');
|
||||
await previousImageButton.click();
|
||||
|
||||
// Verify previous image
|
||||
const selectedImage = page.locator('.selected');
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Zoom in
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await bgImageLocator.hover();
|
||||
const deltaYStep = 100; // equivalent to 1x zoom
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
const zoomedBoundingBox = await bgImageLocator.boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
|
||||
// Center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// Pan Imagery Hints
|
||||
console.log('process.platform is ' + process.platform);
|
||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
||||
expect(expectedAltText).toEqual(imageryHintsText);
|
||||
|
||||
// Click next image button
|
||||
const nextImageButton = await page.locator('.c-nav--next');
|
||||
await nextImageButton.click();
|
||||
|
||||
// Click fixed timespan button
|
||||
await page.locator('.c-button__label >> text=Fixed Timespan').click();
|
||||
|
||||
// Click local clock
|
||||
await page.locator('.icon-clock >> text=Local Clock').click();
|
||||
|
||||
// Zoom in on next image
|
||||
await bgImageLocator.hover();
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
|
||||
// Click previous image button
|
||||
await previousImageButton.click();
|
||||
|
||||
// Verify previous image
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Wait 20ms to verify no new image has come in
|
||||
await page.waitForTimeout(21);
|
||||
|
||||
//Get background-image url from background-image css prop
|
||||
const backgroundImage = await page.locator('.c-imagery__main-image__background-image');
|
||||
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
|
||||
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
|
||||
|
||||
// sleep 21ms
|
||||
await page.waitForTimeout(21);
|
||||
|
||||
// Verify next image has updated
|
||||
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
let backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
|
||||
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
|
||||
|
||||
// Expect backgroundImageUrl2 to be greater then backgroundImageUrl1
|
||||
expect(backgroundImageUrl2 >= backgroundImageUrl1);
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Tabs view', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
@ -21,10 +21,11 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Test for plot autoscale.
|
||||
Testsuite for plot autoscale.
|
||||
*/
|
||||
|
||||
const { test: _test, expect } = require('@playwright/test');
|
||||
const { test: _test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
// create a new `test` API that will not append platform details to snapshot
|
||||
// file names, only for the tests in this file, so that the same snapshots will
|
||||
@ -47,7 +48,10 @@ test.use({
|
||||
});
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test('autoscale off causes no error from undefined user range', async ({ page }) => {
|
||||
test('User can set autoscale with a valid range @snapshot', async ({ page }) => {
|
||||
//This is necessary due to the size of the test suite.
|
||||
await test.setTimeout(120 * 1000);
|
||||
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
await setTimeRange(page);
|
||||
@ -68,14 +72,6 @@ test.describe('ExportAsJSON', () => {
|
||||
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
|
||||
]);
|
||||
|
||||
let errorCount = 0;
|
||||
|
||||
function onError() {
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
page.on('pageerror', onError);
|
||||
|
||||
await page.keyboard.down('Alt');
|
||||
|
||||
await canvas.dragTo(canvas, {
|
||||
@ -91,12 +87,6 @@ test.describe('ExportAsJSON', () => {
|
||||
|
||||
await page.keyboard.up('Alt');
|
||||
|
||||
page.off('pageerror', onError);
|
||||
|
||||
// There would have been an error at this point. So if there isn't, then
|
||||
// we fixed it.
|
||||
expect(errorCount).toBe(0);
|
||||
|
||||
// Ensure the drag worked.
|
||||
await Promise.all([
|
||||
testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
|
||||
@ -134,9 +124,14 @@ async function createSinewaveOverlayPlot(page) {
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.locator('text=OK').click()
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
@ -148,14 +143,19 @@ async function createSinewaveOverlayPlot(page) {
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396/5cfa5c69-17bc-4a99-9545-4da8125380c5?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-single' }*/),
|
||||
page.locator('text=OK').click()
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// focus the overlay plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
@ -168,11 +168,18 @@ async function turnOffAutoscale(page) {
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
|
||||
// uncheck autoscale
|
||||
await page.locator('text=Y Axis Scaling Auto scale Padding >> input[type="checkbox"]').uncheck();
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck();
|
||||
|
||||
// save
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
await Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -180,6 +187,7 @@ async function turnOffAutoscale(page) {
|
||||
*/
|
||||
async function testYTicks(page, values) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
await page.locator('canvas >> nth=1').hover();
|
||||
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
|
||||
|
||||
for (let i = 0, l = values.length; i < l; i += 1) {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 18 KiB |
@ -21,13 +21,18 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality.
|
||||
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('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Log plot tests', () => {
|
||||
test.only('Can create a log plot.', async ({ page }) => {
|
||||
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => {
|
||||
//This is necessary due to the size of the test suite.
|
||||
await test.setTimeout(120 * 1000);
|
||||
|
||||
await makeOverlayPlot(page);
|
||||
await testRegularTicks(page);
|
||||
await enableEditMode(page);
|
||||
@ -39,17 +44,20 @@ test.describe('Log plot tests', () => {
|
||||
await testLogTicks(page);
|
||||
await saveOverlayPlot(page);
|
||||
await testLogTicks(page);
|
||||
await testLogPlotPixels(page);
|
||||
//await testLogPlotPixels(page);
|
||||
|
||||
// refresh page
|
||||
await page.reload();
|
||||
// refresh page and wait for charts and ticks to load
|
||||
await page.waitForTimeout(1 * 1000);
|
||||
await page.reload({ waitUntil: 'networkidle'});
|
||||
await page.waitForSelector('.gl-plot-chart-area');
|
||||
await page.waitForSelector('.gl-plot-y-tick-label');
|
||||
|
||||
// test log ticks hold up after refresh
|
||||
await testLogTicks(page);
|
||||
await testLogPlotPixels(page);
|
||||
//await testLogPlotPixels(page);
|
||||
});
|
||||
|
||||
test.only('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
|
||||
test.skip('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
|
||||
await makeOverlayPlot(page);
|
||||
await enableEditMode(page);
|
||||
await enableLogMode(page);
|
||||
@ -57,7 +65,7 @@ test.describe('Log plot tests', () => {
|
||||
|
||||
// TODO ...export, delete the overlay, then import it...
|
||||
|
||||
await testLogTicks(page);
|
||||
//await testLogTicks(page);
|
||||
|
||||
// TODO, the plot is slightly at different position that in the other test, so this fails.
|
||||
// ...We can fix it by copying all steps from the first test...
|
||||
@ -88,14 +96,18 @@ async function makeOverlayPlot(page) {
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.locator('text=OK').click()
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save the overlay plot
|
||||
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
await saveOverlayPlot(page);
|
||||
|
||||
// create a sinewave generator
|
||||
|
||||
@ -116,15 +128,20 @@ async function makeOverlayPlot(page) {
|
||||
// Click OK to make generator
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f/6e58b26a-8a73-4df6-b3a6-918decc0bbfa?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-single' }*/),
|
||||
page.locator('text=OK').click()
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// click on overlay plot
|
||||
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
@ -133,7 +150,7 @@ async function makeOverlayPlot(page) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testRegularTicks(page) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
const yTicks = await page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(7);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2');
|
||||
await expect(yTicks.nth(1)).toHaveText('0');
|
||||
@ -148,7 +165,7 @@ async function testRegularTicks(page) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testLogTicks(page) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
const yTicks = await page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(28);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2.98');
|
||||
await expect(yTicks.nth(1)).toHaveText('-2.50');
|
||||
@ -186,6 +203,7 @@ async function testLogTicks(page) {
|
||||
async function enableEditMode(page) {
|
||||
// turn on edit mode
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
await expect(await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1)).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -210,17 +228,27 @@ async function disableLogMode(page) {
|
||||
async function saveOverlayPlot(page) {
|
||||
// save overlay plot
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
// FIXME: Remove this eslint exception once implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function testLogPlotPixels(page) {
|
||||
const pixelsMatch = await page.evaluate(async () => {
|
||||
// TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.
|
||||
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await new Promise((r) => setTimeout(r, 5 * 1000));
|
||||
|
||||
// These are some pixels that should be blue points in the log plot.
|
||||
// If the plot changes shape to an unexpected shape, this will
|
@ -20,7 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Time counductor operations', () => {
|
||||
test('validate start time does not exceeds end time', async ({ page }) => {
|
||||
@ -67,3 +68,42 @@ test.describe('Time counductor operations', () => {
|
||||
expect(endDateValidityStatus).not.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// Testing instructions:
|
||||
// Try to change the realtime offsets when in realtime (local clock) mode.
|
||||
test.describe('Time conductor input fields real-time mode', () => {
|
||||
test('validate input fields in real-time mode', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click fixed timespan button
|
||||
await page.locator('.c-button__label >> text=Fixed Timespan').click();
|
||||
|
||||
// Click local clock
|
||||
await page.locator('.icon-clock >> text=Local Clock').click();
|
||||
|
||||
// Click time offset button
|
||||
await page.locator('.c-conductor__delta-button >> text=00:30:00').click();
|
||||
|
||||
// Input start time offset
|
||||
await page.fill('.pr-time-controls__secs', '23');
|
||||
|
||||
// Click the check button
|
||||
await page.locator('.icon-check').click();
|
||||
|
||||
// Verify time was updated on time offset button
|
||||
await expect(page.locator('.c-conductor__delta-button').first()).toContainText('00:30:23');
|
||||
|
||||
// Click time offset set preceding now button
|
||||
await page.locator('.c-conductor__delta-button >> text=00:00:30').click();
|
||||
|
||||
// Input preceding time offset
|
||||
await page.fill('.pr-time-controls__secs', '31');
|
||||
|
||||
// Click the check buttons
|
||||
await page.locator('.icon-check').click();
|
||||
|
||||
// Verify time was updated on preceding time offset button
|
||||
await expect(page.locator('.c-conductor__delta-button').nth(1)).toContainText('00:00:31');
|
||||
});
|
||||
});
|
||||
|
22
e2e/tests/recycled_storage.json
Normal file
22
e2e/tests/recycled_storage.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:8080",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -33,7 +33,8 @@ comfortable running this test during a live mission?" Avoid creating or deleting
|
||||
Make no assumptions about the order that elements appear in the DOM.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {
|
||||
|
@ -24,16 +24,53 @@ import EventEmitter from 'EventEmitter';
|
||||
import uuid from 'uuid';
|
||||
import createExampleUser from './exampleUserCreator';
|
||||
|
||||
const STATUSES = [{
|
||||
key: "NO_STATUS",
|
||||
label: "Not set",
|
||||
iconClass: "icon-question-mark",
|
||||
iconClassPoll: "icon-status-poll-question-mark"
|
||||
}, {
|
||||
key: "GO",
|
||||
label: "GO",
|
||||
iconClass: "icon-check",
|
||||
iconClassPoll: "icon-status-poll-question-mark",
|
||||
statusClass: "s-status-ok",
|
||||
statusBgColor: "#33cc33",
|
||||
statusFgColor: "#000"
|
||||
}, {
|
||||
key: "MAYBE",
|
||||
label: "MAYBE",
|
||||
iconClass: "icon-alert-triangle",
|
||||
iconClassPoll: "icon-status-poll-question-mark",
|
||||
statusClass: "s-status-warning",
|
||||
statusBgColor: "#ffb66c",
|
||||
statusFgColor: "#000"
|
||||
}, {
|
||||
key: "NO_GO",
|
||||
label: "NO GO",
|
||||
iconClass: "icon-circle-slash",
|
||||
iconClassPoll: "icon-status-poll-question-mark",
|
||||
statusClass: "s-status-error",
|
||||
statusBgColor: "#9900cc",
|
||||
statusFgColor: "#fff"
|
||||
}];
|
||||
/**
|
||||
* @implements {StatusUserProvider}
|
||||
*/
|
||||
export default class ExampleUserProvider extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
constructor(openmct, {defaultStatusRole} = {defaultStatusRole: undefined}) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.user = undefined;
|
||||
this.loggedIn = false;
|
||||
this.autoLoginUser = undefined;
|
||||
this.status = STATUSES[1];
|
||||
this.pollQuestion = undefined;
|
||||
this.defaultStatusRole = defaultStatusRole;
|
||||
|
||||
this.ExampleUser = createExampleUser(this.openmct.user.User);
|
||||
this.loginPromise = undefined;
|
||||
}
|
||||
|
||||
isLoggedIn() {
|
||||
@ -45,11 +82,19 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
if (this.loggedIn) {
|
||||
return Promise.resolve(this.user);
|
||||
if (!this.loginPromise) {
|
||||
this.loginPromise = this._login().then(() => this.user);
|
||||
}
|
||||
|
||||
return this._login().then(() => this.user);
|
||||
return this.loginPromise;
|
||||
}
|
||||
|
||||
canProvideStatusForRole() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
canSetPollQuestion() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
hasRole(roleId) {
|
||||
@ -60,6 +105,55 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
return Promise.resolve(this.user.getRoles().includes(roleId));
|
||||
}
|
||||
|
||||
getStatusRoleForCurrentUser() {
|
||||
return Promise.resolve(this.defaultStatusRole);
|
||||
}
|
||||
|
||||
getAllStatusRoles() {
|
||||
return Promise.resolve([this.defaultStatusRole]);
|
||||
}
|
||||
|
||||
getStatusForRole(role) {
|
||||
return Promise.resolve(this.status);
|
||||
}
|
||||
|
||||
async getDefaultStatusForRole(role) {
|
||||
const allRoles = await this.getPossibleStatuses();
|
||||
|
||||
return allRoles?.[0];
|
||||
}
|
||||
|
||||
setStatusForRole(role, status) {
|
||||
this.status = status;
|
||||
this.emit('statusChange', {
|
||||
role,
|
||||
status
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getPollQuestion() {
|
||||
return Promise.resolve({
|
||||
question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
setPollQuestion(pollQuestion) {
|
||||
this.pollQuestion = {
|
||||
question: pollQuestion,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
this.emit("pollQuestionChange", this.pollQuestion);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getPossibleStatuses() {
|
||||
return Promise.resolve(STATUSES);
|
||||
}
|
||||
|
||||
_login() {
|
||||
const id = uuid();
|
||||
|
||||
@ -108,3 +202,6 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider
|
||||
*/
|
||||
|
@ -22,8 +22,19 @@
|
||||
|
||||
import ExampleUserProvider from './ExampleUserProvider';
|
||||
|
||||
export default function ExampleUserPlugin() {
|
||||
export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = {
|
||||
autoLoginUser: 'guest',
|
||||
defaultStatusRole: 'test-role'
|
||||
}) {
|
||||
return function install(openmct) {
|
||||
openmct.user.setProvider(new ExampleUserProvider(openmct));
|
||||
const userProvider = new ExampleUserProvider(openmct, {
|
||||
defaultStatusRole
|
||||
});
|
||||
|
||||
if (autoLoginUser !== undefined) {
|
||||
userProvider.autoLogin(autoLoginUser);
|
||||
}
|
||||
|
||||
openmct.user.setProvider(userProvider);
|
||||
};
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
} from '../../src/utils/testing';
|
||||
import ExampleUserProvider from './ExampleUserProvider';
|
||||
|
||||
xdescribe("The Example User Plugin", () => {
|
||||
describe("The Example User Plugin", () => {
|
||||
let openmct;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -47,9 +47,4 @@ xdescribe("The Example User Plugin", () => {
|
||||
});
|
||||
openmct.install(openmct.plugins.example.ExampleUser());
|
||||
});
|
||||
|
||||
// The rest of the functionality of the ExampleUser Plugin is
|
||||
// tested in both the UserAPISpec.js and in the UserIndicatorPlugin spec.
|
||||
// If that changes, those tests can be moved here.
|
||||
|
||||
});
|
||||
|
34
package.json
34
package.json
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.0.3-SNAPSHOT",
|
||||
"version": "2.0.4-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.16.3",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.0.4",
|
||||
"@percy/playwright": "1.0.2",
|
||||
"@playwright/test": "1.19.2",
|
||||
"@percy/playwright": "1.0.3",
|
||||
"@playwright/test": "1.21.1",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
"@types/jasmine": "^4.0.1",
|
||||
"@types/karma": "^6.3.2",
|
||||
@ -17,15 +17,15 @@
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "10.2.0",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "4.0.0",
|
||||
"d3-axis": "1.0.x",
|
||||
"d3-scale": "1.0.x",
|
||||
"d3-selection": "1.3.x",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.13.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.8.0",
|
||||
"eslint-plugin-playwright": "0.9.0",
|
||||
"eslint-plugin-vue": "8.5.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
@ -35,12 +35,12 @@
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "0.8.0",
|
||||
"jasmine-core": "4.0.1",
|
||||
"jasmine-core": "4.1.1",
|
||||
"jsdoc": "3.5.5",
|
||||
"karma": "6.3.18",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "2.1.1",
|
||||
"karma-coverage": "2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-firefox-launcher": "2.1.2",
|
||||
"karma-jasmine": "4.0.1",
|
||||
@ -52,13 +52,13 @@
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"moment": "2.29.1",
|
||||
"moment": "2.29.3",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"painterro": "1.2.56",
|
||||
"plotly.js-basic-dist": "2.5.0",
|
||||
"plotly.js-gl2d-dist": "2.5.0",
|
||||
"plotly.js-basic-dist": "2.12.0",
|
||||
"plotly.js-gl2d-dist": "2.12.0",
|
||||
"printj": "1.3.1",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
@ -82,8 +82,8 @@
|
||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
|
||||
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
|
||||
"start": "node app.js",
|
||||
"lint": "eslint example src --ext .js,.vue openmct.js",
|
||||
"lint:fix": "eslint example src --ext .js,.vue openmct.js --fix",
|
||||
"lint": "eslint example src e2e --ext .js,.vue openmct.js",
|
||||
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
|
||||
"build:prod": "cross-env webpack --config webpack.prod.js",
|
||||
"build:dev": "webpack --config webpack.dev.js",
|
||||
"build:coverage": "webpack --config webpack.coverage.js",
|
||||
@ -92,9 +92,9 @@
|
||||
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
||||
"test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
|
||||
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor",
|
||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor branding clock exampleImagery",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
"test:e2e:debug": "npm run test:e2e:local -- --debug",
|
||||
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
|
||||
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||
|
@ -241,10 +241,9 @@ define([
|
||||
this.branding = BrandingAPI.default;
|
||||
|
||||
// Plugins that are installed by default
|
||||
|
||||
this.install(this.plugins.Gauge());
|
||||
this.install(this.plugins.Plot());
|
||||
this.install(this.plugins.Chart());
|
||||
this.install(this.plugins.ScatterPlot());
|
||||
this.install(this.plugins.BarChart());
|
||||
this.install(this.plugins.TelemetryTable.default());
|
||||
this.install(PreviewPlugin.default());
|
||||
this.install(LicensesPlugin.default());
|
||||
|
@ -56,7 +56,7 @@ define([
|
||||
CompositionAPI: CompositionAPI,
|
||||
EditorAPI: EditorAPI,
|
||||
FormsAPI: FormsAPI,
|
||||
IndicatorAPI: IndicatorAPI,
|
||||
IndicatorAPI: IndicatorAPI.default,
|
||||
MenuAPI: MenuAPI.default,
|
||||
NotificationAPI: NotificationAPI.default,
|
||||
ObjectAPI: ObjectAPI,
|
||||
|
@ -23,10 +23,13 @@
|
||||
import FormController from './FormController';
|
||||
import FormProperties from './components/FormProperties.vue';
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default class FormsAPI {
|
||||
export default class FormsAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.formController = new FormController(openmct);
|
||||
}
|
||||
@ -107,6 +110,8 @@ export default class FormsAPI {
|
||||
let onDismiss;
|
||||
let onSave;
|
||||
|
||||
const self = this;
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
onSave = onFormSave(resolve);
|
||||
onDismiss = onFormDismiss(reject);
|
||||
@ -115,7 +120,7 @@ export default class FormsAPI {
|
||||
const vm = new Vue({
|
||||
components: { FormProperties },
|
||||
provide: {
|
||||
openmct: this.openmct
|
||||
openmct: self.openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -132,7 +137,7 @@ export default class FormsAPI {
|
||||
if (element) {
|
||||
element.append(formElement);
|
||||
} else {
|
||||
overlay = this.openmct.overlays.overlay({
|
||||
overlay = self.openmct.overlays.overlay({
|
||||
element: vm.$el,
|
||||
size: 'small',
|
||||
onDestroy: () => vm.$destroy()
|
||||
@ -140,6 +145,7 @@ export default class FormsAPI {
|
||||
}
|
||||
|
||||
function onFormPropertyChange(data) {
|
||||
self.emit('onFormPropertyChange', data);
|
||||
if (onChange) {
|
||||
onChange(data);
|
||||
}
|
||||
|
157
src/api/forms/FormsAPISpec.js
Normal file
157
src/api/forms/FormsAPISpec.js
Normal file
@ -0,0 +1,157 @@
|
||||
/*****************************************************************************
|
||||
* 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 { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
describe('The Forms API', () => {
|
||||
let openmct;
|
||||
let element;
|
||||
|
||||
beforeEach((done) => {
|
||||
element = document.createElement('div');
|
||||
element.style.display = 'block';
|
||||
element.style.width = '1920px';
|
||||
element.style.height = '1080px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
|
||||
openmct.startHeadless(element);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('openmct supports form API', () => {
|
||||
expect(openmct.forms).not.toBe(null);
|
||||
});
|
||||
|
||||
describe('check default form controls exists', () => {
|
||||
it('autocomplete', () => {
|
||||
const control = openmct.forms.getFormControl('autocomplete');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('clock', () => {
|
||||
const control = openmct.forms.getFormControl('composite');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('datetime', () => {
|
||||
const control = openmct.forms.getFormControl('datetime');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('file-input', () => {
|
||||
const control = openmct.forms.getFormControl('file-input');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('locator', () => {
|
||||
const control = openmct.forms.getFormControl('locator');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('numberfield', () => {
|
||||
const control = openmct.forms.getFormControl('numberfield');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('select', () => {
|
||||
const control = openmct.forms.getFormControl('select');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('textarea', () => {
|
||||
const control = openmct.forms.getFormControl('textarea');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('textfield', () => {
|
||||
const control = openmct.forms.getFormControl('textfield');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports user defined form controls', () => {
|
||||
const newFormControl = {
|
||||
show: () => {
|
||||
console.log('show new control');
|
||||
},
|
||||
destroy: () => {
|
||||
console.log('destroy');
|
||||
}
|
||||
};
|
||||
openmct.forms.addNewFormControl('newFormControl', newFormControl);
|
||||
const control = openmct.forms.getFormControl('newFormControl');
|
||||
expect(control).not.toBe(null);
|
||||
expect(control.show).not.toBe(null);
|
||||
expect(control.destroy).not.toBe(null);
|
||||
});
|
||||
|
||||
describe('show form on UI', () => {
|
||||
let formStructure;
|
||||
|
||||
beforeEach(() => {
|
||||
formStructure = {
|
||||
title: 'Test Show Form',
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: 'name',
|
||||
control: 'textfield',
|
||||
name: 'Title',
|
||||
pattern: '\\S+',
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: 'Test Name'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
it('when container element is provided', (done) => {
|
||||
openmct.forms.showForm(formStructure, { element }).catch(() => {
|
||||
done();
|
||||
});
|
||||
const titleElement = element.querySelector('.c-overlay__dialog-title');
|
||||
expect(titleElement.textContent).toBe(formStructure.title);
|
||||
|
||||
element.querySelector('.js-cancel-button').click();
|
||||
});
|
||||
|
||||
it('when container element is not provided', (done) => {
|
||||
openmct.forms.showForm(formStructure).catch(() => {
|
||||
done();
|
||||
});
|
||||
|
||||
const titleElement = document.querySelector('.c-overlay__dialog-title');
|
||||
const title = titleElement.textContent;
|
||||
|
||||
expect(title).toBe(formStructure.title);
|
||||
document.querySelector('.js-cancel-button').click();
|
||||
});
|
||||
});
|
||||
});
|
@ -21,9 +21,9 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-form">
|
||||
<div class="c-form js-form">
|
||||
<div class="c-overlay__top-bar c-form__top-bar">
|
||||
<div class="c-overlay__dialog-title">{{ model.title }}</div>
|
||||
<div class="c-overlay__dialog-title js-form-title">{{ model.title }}</div>
|
||||
<div class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
|
||||
</div>
|
||||
<form
|
||||
@ -70,7 +70,7 @@
|
||||
</button>
|
||||
<button
|
||||
tabindex="0"
|
||||
class="c-button"
|
||||
class="c-button js-cancel-button"
|
||||
@click="onDismiss"
|
||||
>
|
||||
{{ cancelLabel }}
|
||||
|
@ -40,6 +40,12 @@
|
||||
>
|
||||
{{ name }}
|
||||
</button>
|
||||
<button
|
||||
v-if="removable"
|
||||
class="c-button icon-trash"
|
||||
title="Remove file"
|
||||
@click="removeFile"
|
||||
></button>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
@ -63,6 +69,9 @@ export default {
|
||||
const fileInfo = this.fileInfo || this.model.value;
|
||||
|
||||
return fileInfo && fileInfo.name || this.model.text;
|
||||
},
|
||||
removable() {
|
||||
return (this.fileInfo || this.model.value) && this.model.removable;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -97,6 +106,15 @@ export default {
|
||||
},
|
||||
selectFile() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
removeFile() {
|
||||
this.model.value = undefined;
|
||||
this.fileInfo = undefined;
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: undefined
|
||||
};
|
||||
this.$emit('onChange', data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -19,27 +19,27 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
define([
|
||||
'./SimpleIndicator',
|
||||
'lodash'
|
||||
], function (
|
||||
SimpleIndicator,
|
||||
_
|
||||
) {
|
||||
function IndicatorAPI(openmct) {
|
||||
|
||||
import EventEmitter from "EventEmitter";
|
||||
import SimpleIndicator from "./SimpleIndicator";
|
||||
|
||||
class IndicatorAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.indicatorObjects = [];
|
||||
}
|
||||
|
||||
IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
|
||||
getIndicatorObjectsByPriority() {
|
||||
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
return sortedIndicators;
|
||||
};
|
||||
}
|
||||
|
||||
IndicatorAPI.prototype.simpleIndicator = function () {
|
||||
simpleIndicator() {
|
||||
return new SimpleIndicator(this.openmct);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts an indicator object, which is a simple object
|
||||
@ -62,14 +62,16 @@ define([
|
||||
* myIndicator.iconClass("icon-info");
|
||||
*
|
||||
*/
|
||||
IndicatorAPI.prototype.add = function (indicator) {
|
||||
add(indicator) {
|
||||
if (!indicator.priority) {
|
||||
indicator.priority = this.openmct.priority.DEFAULT;
|
||||
}
|
||||
|
||||
this.indicatorObjects.push(indicator);
|
||||
};
|
||||
|
||||
return IndicatorAPI;
|
||||
this.emit('addIndicator', indicator);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
export default IndicatorAPI;
|
||||
|
@ -20,82 +20,107 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['zepto', './res/indicator-template.html'],
|
||||
function ($, indicatorTemplate) {
|
||||
const DEFAULT_ICON_CLASS = 'icon-info';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import indicatorTemplate from './res/indicator-template.html';
|
||||
|
||||
function SimpleIndicator(openmct) {
|
||||
this.openmct = openmct;
|
||||
this.element = $(indicatorTemplate)[0];
|
||||
this.priority = openmct.priority.DEFAULT;
|
||||
const DEFAULT_ICON_CLASS = 'icon-info';
|
||||
|
||||
this.textElement = this.element.querySelector('.js-indicator-text');
|
||||
class SimpleIndicator extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
//Set defaults
|
||||
this.text('New Indicator');
|
||||
this.description('');
|
||||
this.iconClass(DEFAULT_ICON_CLASS);
|
||||
this.statusClass('');
|
||||
this.openmct = openmct;
|
||||
this.element = compileTemplate(indicatorTemplate)[0];
|
||||
this.priority = openmct.priority.DEFAULT;
|
||||
|
||||
this.textElement = this.element.querySelector('.js-indicator-text');
|
||||
|
||||
//Set defaults
|
||||
this.text('New Indicator');
|
||||
this.description('');
|
||||
this.iconClass(DEFAULT_ICON_CLASS);
|
||||
|
||||
this.click = this.click.bind(this);
|
||||
|
||||
this.element.addEventListener('click', this.click);
|
||||
openmct.once('destroy', () => {
|
||||
this.removeAllListeners();
|
||||
this.element.removeEventListener('click', this.click);
|
||||
});
|
||||
}
|
||||
|
||||
text(text) {
|
||||
if (text !== undefined && text !== this.textValue) {
|
||||
this.textValue = text;
|
||||
this.textElement.innerText = text;
|
||||
|
||||
if (!text) {
|
||||
this.element.classList.add('hidden');
|
||||
} else {
|
||||
this.element.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
SimpleIndicator.prototype.text = function (text) {
|
||||
if (text !== undefined && text !== this.textValue) {
|
||||
this.textValue = text;
|
||||
this.textElement.innerText = text;
|
||||
|
||||
if (!text) {
|
||||
this.element.classList.add('hidden');
|
||||
} else {
|
||||
this.element.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
return this.textValue;
|
||||
};
|
||||
|
||||
SimpleIndicator.prototype.description = function (description) {
|
||||
if (description !== undefined && description !== this.descriptionValue) {
|
||||
this.descriptionValue = description;
|
||||
this.element.title = description;
|
||||
}
|
||||
|
||||
return this.descriptionValue;
|
||||
};
|
||||
|
||||
SimpleIndicator.prototype.iconClass = function (iconClass) {
|
||||
if (iconClass !== undefined && iconClass !== this.iconClassValue) {
|
||||
// element.classList is precious and throws errors if you try and add
|
||||
// or remove empty strings
|
||||
if (this.iconClassValue) {
|
||||
this.element.classList.remove(this.iconClassValue);
|
||||
}
|
||||
|
||||
if (iconClass) {
|
||||
this.element.classList.add(iconClass);
|
||||
}
|
||||
|
||||
this.iconClassValue = iconClass;
|
||||
}
|
||||
|
||||
return this.iconClassValue;
|
||||
};
|
||||
|
||||
SimpleIndicator.prototype.statusClass = function (statusClass) {
|
||||
if (statusClass !== undefined && statusClass !== this.statusClassValue) {
|
||||
if (this.statusClassValue) {
|
||||
this.element.classList.remove(this.statusClassValue);
|
||||
}
|
||||
|
||||
if (statusClass) {
|
||||
this.element.classList.add(statusClass);
|
||||
}
|
||||
|
||||
this.statusClassValue = statusClass;
|
||||
}
|
||||
|
||||
return this.statusClassValue;
|
||||
};
|
||||
|
||||
return SimpleIndicator;
|
||||
return this.textValue;
|
||||
}
|
||||
);
|
||||
|
||||
description(description) {
|
||||
if (description !== undefined && description !== this.descriptionValue) {
|
||||
this.descriptionValue = description;
|
||||
this.element.title = description;
|
||||
}
|
||||
|
||||
return this.descriptionValue;
|
||||
}
|
||||
|
||||
iconClass(iconClass) {
|
||||
if (iconClass !== undefined && iconClass !== this.iconClassValue) {
|
||||
// element.classList is precious and throws errors if you try and add
|
||||
// or remove empty strings
|
||||
if (this.iconClassValue) {
|
||||
this.element.classList.remove(this.iconClassValue);
|
||||
}
|
||||
|
||||
if (iconClass) {
|
||||
this.element.classList.add(iconClass);
|
||||
}
|
||||
|
||||
this.iconClassValue = iconClass;
|
||||
}
|
||||
|
||||
return this.iconClassValue;
|
||||
}
|
||||
|
||||
statusClass(statusClass) {
|
||||
if (arguments.length === 1 && statusClass !== this.statusClassValue) {
|
||||
if (this.statusClassValue) {
|
||||
this.element.classList.remove(this.statusClassValue);
|
||||
}
|
||||
|
||||
if (statusClass !== undefined) {
|
||||
this.element.classList.add(statusClass);
|
||||
}
|
||||
|
||||
this.statusClassValue = statusClass;
|
||||
}
|
||||
|
||||
return this.statusClassValue;
|
||||
}
|
||||
|
||||
click(event) {
|
||||
this.emit('click', event);
|
||||
}
|
||||
|
||||
getElement() {
|
||||
return this.element;
|
||||
}
|
||||
}
|
||||
|
||||
function compileTemplate(htmlTemplate) {
|
||||
const templateNode = document.createElement('template');
|
||||
templateNode.innerHTML = htmlTemplate;
|
||||
|
||||
return templateNode.content.cloneNode(true).children;
|
||||
}
|
||||
|
||||
export default SimpleIndicator;
|
||||
|
@ -26,29 +26,31 @@ import { createOpenMct, createMouseEvent, resetApplicationState } from '../../ut
|
||||
|
||||
describe ('The Menu API', () => {
|
||||
let openmct;
|
||||
let element;
|
||||
let appHolder;
|
||||
let menuAPI;
|
||||
let actionsArray;
|
||||
let x;
|
||||
let y;
|
||||
let result;
|
||||
let onDestroy;
|
||||
let menuElement;
|
||||
|
||||
const x = 8;
|
||||
const y = 16;
|
||||
|
||||
const menuOptions = {
|
||||
onDestroy: () => {
|
||||
console.log('default onDestroy');
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach((done) => {
|
||||
const appHolder = document.createElement('div');
|
||||
appHolder = document.createElement('div');
|
||||
appHolder.style.display = 'block';
|
||||
appHolder.style.width = '1920px';
|
||||
appHolder.style.height = '1080px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
element = document.createElement('div');
|
||||
element.style.display = 'block';
|
||||
element.style.width = '1920px';
|
||||
element.style.height = '1080px';
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless(appHolder);
|
||||
openmct.startHeadless();
|
||||
|
||||
menuAPI = new MenuAPI(openmct);
|
||||
actionsArray = [
|
||||
@ -56,7 +58,7 @@ describe ('The Menu API', () => {
|
||||
key: 'test-css-class-1',
|
||||
name: 'Test Action 1',
|
||||
cssClass: 'icon-clock',
|
||||
description: 'This is a test action',
|
||||
description: 'This is a test action 1',
|
||||
onItemClicked: () => {
|
||||
result = 'Test Action 1 Invoked';
|
||||
}
|
||||
@ -65,149 +67,165 @@ describe ('The Menu API', () => {
|
||||
key: 'test-css-class-2',
|
||||
name: 'Test Action 2',
|
||||
cssClass: 'icon-clock',
|
||||
description: 'This is a test action',
|
||||
description: 'This is a test action 2',
|
||||
onItemClicked: () => {
|
||||
result = 'Test Action 2 Invoked';
|
||||
}
|
||||
}
|
||||
];
|
||||
x = 8;
|
||||
y = 16;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe("showMenu method", () => {
|
||||
it("creates an instance of Menu when invoked", () => {
|
||||
menuAPI.showMenu(x, y, actionsArray);
|
||||
|
||||
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
|
||||
describe('showMenu method', () => {
|
||||
beforeAll(() => {
|
||||
spyOn(menuOptions, 'onDestroy').and.callThrough();
|
||||
});
|
||||
|
||||
describe("creates a menu component", () => {
|
||||
let menuComponent;
|
||||
let vueComponent;
|
||||
it('creates an instance of Menu when invoked', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
beforeEach(() => {
|
||||
onDestroy = jasmine.createSpy('onDestroy');
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
const menuOptions = {
|
||||
onDestroy
|
||||
};
|
||||
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
|
||||
document.body.click();
|
||||
});
|
||||
|
||||
describe('creates a menu component', () => {
|
||||
it('with all the actions passed in', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
vueComponent = menuAPI.menuComponent.component;
|
||||
menuComponent = document.querySelector(".c-menu");
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
expect(menuElement).toBeDefined();
|
||||
|
||||
spyOn(vueComponent, '$destroy');
|
||||
});
|
||||
|
||||
it("renders a menu component in the expected x and y coordinates", () => {
|
||||
let boundingClientRect = menuComponent.getBoundingClientRect();
|
||||
let left = boundingClientRect.left;
|
||||
let top = boundingClientRect.top;
|
||||
|
||||
expect(left).toEqual(x);
|
||||
expect(top).toEqual(y);
|
||||
});
|
||||
|
||||
it("with all the actions passed in", () => {
|
||||
expect(menuComponent).toBeDefined();
|
||||
|
||||
let listItems = menuComponent.children[0].children;
|
||||
const listItems = menuElement.children[0].children;
|
||||
|
||||
expect(listItems.length).toEqual(actionsArray.length);
|
||||
document.body.click();
|
||||
});
|
||||
|
||||
it("with click-able menu items, that will invoke the correct callBacks", () => {
|
||||
let listItem1 = menuComponent.children[0].children[0];
|
||||
it('with click-able menu items, that will invoke the correct callBack', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
const listItem1 = menuElement.children[0].children[0];
|
||||
|
||||
listItem1.click();
|
||||
|
||||
expect(result).toEqual("Test Action 1 Invoked");
|
||||
expect(result).toEqual('Test Action 1 Invoked');
|
||||
});
|
||||
|
||||
it("dismisses the menu when action is clicked on", () => {
|
||||
let listItem1 = menuComponent.children[0].children[0];
|
||||
it('dismisses the menu when action is clicked on', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
const listItem1 = menuElement.children[0].children[0];
|
||||
listItem1.click();
|
||||
|
||||
let menu = document.querySelector('.c-menu');
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
|
||||
expect(menu).toBeNull();
|
||||
expect(menuElement).toBeNull();
|
||||
});
|
||||
|
||||
it("invokes the destroy method when menu is dismissed", () => {
|
||||
it('invokes the destroy method when menu is dismissed', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
const vueComponent = menuAPI.menuComponent.component;
|
||||
spyOn(vueComponent, '$destroy');
|
||||
|
||||
document.body.click();
|
||||
|
||||
expect(vueComponent.$destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("invokes the onDestroy callback if passed in", () => {
|
||||
document.body.click();
|
||||
it('invokes the onDestroy callback if passed in', (done) => {
|
||||
let count = 0;
|
||||
menuOptions.onDestroy = () => {
|
||||
count++;
|
||||
expect(count).toEqual(1);
|
||||
done();
|
||||
};
|
||||
|
||||
expect(onDestroy).toHaveBeenCalled();
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
document.body.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("superMenu method", () => {
|
||||
it("creates a superMenu", () => {
|
||||
menuAPI.showSuperMenu(x, y, actionsArray);
|
||||
describe('superMenu method', () => {
|
||||
it('creates a superMenu', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
const superMenu = document.querySelector('.c-super-menu__menu');
|
||||
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
|
||||
menuElement = document.querySelector('.c-super-menu__menu');
|
||||
|
||||
expect(superMenu).not.toBeNull();
|
||||
expect(menuElement).not.toBeNull();
|
||||
document.body.click();
|
||||
});
|
||||
|
||||
it("Mouse over a superMenu shows correct description", (done) => {
|
||||
menuAPI.showSuperMenu(x, y, actionsArray);
|
||||
it('Mouse over a superMenu shows correct description', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
const superMenu = document.querySelector('.c-super-menu__menu');
|
||||
const superMenuItem = superMenu.querySelector('li');
|
||||
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
|
||||
menuElement = document.querySelector('.c-super-menu__menu');
|
||||
|
||||
const superMenuItem = menuElement.querySelector('li');
|
||||
const mouseOverEvent = createMouseEvent('mouseover');
|
||||
|
||||
superMenuItem.dispatchEvent(mouseOverEvent);
|
||||
const itemDescription = document.querySelector('.l-item-description__description');
|
||||
|
||||
setTimeout(() => {
|
||||
menuAPI.menuComponent.component.$nextTick(() => {
|
||||
expect(menuElement).not.toBeNull();
|
||||
expect(itemDescription.innerText).toEqual(actionsArray[0].description);
|
||||
expect(superMenu).not.toBeNull();
|
||||
done();
|
||||
}, 300);
|
||||
|
||||
document.body.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Menu Placements", () => {
|
||||
it("default menu position BOTTOM_RIGHT", () => {
|
||||
menuAPI.showMenu(x, y, actionsArray);
|
||||
|
||||
const menu = document.querySelector('.c-menu');
|
||||
|
||||
const boundingClientRect = menu.getBoundingClientRect();
|
||||
const left = boundingClientRect.left;
|
||||
const top = boundingClientRect.top;
|
||||
|
||||
expect(left).toEqual(x);
|
||||
expect(top).toEqual(y);
|
||||
});
|
||||
|
||||
it("menu position BOTTOM_RIGHT", () => {
|
||||
const menuOptions = {
|
||||
placement: openmct.menus.menuPlacement.BOTTOM_RIGHT
|
||||
};
|
||||
describe('Menu Placements', () => {
|
||||
it('default menu position BOTTOM_RIGHT', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
|
||||
const menu = document.querySelector('.c-menu');
|
||||
const boundingClientRect = menu.getBoundingClientRect();
|
||||
const boundingClientRect = menuElement.getBoundingClientRect();
|
||||
const left = boundingClientRect.left;
|
||||
const top = boundingClientRect.top;
|
||||
|
||||
expect(left).toEqual(x);
|
||||
expect(top).toEqual(y);
|
||||
|
||||
document.body.click();
|
||||
});
|
||||
|
||||
it('menu position BOTTOM_RIGHT', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
|
||||
const boundingClientRect = menuElement.getBoundingClientRect();
|
||||
const left = boundingClientRect.left;
|
||||
const top = boundingClientRect.top;
|
||||
|
||||
expect(left).toEqual(x);
|
||||
expect(top).toEqual(y);
|
||||
|
||||
document.body.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -22,12 +22,14 @@
|
||||
|
||||
export default class Transaction {
|
||||
constructor(objectAPI) {
|
||||
this.dirtyObjects = new Set();
|
||||
this.dirtyObjects = {};
|
||||
this.objectAPI = objectAPI;
|
||||
}
|
||||
|
||||
add(object) {
|
||||
this.dirtyObjects.add(object);
|
||||
const key = this.objectAPI.makeKeyString(object.identifier);
|
||||
|
||||
this.dirtyObjects[key] = object;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
@ -37,7 +39,8 @@ export default class Transaction {
|
||||
commit() {
|
||||
const promiseArray = [];
|
||||
const save = this.objectAPI.save.bind(this.objectAPI);
|
||||
this.dirtyObjects.forEach(object => {
|
||||
|
||||
Object.values(this.dirtyObjects).forEach(object => {
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, save));
|
||||
});
|
||||
|
||||
@ -48,7 +51,9 @@ export default class Transaction {
|
||||
return new Promise((resolve, reject) => {
|
||||
action(object)
|
||||
.then((success) => {
|
||||
this.dirtyObjects.delete(object);
|
||||
const key = this.objectAPI.makeKeyString(object.identifier);
|
||||
|
||||
delete this.dirtyObjects[key];
|
||||
resolve(success);
|
||||
})
|
||||
.catch(reject);
|
||||
@ -57,7 +62,8 @@ export default class Transaction {
|
||||
|
||||
getDirtyObject(identifier) {
|
||||
let dirtyObject;
|
||||
this.dirtyObjects.forEach(object => {
|
||||
|
||||
Object.values(this.dirtyObjects).forEach(object => {
|
||||
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
|
||||
if (areIdsEqual) {
|
||||
dirtyObject = object;
|
||||
@ -67,14 +73,11 @@ export default class Transaction {
|
||||
return dirtyObject;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.dirtyObjects = new Set();
|
||||
}
|
||||
|
||||
_clear() {
|
||||
const promiseArray = [];
|
||||
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
|
||||
this.dirtyObjects.forEach(object => {
|
||||
|
||||
Object.values(this.dirtyObjects).forEach(object => {
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
|
||||
});
|
||||
|
||||
|
@ -34,24 +34,24 @@ describe("Transaction Class", () => {
|
||||
});
|
||||
|
||||
it('has no dirty objects', () => {
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
||||
});
|
||||
|
||||
it('add(), adds object to dirtyObjects', () => {
|
||||
const mockDomainObjects = createMockDomainObjects();
|
||||
transaction.add(mockDomainObjects[0]);
|
||||
expect(transaction.dirtyObjects.size).toEqual(1);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
|
||||
});
|
||||
|
||||
it('cancel(), clears all dirtyObjects', (done) => {
|
||||
const mockDomainObjects = createMockDomainObjects(3);
|
||||
mockDomainObjects.forEach(transaction.add.bind(transaction));
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(3);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
|
||||
|
||||
transaction.cancel()
|
||||
.then(success => {
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
||||
}).finally(done);
|
||||
});
|
||||
|
||||
@ -59,12 +59,12 @@ describe("Transaction Class", () => {
|
||||
const mockDomainObjects = createMockDomainObjects(3);
|
||||
mockDomainObjects.forEach(transaction.add.bind(transaction));
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(3);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
|
||||
spyOn(objectAPI, 'save').and.callThrough();
|
||||
|
||||
transaction.commit()
|
||||
.then(success => {
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
||||
expect(objectAPI.save.calls.count()).toEqual(3);
|
||||
}).finally(done);
|
||||
});
|
||||
@ -73,7 +73,7 @@ describe("Transaction Class", () => {
|
||||
const mockDomainObjects = createMockDomainObjects();
|
||||
transaction.add(mockDomainObjects[0]);
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(1);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
|
||||
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
|
||||
|
||||
expect(dirtyObject).toEqual(mockDomainObjects[0]);
|
||||
@ -82,7 +82,7 @@ describe("Transaction Class", () => {
|
||||
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
|
||||
const mockDomainObjects = createMockDomainObjects();
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
||||
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
|
||||
|
||||
expect(dirtyObject).toEqual(undefined);
|
||||
|
@ -512,7 +512,7 @@ define([
|
||||
TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
|
||||
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
|
||||
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
|
||||
const hasRequestProvider = Object.hasOwn(requestProvider, 'request');
|
||||
const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
|
||||
|
||||
return supportsRequest && hasRequestProvider;
|
||||
});
|
||||
|
@ -22,11 +22,7 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
const ERRORS = {
|
||||
TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.',
|
||||
LOADED: 'Telemetry Collection has already been loaded.'
|
||||
};
|
||||
import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants';
|
||||
|
||||
/** Class representing a Telemetry Collection. */
|
||||
|
||||
@ -61,7 +57,7 @@ export class TelemetryCollection extends EventEmitter {
|
||||
*/
|
||||
load() {
|
||||
if (this.loaded) {
|
||||
this._error(ERRORS.LOADED);
|
||||
this._error(LOADED_ERROR);
|
||||
}
|
||||
|
||||
this._setTimeSystem(this.openmct.time.timeSystem());
|
||||
@ -185,8 +181,8 @@ export class TelemetryCollection extends EventEmitter {
|
||||
|
||||
for (let datum of data) {
|
||||
parsedValue = this.parseTime(datum);
|
||||
beforeStartOfBounds = parsedValue <= this.lastBounds.start;
|
||||
afterEndOfBounds = parsedValue >= this.lastBounds.end;
|
||||
beforeStartOfBounds = parsedValue < this.lastBounds.start;
|
||||
afterEndOfBounds = parsedValue > this.lastBounds.end;
|
||||
|
||||
if (!afterEndOfBounds && !beforeStartOfBounds) {
|
||||
let isDuplicate = false;
|
||||
@ -267,6 +263,10 @@ export class TelemetryCollection extends EventEmitter {
|
||||
this.lastBounds = bounds;
|
||||
|
||||
if (isTick) {
|
||||
if (this.timeKey === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// need to check futureBuffer and need to check
|
||||
// if anything has fallen out of bounds
|
||||
let startIndex = 0;
|
||||
@ -306,7 +306,6 @@ export class TelemetryCollection extends EventEmitter {
|
||||
if (added.length > 0) {
|
||||
this.emit('add', added);
|
||||
}
|
||||
|
||||
} else {
|
||||
// user bounds change, reset
|
||||
this._reset();
|
||||
@ -326,12 +325,16 @@ export class TelemetryCollection extends EventEmitter {
|
||||
let domains = this.metadata.valuesForHints(['domain']);
|
||||
let domain = domains.find((d) => d.key === timeSystem.key);
|
||||
|
||||
if (domain === undefined) {
|
||||
this._error(ERRORS.TIMESYSTEM_KEY);
|
||||
if (domain !== undefined) {
|
||||
// timeKey is used to create a dummy datum used for sorting
|
||||
this.timeKey = domain.source;
|
||||
} else {
|
||||
this.timeKey = undefined;
|
||||
|
||||
this._warn(TIMESYSTEM_KEY_WARNING);
|
||||
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
|
||||
}
|
||||
|
||||
// timeKey is used to create a dummy datum used for sorting
|
||||
this.timeKey = domain.source; // this defaults to key if no source is set
|
||||
let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
@ -402,4 +405,8 @@ export class TelemetryCollection extends EventEmitter {
|
||||
_error(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
_warn(message) {
|
||||
console.warn(message);
|
||||
}
|
||||
}
|
||||
|
101
src/api/telemetry/TelemetryCollectionSpec.js
Normal file
101
src/api/telemetry/TelemetryCollectionSpec.js
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.
|
||||
*****************************************************************************/
|
||||
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
import { TIMESYSTEM_KEY_WARNING } from './constants';
|
||||
|
||||
describe('Telemetry Collection', () => {
|
||||
let openmct;
|
||||
let mockMetadataProvider;
|
||||
let mockMetadata = {};
|
||||
let domainObject;
|
||||
|
||||
beforeEach(done => {
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
|
||||
domainObject = {
|
||||
identifier: {
|
||||
key: 'a',
|
||||
namespace: 'b'
|
||||
},
|
||||
type: 'sample-type'
|
||||
};
|
||||
|
||||
mockMetadataProvider = {
|
||||
key: 'mockMetadataProvider',
|
||||
supportsMetadata() {
|
||||
return true;
|
||||
},
|
||||
getMetadata() {
|
||||
return mockMetadata;
|
||||
}
|
||||
};
|
||||
|
||||
openmct.telemetry.addProvider(mockMetadataProvider);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState();
|
||||
});
|
||||
|
||||
it('Warns if telemetry metadata does not match the active timesystem', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: 'foo',
|
||||
name: 'Bar',
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
|
||||
spyOn(telemetryCollection, '_warn');
|
||||
telemetryCollection.load();
|
||||
|
||||
expect(telemetryCollection._warn).toHaveBeenCalledOnceWith(TIMESYSTEM_KEY_WARNING);
|
||||
});
|
||||
|
||||
it('Does not warn if telemetry metadata matches the active timesystem', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: 'utc',
|
||||
name: 'Timestamp',
|
||||
format: 'utc',
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
|
||||
spyOn(telemetryCollection, '_warn');
|
||||
telemetryCollection.load();
|
||||
|
||||
expect(telemetryCollection._warn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -138,7 +138,7 @@ define([
|
||||
valueMetadata = this.values()[0];
|
||||
}
|
||||
|
||||
return valueMetadata.key;
|
||||
return valueMetadata;
|
||||
};
|
||||
|
||||
return TelemetryMetadataManager;
|
||||
|
25
src/api/telemetry/constants.js
Normal file
25
src/api/telemetry/constants.js
Normal file
@ -0,0 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
export const TIMESYSTEM_KEY_WARNING = 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.';
|
||||
export const TIMESYSTEM_KEY_NOTIFICATION = 'Telemetry metadata does not match the active time system.';
|
||||
export const LOADED_ERROR = 'Telemetry Collection has already been loaded.';
|
298
src/api/user/StatusAPI.js
Normal file
298
src/api/user/StatusAPI.js
Normal file
@ -0,0 +1,298 @@
|
||||
/*****************************************************************************
|
||||
* 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 EventEmitter from "EventEmitter";
|
||||
|
||||
export default class StatusAPI extends EventEmitter {
|
||||
#userAPI;
|
||||
#openmct;
|
||||
|
||||
constructor(userAPI, openmct) {
|
||||
super();
|
||||
this.#userAPI = userAPI;
|
||||
this.#openmct = openmct;
|
||||
|
||||
this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
|
||||
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
|
||||
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
|
||||
|
||||
this.#openmct.once('destroy', () => {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (typeof provider?.off === 'function') {
|
||||
provider.off('statusChange', this.onProviderStatusChange);
|
||||
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
|
||||
}
|
||||
});
|
||||
|
||||
this.#userAPI.on('providerAdded', this.listenToStatusEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status.
|
||||
* @returns {Promise<PollQuestion>}
|
||||
*/
|
||||
getPollQuestion() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getPollQuestion) {
|
||||
return provider.getPollQuestion();
|
||||
} else {
|
||||
this.#userAPI.error("User provider does not support polling questions");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status.
|
||||
* @param {String} questionText - The text of the question
|
||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
||||
*/
|
||||
async setPollQuestion(questionText) {
|
||||
if (this.canSetPollQuestion()) {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
const result = await provider.setPollQuestion(questionText);
|
||||
|
||||
// TODO re-implement clearing all statuses
|
||||
|
||||
try {
|
||||
await this.resetAllStatuses();
|
||||
} catch (error) {
|
||||
console.warn("Poll question set but unable to clear operator statuses.");
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
this.#userAPI.error("User provider does not support setting polling question");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the currently logged in user set the operator status poll question.
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
canSetPollQuestion() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.canSetPollQuestion) {
|
||||
return provider.canSetPollQuestion();
|
||||
} else {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
|
||||
*/
|
||||
async getPossibleStatuses() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getPossibleStatuses) {
|
||||
const possibleStatuses = await provider.getPossibleStatuses() || [];
|
||||
|
||||
return possibleStatuses.map(status => status);
|
||||
} else {
|
||||
this.#userAPI.error("User provider cannot provide statuses");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("./UserAPI").Role} role The role to fetch the current status for.
|
||||
* @returns {Promise<Status>} the current status of the provided role
|
||||
*/
|
||||
async getStatusForRole(role) {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getStatusForRole) {
|
||||
const status = await provider.getStatusForRole(role);
|
||||
if (status !== undefined) {
|
||||
return status;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
this.#userAPI.error("User provider does not support role status");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("./UserAPI").Role} role
|
||||
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role
|
||||
* @see StatusUserProvider
|
||||
*/
|
||||
canProvideStatusForRole(role) {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.canProvideStatusForRole) {
|
||||
return provider.canProvideStatusForRole(role);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("./UserAPI").Role} role The role to set the status for.
|
||||
* @param {Status} status The status to set for the provided role
|
||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
||||
*/
|
||||
setStatusForRole(role, status) {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.setStatusForRole) {
|
||||
return provider.setStatusForRole(role, status);
|
||||
} else {
|
||||
this.#userAPI.error("User provider does not support setting role status");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the status of the provided role back to its default status.
|
||||
* @param {import("./UserAPI").Role} role The role to set the status for.
|
||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
||||
*/
|
||||
async resetStatusForRole(role) {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
const defaultStatus = await this.getDefaultStatus();
|
||||
|
||||
if (provider.setStatusForRole) {
|
||||
return provider.setStatusForRole(role, defaultStatus);
|
||||
} else {
|
||||
this.#userAPI.error("User provider does not support resetting role status");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the status of all operators to their default status
|
||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
||||
*/
|
||||
async resetAllStatuses() {
|
||||
const allStatusRoles = await this.getAllStatusRoles();
|
||||
|
||||
return Promise.all(allStatusRoles.map(role => this.resetStatusForRole(role)));
|
||||
}
|
||||
|
||||
/**
|
||||
* The default status. This is the status that will be used before the user has selected any status.
|
||||
* @param {import("./UserAPI").Role} role
|
||||
* @returns {Promise<Status>} the default operator status if no other has been set.
|
||||
*/
|
||||
async getDefaultStatusForRole(role) {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
const defaultStatus = await provider.getDefaultStatusForRole(role);
|
||||
|
||||
return defaultStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* All possible status roles. A status role is a user role that can provide status. In some systems
|
||||
* this may be all user roles, but there may be cases where some users are not are not polled
|
||||
* for status if they do not have a real-time operational role.
|
||||
*
|
||||
* @returns {Promise<Array<import("./UserAPI").Role>>} the default operator status if no other has been set.
|
||||
*/
|
||||
getAllStatusRoles() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getAllStatusRoles) {
|
||||
return provider.getAllStatusRoles();
|
||||
} else {
|
||||
this.#userAPI.error("User provider cannot provide all status roles");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The status role of the current user. A user may have multiple roles, but will only have one role
|
||||
* that provides status at any time.
|
||||
* @returns {Promise<import("./UserAPI").Role>} the role for which the current user can provide status.
|
||||
*/
|
||||
getStatusRoleForCurrentUser() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getStatusRoleForCurrentUser) {
|
||||
return provider.getStatusRoleForCurrentUser();
|
||||
} else {
|
||||
this.#userAPI.error("User provider cannot provide role status for this user");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
|
||||
* @see StatusUserProvider
|
||||
*/
|
||||
async canProvideStatusForCurrentUser() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getStatusRoleForCurrentUser) {
|
||||
const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser();
|
||||
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
|
||||
|
||||
return canProvideStatus;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private internal function that cannot be made #private because it needs to be registered as a callback to the user provider
|
||||
* @private
|
||||
*/
|
||||
listenToStatusEvents(provider) {
|
||||
if (typeof provider.on === 'function') {
|
||||
provider.on('statusChange', this.onProviderStatusChange);
|
||||
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onProviderStatusChange(newStatus) {
|
||||
this.emit('statusChange', newStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onProviderPollQuestionChange(pollQuestion) {
|
||||
this.emit('pollQuestionChange', pollQuestion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./UserAPI').UserProvider} UserProvider
|
||||
*/
|
||||
/**
|
||||
* @typedef {import('./UserAPI').StatusUserProvider} StatusUserProvider
|
||||
*/
|
||||
/**
|
||||
* The PollQuestion type
|
||||
* @typedef {Object} PollQuestion
|
||||
* @property {String} question - The question to be presented to users
|
||||
* @property {Number} timestamp - The time that the poll question was set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Status type
|
||||
* @typedef {Object} Status
|
||||
* @property {String} key - A unique identifier for this status
|
||||
* @property {Number} label - A human readable label for this status
|
||||
*/
|
81
src/api/user/StatusUserProvider.js
Normal file
81
src/api/user/StatusUserProvider.js
Normal file
@ -0,0 +1,81 @@
|
||||
/*****************************************************************************
|
||||
* 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 UserProvider from "./UserProvider";
|
||||
|
||||
export default class StatusUserProvider extends UserProvider {
|
||||
/**
|
||||
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
|
||||
* @param {Function} callback a function to invoke when this event occurs
|
||||
*/
|
||||
on(event, callback) {}
|
||||
/**
|
||||
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
|
||||
* @param {Function} callback the callback function used to register the listener
|
||||
*/
|
||||
off(event, callback) {}
|
||||
/**
|
||||
* @returns {import("./StatusAPI").PollQuestion} the current status poll question
|
||||
*/
|
||||
async getPollQuestion() {}
|
||||
/**
|
||||
* @param {import("./StatusAPI").PollQuestion} pollQuestion a new poll question to set
|
||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false
|
||||
*/
|
||||
async setPollQuestion(pollQuestion) {}
|
||||
/**
|
||||
* @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false
|
||||
*/
|
||||
async canSetPollQuestion() {}
|
||||
/**
|
||||
* @returns {Promise<Array<import("./StatusAPI").Status>>} a list of the possible statuses that an operator can be in
|
||||
*/
|
||||
async getPossibleStatuses() {}
|
||||
/**
|
||||
* @param {import("./UserAPI").Role} role
|
||||
* @returns {Promise<import("./StatusAPI").Status}
|
||||
*/
|
||||
async getStatusForRole(role) {}
|
||||
/**
|
||||
* @param {import("./UserAPI").Role} role
|
||||
* @returns {Promise<import("./StatusAPI").Status}
|
||||
*/
|
||||
async getDefaultStatusForRole(role) {}
|
||||
/**
|
||||
* @param {import("./UserAPI").Role} role
|
||||
* @param {*} status
|
||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
||||
*/
|
||||
async setStatusForRole(role, status) {}
|
||||
/**
|
||||
* @param {import("./UserAPI").Role} role
|
||||
* @returns {Promise<Boolean} true if the user provider can provide status for the given role
|
||||
*/
|
||||
async canProvideStatusForRole(role) {}
|
||||
/**
|
||||
* @returns {Promise<Array<import("./UserAPI").Role>>} a list of all available status roles, if user permissions allow it.
|
||||
*/
|
||||
async getAllStatusRoles() {}
|
||||
/**
|
||||
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
|
||||
*/
|
||||
async getStatusRoleForCurrentUser() {}
|
||||
}
|
@ -25,16 +25,22 @@ import {
|
||||
MULTIPLE_PROVIDER_ERROR,
|
||||
NO_PROVIDER_ERROR
|
||||
} from './constants';
|
||||
import StatusAPI from './StatusAPI';
|
||||
import User from './User';
|
||||
|
||||
class UserAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
/**
|
||||
* @param {OpenMCT} openmct
|
||||
* @param {UserAPIConfiguration} config
|
||||
*/
|
||||
constructor(openmct, config) {
|
||||
super();
|
||||
|
||||
this._openmct = openmct;
|
||||
this._provider = undefined;
|
||||
|
||||
this.User = User;
|
||||
this.status = new StatusAPI(this, openmct, config);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,14 +53,17 @@ class UserAPI extends EventEmitter {
|
||||
*/
|
||||
setProvider(provider) {
|
||||
if (this.hasProvider()) {
|
||||
this._error(MULTIPLE_PROVIDER_ERROR);
|
||||
this.error(MULTIPLE_PROVIDER_ERROR);
|
||||
}
|
||||
|
||||
this._provider = provider;
|
||||
|
||||
this.emit('providerAdded', this._provider);
|
||||
}
|
||||
|
||||
getProvider() {
|
||||
return this._provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user provider has been set.
|
||||
*
|
||||
@ -74,7 +83,7 @@ class UserAPI extends EventEmitter {
|
||||
* @throws Will throw an error if no user provider is set
|
||||
*/
|
||||
getCurrentUser() {
|
||||
this._noProviderCheck();
|
||||
this.noProviderCheck();
|
||||
|
||||
return this._provider.getCurrentUser();
|
||||
}
|
||||
@ -105,7 +114,7 @@ class UserAPI extends EventEmitter {
|
||||
* @throws Will throw an error if no user provider is set
|
||||
*/
|
||||
hasRole(roleId) {
|
||||
this._noProviderCheck();
|
||||
this.noProviderCheck();
|
||||
|
||||
return this._provider.hasRole(roleId);
|
||||
}
|
||||
@ -116,9 +125,9 @@ class UserAPI extends EventEmitter {
|
||||
* @private
|
||||
* @throws Will throw an error if no user provider is set
|
||||
*/
|
||||
_noProviderCheck() {
|
||||
noProviderCheck() {
|
||||
if (!this.hasProvider()) {
|
||||
this._error(NO_PROVIDER_ERROR);
|
||||
this.error(NO_PROVIDER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,9 +138,26 @@ class UserAPI extends EventEmitter {
|
||||
* @param {string} error description of error
|
||||
* @throws Will throw error passed in
|
||||
*/
|
||||
_error(error) {
|
||||
error(error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserAPI;
|
||||
/**
|
||||
* @typedef {String} Role
|
||||
*/
|
||||
/**
|
||||
* @typedef {Object} OpenMCT
|
||||
*/
|
||||
/**
|
||||
* @typedef {{statusStyles: Object.<string, StatusStyleDefinition>}} UserAPIConfiguration
|
||||
*/
|
||||
/**
|
||||
* @typedef {Object} StatusStyleDefinition
|
||||
* @property {String} iconClass The icon class to apply to the status indicator when this status is active "icon-circle-slash",
|
||||
* @property {String} iconClassPoll The icon class to apply to the poll question indicator when this style is active eg. "icon-status-poll-question-mark"
|
||||
* @property {String} statusClass The class to apply to the indicator when this status is active eg. "s-status-error"
|
||||
* @property {String} statusBgColor The background color to apply in the status summary section of the poll question popup for this status eg."#9900cc"
|
||||
* @property {String} statusFgColor The foreground color to apply in the status summary section of the poll question popup for this status eg. "#fff"
|
||||
*/
|
||||
|
@ -40,11 +40,13 @@ describe("The User API", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
const activeOverlays = openmct.overlays.activeOverlays;
|
||||
activeOverlays.forEach(overlay => overlay.dismiss());
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe('with regard to user providers', () => {
|
||||
|
||||
it('allows you to specify a user provider', () => {
|
||||
openmct.user.on('providerAdded', (provider) => {
|
||||
expect(provider).toBeInstanceOf(ExampleUserProvider);
|
||||
|
36
src/api/user/UserProvider.js
Normal file
36
src/api/user/UserProvider.js
Normal file
@ -0,0 +1,36 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
export default class UserProvider {
|
||||
/**
|
||||
* @returns {Promise<User>} A promise that resolves with the currently logged in user
|
||||
*/
|
||||
getCurrentUser() {}
|
||||
/**
|
||||
* @returns {Boolean} true if a user is currently logged in, otherwise false
|
||||
*/
|
||||
isLoggedIn() {}
|
||||
/**
|
||||
* @param {String} role
|
||||
* @returns {Promise<Boolean>} true if the current user has the given role
|
||||
*/
|
||||
hasRole(role) {}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
@ -114,14 +113,12 @@ export default {
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.bounds = this.openmct.time.bounds();
|
||||
|
||||
this.limitEvaluator = this.openmct
|
||||
.telemetry
|
||||
.limitEvaluator(this.domainObject);
|
||||
|
||||
this.openmct.time.on('timeSystem', this.updateTimeSystem);
|
||||
this.openmct.time.on('bounds', this.updateBounds);
|
||||
|
||||
this.timestampKey = this.openmct.time.timeSystem().key;
|
||||
|
||||
@ -135,72 +132,41 @@ export default {
|
||||
|
||||
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
|
||||
|
||||
this.unsubscribe = this.openmct
|
||||
.telemetry
|
||||
.subscribe(this.domainObject, this.setLatestValues);
|
||||
|
||||
this.requestHistory();
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
});
|
||||
this.telemetryCollection.on('add', this.setLatestValues);
|
||||
this.telemetryCollection.on('clear', this.resetValues);
|
||||
this.telemetryCollection.load();
|
||||
|
||||
if (this.hasUnits) {
|
||||
this.setUnit();
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.unsubscribe();
|
||||
this.openmct.time.off('timeSystem', this.updateTimeSystem);
|
||||
this.openmct.time.off('bounds', this.updateBounds);
|
||||
this.telemetryCollection.off('add', this.setLatestValues);
|
||||
this.telemetryCollection.off('clear', this.resetValues);
|
||||
|
||||
this.telemetryCollection.destroy();
|
||||
},
|
||||
methods: {
|
||||
updateView() {
|
||||
if (!this.updatingView) {
|
||||
this.updatingView = true;
|
||||
requestAnimationFrame(() => {
|
||||
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
|
||||
|
||||
if (this.shouldUpdate(newTimestamp)) {
|
||||
this.timestamp = newTimestamp;
|
||||
this.datum = this.latestDatum;
|
||||
}
|
||||
|
||||
this.timestamp = this.getParsedTimestamp(this.latestDatum);
|
||||
this.datum = this.latestDatum;
|
||||
this.updatingView = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
setLatestValues(datum) {
|
||||
this.latestDatum = datum;
|
||||
|
||||
setLatestValues(data) {
|
||||
this.latestDatum = data[data.length - 1];
|
||||
this.updateView();
|
||||
},
|
||||
shouldUpdate(newTimestamp) {
|
||||
return this.inBounds(newTimestamp)
|
||||
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
|
||||
},
|
||||
requestHistory() {
|
||||
this.openmct
|
||||
.telemetry
|
||||
.request(this.domainObject, {
|
||||
start: this.bounds.start,
|
||||
end: this.bounds.end,
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
})
|
||||
.then((array) => this.setLatestValues(array[array.length - 1]))
|
||||
.catch((error) => {
|
||||
console.warn('Error fetching data', error);
|
||||
});
|
||||
},
|
||||
updateBounds(bounds, isTick) {
|
||||
this.bounds = bounds;
|
||||
if (!isTick) {
|
||||
this.resetValues();
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
inBounds(timestamp) {
|
||||
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
|
||||
},
|
||||
updateTimeSystem(timeSystem) {
|
||||
this.resetValues();
|
||||
this.timestampKey = timeSystem.key;
|
||||
},
|
||||
updateViewContext() {
|
||||
@ -241,4 +207,3 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -46,6 +46,7 @@ describe("The LAD Table", () => {
|
||||
|
||||
let openmct;
|
||||
let ladPlugin;
|
||||
let historicalProvider;
|
||||
let parent;
|
||||
let child;
|
||||
let telemetryCount = 3;
|
||||
@ -81,6 +82,13 @@ describe("The LAD Table", () => {
|
||||
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
|
||||
|
||||
historicalProvider = {
|
||||
request: () => {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
};
|
||||
spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
|
||||
|
||||
openmct.time.bounds({
|
||||
start: bounds.start,
|
||||
end: bounds.end
|
||||
@ -147,7 +155,7 @@ describe("The LAD Table", () => {
|
||||
// add another telemetry object as composition in lad table to test multi rows
|
||||
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(async (done) => {
|
||||
let telemetryRequestResolve;
|
||||
let telemetryObjectResolve;
|
||||
let anotherTelemetryObjectResolve;
|
||||
@ -166,11 +174,12 @@ describe("The LAD Table", () => {
|
||||
callBack();
|
||||
});
|
||||
|
||||
openmct.telemetry.request.and.callFake(() => {
|
||||
historicalProvider.request = () => {
|
||||
telemetryRequestResolve(mockTelemetry);
|
||||
|
||||
return telemetryRequestPromise;
|
||||
});
|
||||
};
|
||||
|
||||
openmct.objects.get.and.callFake((obj) => {
|
||||
if (obj.key === 'telemetry-object') {
|
||||
telemetryObjectResolve(mockObj.telemetry);
|
||||
@ -195,6 +204,8 @@ describe("The LAD Table", () => {
|
||||
|
||||
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
|
||||
await Vue.nextTick();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("should show one row per object in the composition", () => {
|
||||
|
@ -40,6 +40,14 @@ export default {
|
||||
BarGraph
|
||||
},
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
this.telemetryObjects = {};
|
||||
this.telemetryObjectFormats = {};
|
||||
@ -247,7 +255,7 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
const trace = {
|
||||
let trace = {
|
||||
key,
|
||||
name: telemetryObject.name,
|
||||
x: xValues,
|
||||
@ -255,13 +263,18 @@ export default {
|
||||
text: yValues.map(String),
|
||||
xAxisMetadata: axisMetadata.xAxisMetadata,
|
||||
yAxisMetadata: axisMetadata.yAxisMetadata,
|
||||
type: 'bar',
|
||||
type: this.options.type ? this.options.type : 'bar',
|
||||
marker: {
|
||||
color: this.domainObject.configuration.barStyles.series[key].color
|
||||
},
|
||||
hoverinfo: 'skip'
|
||||
};
|
||||
|
||||
if (this.options.type) {
|
||||
trace.mode = 'markers';
|
||||
trace.hoverinfo = 'x+y';
|
||||
}
|
||||
|
||||
this.addTrace(trace, key);
|
||||
},
|
||||
isDataInTimeRange(datum, key) {
|
57
src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js
Normal file
57
src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js
Normal file
@ -0,0 +1,57 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, 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 { SCATTER_PLOT_KEY } from './scatterPlotConstants';
|
||||
|
||||
export default function ScatterPlotCompositionPolicy(openmct) {
|
||||
function hasRange(metadata) {
|
||||
const rangeValues = metadata.valuesForHints(['range']).map((value) => {
|
||||
return value.source;
|
||||
});
|
||||
|
||||
const uniqueRangeValues = new Set(rangeValues);
|
||||
|
||||
return uniqueRangeValues && uniqueRangeValues.size > 1;
|
||||
}
|
||||
|
||||
function hasScatterPlotTelemetry(domainObject) {
|
||||
if (!openmct.telemetry.isTelemetryObject(domainObject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let metadata = openmct.telemetry.getMetadata(domainObject);
|
||||
|
||||
return metadata.values().length > 0 && hasRange(metadata);
|
||||
}
|
||||
|
||||
return {
|
||||
allow: function (parent, child) {
|
||||
if (parent.type === SCATTER_PLOT_KEY) {
|
||||
if ((child.type === 'conditionSet') || (!hasScatterPlotTelemetry(child))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
146
src/plugins/charts/scatter/ScatterPlotForm.vue
Normal file
146
src/plugins/charts/scatter/ScatterPlotForm.vue
Normal file
@ -0,0 +1,146 @@
|
||||
/*****************************************************************************
|
||||
* 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>
|
||||
<span class="form-control">
|
||||
<span
|
||||
class="field control"
|
||||
:class="model.cssClass"
|
||||
>
|
||||
<div
|
||||
class="c-form--sub-grid"
|
||||
>
|
||||
<div class="c-form__row">
|
||||
<span
|
||||
class="req-indicator"
|
||||
:class="{'req': isRequired}"
|
||||
>
|
||||
</span>
|
||||
<label>Minimum X axis value</label>
|
||||
<input
|
||||
ref="domainMin"
|
||||
v-model.number="domainMin"
|
||||
data-field-name="domainMin"
|
||||
type="number"
|
||||
@input="onChange('domainMin')"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="c-form__row">
|
||||
<span
|
||||
class="req-indicator"
|
||||
:class="{'req': isRequired}"
|
||||
>
|
||||
</span>
|
||||
<label>Maximum X axis value</label>
|
||||
<input
|
||||
ref="domainMax"
|
||||
v-model.number="domainMax"
|
||||
data-field-name="domainMax"
|
||||
type="number"
|
||||
@input="onChange('domainMax')"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="c-form__row">
|
||||
<span
|
||||
class="req-indicator"
|
||||
:class="{'req': isRequired}"
|
||||
>
|
||||
</span>
|
||||
<label>Minimum Y axis value</label>
|
||||
<input
|
||||
ref="rangeMin"
|
||||
v-model.number="rangeMin"
|
||||
data-field-name="rangeMin"
|
||||
type="number"
|
||||
@input="onChange('rangeMin')"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="c-form__row">
|
||||
<span
|
||||
class="req-indicator"
|
||||
:class="{'req': isRequired}"
|
||||
>
|
||||
</span>
|
||||
<label>Maximum Y axis value</label>
|
||||
<input
|
||||
ref="rangeMax"
|
||||
v-model.number="rangeMax"
|
||||
data-field-name="rangeMax"
|
||||
type="number"
|
||||
@input="onChange('rangeMax')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rangeMax: this.model.value.rangeMax,
|
||||
rangeMin: this.model.value.rangeMin,
|
||||
domainMax: this.model.value.domainMax,
|
||||
domainMin: this.model.value.domainMin
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isRequired() {
|
||||
return [this.rangeMax, this.rangeMin, this.domainMin, this.domainMax].some(value => value !== undefined && value !== '');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(property) {
|
||||
if (this[property] === '') {
|
||||
this[property] = undefined;
|
||||
}
|
||||
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: {
|
||||
rangeMax: this.rangeMax,
|
||||
rangeMin: this.rangeMin,
|
||||
domainMax: this.domainMax,
|
||||
domainMin: this.domainMin
|
||||
}
|
||||
};
|
||||
|
||||
if (property) {
|
||||
this.model.validate(data);
|
||||
}
|
||||
|
||||
this.$emit('onChange', data);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
346
src/plugins/charts/scatter/ScatterPlotView.vue
Normal file
346
src/plugins/charts/scatter/ScatterPlotView.vue
Normal file
@ -0,0 +1,346 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2021, 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>
|
||||
<ScatterPlotWithUnderlay
|
||||
class="c-plot c-scatter-chart-view"
|
||||
:data="trace"
|
||||
:plot-axis-title="plotAxisTitle"
|
||||
@subscribe="subscribeToAll"
|
||||
@unsubscribe="removeAllSubscriptions"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScatterPlotWithUnderlay from './ScatterPlotWithUnderlay.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ScatterPlotWithUnderlay
|
||||
},
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
data() {
|
||||
this.telemetryObjects = {};
|
||||
this.telemetryObjectFormats = {};
|
||||
this.valuesByTimestamp = {};
|
||||
this.subscriptions = [];
|
||||
|
||||
return {
|
||||
trace: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
plotAxisTitle() {
|
||||
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
|
||||
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
|
||||
const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';
|
||||
|
||||
return {
|
||||
xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,
|
||||
yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setTimeContext();
|
||||
this.loadComposition();
|
||||
this.reloadTelemetry = this.reloadTelemetry.bind(this);
|
||||
this.reloadTelemetry = _.debounce(this.reloadTelemetry, 500);
|
||||
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.reloadTelemetry);
|
||||
this.unobserveUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.reloadTelemetry);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
if (!this.composition) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeAllSubscriptions();
|
||||
|
||||
this.composition.off('add', this.addToComposition);
|
||||
this.composition.off('remove', this.removeTelemetryObject);
|
||||
if (this.unobserve) {
|
||||
this.unobserve();
|
||||
}
|
||||
|
||||
if (this.unobserveUnderlayRanges) {
|
||||
this.unobserveUnderlayRanges();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||
this.followTimeContext();
|
||||
|
||||
},
|
||||
followTimeContext() {
|
||||
this.timeContext.on('bounds', this.reloadTelemetry);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off('bounds', this.reloadTelemetry);
|
||||
}
|
||||
},
|
||||
addToComposition(telemetryObject) {
|
||||
if (Object.values(this.telemetryObjects).length > 0) {
|
||||
this.confirmRemoval(telemetryObject);
|
||||
} else {
|
||||
this.addTelemetryObject(telemetryObject);
|
||||
}
|
||||
},
|
||||
removeFromComposition(telemetryObject) {
|
||||
let composition = this.domainObject.composition.filter(id =>
|
||||
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
|
||||
);
|
||||
|
||||
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
|
||||
},
|
||||
addTelemetryObject(telemetryObject) {
|
||||
// grab information we need from the added telmetry object
|
||||
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
this.telemetryObjects[key] = telemetryObject;
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);
|
||||
this.getDataForTelemetry(key);
|
||||
},
|
||||
confirmRemoval(telemetryObject) {
|
||||
const dialog = this.openmct.overlays.dialog({
|
||||
iconClass: 'alert',
|
||||
message: 'This action will replace the current telemetry source. Do you want to continue?',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Ok',
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
const oldTelemetryObject = Object.values(this.telemetryObjects)[0];
|
||||
this.removeFromComposition(oldTelemetryObject);
|
||||
this.removeTelemetryObject(oldTelemetryObject.identifier);
|
||||
this.valuesByTimestamp = {};
|
||||
this.addTelemetryObject(telemetryObject);
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
callback: () => {
|
||||
this.removeFromComposition(telemetryObject);
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
getTelemetryProcessor(keyString) {
|
||||
return (telemetry) => {
|
||||
//Check that telemetry object has not been removed since telemetry was requested.
|
||||
const telemetryObject = this.telemetryObjects[keyString];
|
||||
if (!telemetryObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
telemetry.forEach(datum => {
|
||||
this.addDataToGraph(telemetryObject, datum);
|
||||
});
|
||||
this.updateTrace(telemetryObject);
|
||||
};
|
||||
},
|
||||
getAxisMetadata(telemetryObject) {
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
if (!metadata) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return metadata.valuesForHints(['range']);
|
||||
},
|
||||
loadComposition() {
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.composition.on('add', this.addToComposition);
|
||||
this.composition.on('remove', this.removeTelemetryObject);
|
||||
this.composition.load();
|
||||
},
|
||||
reloadTelemetry() {
|
||||
this.valuesByTimestamp = {};
|
||||
|
||||
Object.keys(this.telemetryObjects).forEach(key => {
|
||||
this.getDataForTelemetry(key);
|
||||
});
|
||||
},
|
||||
getDataForTelemetry(key) {
|
||||
const telemetryObject = this.telemetryObjects[key];
|
||||
if (!telemetryObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const telemetryProcessor = this.getTelemetryProcessor(key);
|
||||
const options = this.getOptions();
|
||||
this.openmct.telemetry.request(telemetryObject, options).then(telemetryProcessor);
|
||||
this.subscribeToObject(telemetryObject);
|
||||
},
|
||||
removeTelemetryObject(identifier) {
|
||||
const key = this.openmct.objects.makeKeyString(identifier);
|
||||
if (this.telemetryObjects[key]) {
|
||||
delete this.telemetryObjects[key];
|
||||
}
|
||||
|
||||
if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
|
||||
delete this.telemetryObjectFormats[key];
|
||||
}
|
||||
|
||||
this.removeSubscription(key);
|
||||
},
|
||||
addDataToGraph(telemetryObject, data) {
|
||||
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
|
||||
if (data.message) {
|
||||
this.openmct.notifications.alert(data.message);
|
||||
}
|
||||
|
||||
if (!this.domainObject.configuration.axes.xKey || !this.domainObject.configuration.axes.yKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamp = this.getTimestampForDatum(data, key, telemetryObject);
|
||||
let valueForTimestamp = this.valuesByTimestamp[timestamp] || {};
|
||||
|
||||
//populate x values
|
||||
let metadataKey = this.domainObject.configuration.axes.xKey;
|
||||
if (data[metadataKey] !== undefined) {
|
||||
valueForTimestamp.x = this.format(key, metadataKey, data);
|
||||
}
|
||||
|
||||
metadataKey = this.domainObject.configuration.axes.yKey;
|
||||
if (data[metadataKey] !== undefined) {
|
||||
valueForTimestamp.y = this.format(key, metadataKey, data);
|
||||
}
|
||||
|
||||
this.valuesByTimestamp[timestamp] = valueForTimestamp;
|
||||
},
|
||||
updateTrace(telemetryObject) {
|
||||
const xAndyValues = Object.values(this.valuesByTimestamp);
|
||||
const xValues = xAndyValues.map(value => value.x);
|
||||
const yValues = xAndyValues.map(value => value.y);
|
||||
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
||||
const xAxisMetadata = axisMetadata.find(metadata => metadata.source === this.domainObject.configuration.axes.xKey);
|
||||
let yAxisMetadata = {};
|
||||
if (this.domainObject.configuration.axes.yKey) {
|
||||
yAxisMetadata = axisMetadata.find(metadata => metadata.source === this.domainObject.configuration.axes.yKey);
|
||||
}
|
||||
|
||||
let trace = {
|
||||
key: this.openmct.objects.makeKeyString(this.domainObject.identifier),
|
||||
name: this.domainObject.name,
|
||||
x: xValues,
|
||||
y: yValues,
|
||||
text: yValues.map(String),
|
||||
xAxisMetadata: xAxisMetadata,
|
||||
yAxisMetadata: yAxisMetadata,
|
||||
type: 'scatter',
|
||||
mode: 'markers',
|
||||
marker: {
|
||||
color: this.domainObject.configuration.styles.color
|
||||
},
|
||||
hoverinfo: 'x+y'
|
||||
};
|
||||
|
||||
if (this.domainObject.configuration.ranges !== undefined && this.domainObject.configuration.ranges.domainMin !== undefined && this.domainObject.configuration.ranges.domainMax !== undefined) {
|
||||
trace.xaxis = {
|
||||
min: this.domainObject.configuration.ranges.domainMin,
|
||||
max: this.domainObject.configuration.ranges.domainMax
|
||||
};
|
||||
}
|
||||
|
||||
if (this.domainObject.configuration.ranges !== undefined && this.domainObject.configuration.ranges.rangeMin !== undefined && this.domainObject.configuration.ranges.rangeMax !== undefined) {
|
||||
trace.yaxis = {
|
||||
min: this.domainObject.configuration.ranges.rangeMin,
|
||||
max: this.domainObject.configuration.ranges.rangeMax
|
||||
};
|
||||
}
|
||||
|
||||
this.trace = [trace];
|
||||
},
|
||||
getTimestampForDatum(datum, key, telemetryObject) {
|
||||
const timeSystemKey = this.timeContext.timeSystem().key;
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
let metadataValue = metadata.value(timeSystemKey) || { format: timeSystemKey };
|
||||
|
||||
return this.parse(key, metadataValue.source, datum);
|
||||
},
|
||||
format(telemetryObjectKey, metadataKey, data) {
|
||||
const formats = this.telemetryObjectFormats[telemetryObjectKey];
|
||||
|
||||
return formats[metadataKey].format(data);
|
||||
},
|
||||
parse(telemetryObjectKey, metadataKey, datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formats = this.telemetryObjectFormats[telemetryObjectKey];
|
||||
|
||||
return formats[metadataKey].parse(datum);
|
||||
},
|
||||
getOptions() {
|
||||
const { start, end } = this.timeContext.bounds();
|
||||
|
||||
return {
|
||||
end,
|
||||
start
|
||||
};
|
||||
},
|
||||
subscribeToObject(telemetryObject) {
|
||||
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
|
||||
this.removeSubscription(key);
|
||||
|
||||
const options = this.getOptions();
|
||||
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
|
||||
data => this.addDataToGraph(telemetryObject, data)
|
||||
, options);
|
||||
|
||||
this.subscriptions.push({
|
||||
key,
|
||||
unsubscribe
|
||||
});
|
||||
},
|
||||
subscribeToAll() {
|
||||
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||
telemetryObjects.forEach(this.subscribeToObject);
|
||||
},
|
||||
removeAllSubscriptions() {
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
this.subscriptions = [];
|
||||
},
|
||||
removeSubscription(key) {
|
||||
const found = this.subscriptions.findIndex(subscription => subscription.key === key);
|
||||
if (found > -1) {
|
||||
this.subscriptions[found].unsubscribe();
|
||||
this.subscriptions.splice(found, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
79
src/plugins/charts/scatter/ScatterPlotViewProvider.js
Normal file
79
src/plugins/charts/scatter/ScatterPlotViewProvider.js
Normal file
@ -0,0 +1,79 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, 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 ScatterPlotView from './ScatterPlotView.vue';
|
||||
import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW, TIME_STRIP_KEY } from './scatterPlotConstants.js';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function ScatterPlotViewProvider(openmct) {
|
||||
function isCompactView(objectPath) {
|
||||
let isChildOfTimeStrip = objectPath.find(object => object.type === TIME_STRIP_KEY);
|
||||
|
||||
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
return {
|
||||
key: SCATTER_PLOT_VIEW,
|
||||
name: 'Scatter Plot',
|
||||
cssClass: 'icon-telemetry',
|
||||
canView(domainObject, objectPath) {
|
||||
return domainObject && domainObject.type === SCATTER_PLOT_KEY;
|
||||
},
|
||||
|
||||
canEdit(domainObject, objectPath) {
|
||||
return domainObject && domainObject.type === SCATTER_PLOT_KEY;
|
||||
},
|
||||
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
let isCompact = isCompactView(objectPath);
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
ScatterPlotView
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
path: objectPath
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
options: {
|
||||
compact: isCompact
|
||||
}
|
||||
};
|
||||
},
|
||||
template: '<scatter-plot-view :options="options"></scatter-plot-view>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
393
src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue
Normal file
393
src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue
Normal file
@ -0,0 +1,393 @@
|
||||
<template>
|
||||
<div
|
||||
ref="plotWrapper"
|
||||
class="has-local-controls"
|
||||
:class="{ 's-unsynced' : isZoomed }"
|
||||
>
|
||||
<div
|
||||
v-if="isZoomed"
|
||||
class="l-state-indicators"
|
||||
>
|
||||
<span
|
||||
class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
|
||||
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
|
||||
></span>
|
||||
</div>
|
||||
<div
|
||||
ref="plot"
|
||||
class="c-scatter-chart"
|
||||
></div>
|
||||
<div
|
||||
ref="localControl"
|
||||
class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
|
||||
>
|
||||
<button
|
||||
v-if="data.length"
|
||||
class="c-button icon-reset"
|
||||
:disabled="!isZoomed"
|
||||
title="Reset pan/zoom"
|
||||
@click="reset()"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Plotly from 'plotly-basic';
|
||||
|
||||
const MULTI_AXES_X_PADDING_PERCENT = {
|
||||
LEFT: 8,
|
||||
RIGHT: 94
|
||||
};
|
||||
|
||||
import { getValidatedData } from "@/plugins/plan/util";
|
||||
|
||||
const PATH_COLORS = ['blue', 'red', 'green'];
|
||||
const MARKER_COLOR = 'white';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
plotAxisTitle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isZoomed: false,
|
||||
yAxisRange: {
|
||||
min: '',
|
||||
max: ''
|
||||
},
|
||||
xAxisRange: {
|
||||
min: '',
|
||||
max: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
immediate: false,
|
||||
handler: 'updateData'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getUnderlayPlotData();
|
||||
|
||||
Plotly.newPlot(this.$refs.plot, Array.from(this.data.concat(this.getShapes(this.shapesData))), this.getLayout(), {
|
||||
responsive: true,
|
||||
displayModeBar: false
|
||||
});
|
||||
this.registerListeners();
|
||||
|
||||
this.$refs.plot.on('plotly_relayout', this.zoom);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$refs.plot && this.$refs.plot.off) {
|
||||
this.$refs.plot.off('plotly_relayout', this.zoom);
|
||||
}
|
||||
|
||||
if (this.plotResizeObserver) {
|
||||
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
||||
clearTimeout(this.resizeTimer);
|
||||
}
|
||||
|
||||
if (this.unlistenUnderlay) {
|
||||
this.unlistenUnderlay();
|
||||
}
|
||||
|
||||
if (this.unlistenUnderlayRanges) {
|
||||
this.unlistenUnderlayRanges();
|
||||
}
|
||||
|
||||
if (this.unobserveColorChanges) {
|
||||
this.unobserveColorChanges();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getUnderlayPlotData() {
|
||||
if (this.domainObject.selectFile) {
|
||||
this.shapesData = getValidatedData(this.domainObject);
|
||||
} else {
|
||||
this.shapesData = [];
|
||||
}
|
||||
},
|
||||
observeForUnderlayPlotChanges() {
|
||||
this.getUnderlayPlotData();
|
||||
this.updateData();
|
||||
},
|
||||
getAxisMinMax() {
|
||||
if (!this.data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, use x and y axes min, max values only if an underlay is available
|
||||
if (this.shapesData.length && this.data[0].xaxis) {
|
||||
this.xAxisRange = this.data[0].xaxis;
|
||||
}
|
||||
|
||||
if (this.shapesData.length && this.data[0].yaxis) {
|
||||
this.yAxisRange = this.data[0].yaxis;
|
||||
}
|
||||
},
|
||||
getLayout() {
|
||||
this.getAxisMinMax();
|
||||
|
||||
const yAxesMeta = this.getYAxisMeta();
|
||||
const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);
|
||||
const xAxisDomain = this.getXAxisDomain(yAxesMeta);
|
||||
|
||||
const shapes = this.shapesData.map((shapeData, index) => {
|
||||
if (!shapeData.x || !shapeData.y
|
||||
|| !shapeData.x.length || !shapeData.y.length
|
||||
|| shapeData.x.length !== shapeData.y.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let path = `M ${shapeData.x[0]},${shapeData.y[0]}`;
|
||||
shapeData.x.forEach((point, shapeIndex) => {
|
||||
if (shapeIndex > 0) {
|
||||
path = `${path} L${point},${shapeData.y[shapeIndex]}`;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
path,
|
||||
type: 'path',
|
||||
line: {
|
||||
color: PATH_COLORS[index]
|
||||
},
|
||||
opacity: 0.5
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
autosize: true,
|
||||
showlegend: false,
|
||||
textposition: 'auto',
|
||||
font: {
|
||||
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
|
||||
size: '12px',
|
||||
color: '#666'
|
||||
},
|
||||
xaxis: {
|
||||
domain: xAxisDomain,
|
||||
range: [this.xAxisRange.min, this.xAxisRange.max],
|
||||
title: this.plotAxisTitle.xAxisTitle,
|
||||
automargin: true
|
||||
},
|
||||
yaxis: primaryYaxis,
|
||||
margin: {
|
||||
l: 5,
|
||||
r: 5,
|
||||
t: 5,
|
||||
b: 0
|
||||
},
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
shapes,
|
||||
layer: 'below'
|
||||
};
|
||||
},
|
||||
getYAxisMeta() {
|
||||
const yAxisMeta = {};
|
||||
|
||||
this.data.forEach(datum => {
|
||||
const yAxisMetadata = datum.yAxisMetadata;
|
||||
const range = '1';
|
||||
const side = 'left';
|
||||
const name = yAxisMetadata.name;
|
||||
const unit = yAxisMetadata.units;
|
||||
|
||||
yAxisMeta[range] = {
|
||||
range,
|
||||
side,
|
||||
name,
|
||||
unit
|
||||
};
|
||||
});
|
||||
|
||||
return yAxisMeta;
|
||||
},
|
||||
getXAxisDomain(yAxisMeta) {
|
||||
let leftPaddingPerc = 0;
|
||||
let rightPaddingPerc = 100;
|
||||
let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right'));
|
||||
let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left'));
|
||||
if (yAxisMeta && rightSide.length > 1) {
|
||||
rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;
|
||||
}
|
||||
|
||||
if (yAxisMeta && leftSide.length > 1) {
|
||||
leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;
|
||||
}
|
||||
|
||||
return [leftPaddingPerc / 100, rightPaddingPerc / 100];
|
||||
},
|
||||
getYaxisLayout(yAxisMeta) {
|
||||
if (!yAxisMeta) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { name, range, side = 'left', unit } = yAxisMeta;
|
||||
const title = `${name} ${unit ? '(' + unit + ')' : ''}`;
|
||||
const yaxis = {
|
||||
automargin: true,
|
||||
title
|
||||
};
|
||||
|
||||
let yRange = this.yAxisRange;
|
||||
if (range === '1') {
|
||||
yaxis.range = [yRange.min, yRange.max];
|
||||
|
||||
return yaxis;
|
||||
}
|
||||
|
||||
yaxis.range = [yRange.min, yRange.max];
|
||||
yaxis.anchor = side.toLowerCase() === 'left'
|
||||
? 'free'
|
||||
: 'x';
|
||||
yaxis.showline = side.toLowerCase() === 'left';
|
||||
yaxis.side = side.toLowerCase();
|
||||
yaxis.overlaying = 'y';
|
||||
yaxis.position = 0.01;
|
||||
|
||||
return yaxis;
|
||||
},
|
||||
registerListeners() {
|
||||
this.unobserveColorChanges = this.openmct.objects.observe(this.domainObject, 'configuration.styles.color', this.updateColors);
|
||||
this.unlistenUnderlay = this.openmct.objects.observe(this.domainObject, 'selectFile', this.observeForUnderlayPlotChanges);
|
||||
this.unlistenUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.updateData);
|
||||
this.resizeTimer = false;
|
||||
if (window.ResizeObserver) {
|
||||
this.plotResizeObserver = new ResizeObserver(() => {
|
||||
// debounce and trigger window resize so that plotly can resize the plot
|
||||
clearTimeout(this.resizeTimer);
|
||||
this.resizeTimer = setTimeout(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}, 250);
|
||||
});
|
||||
this.plotResizeObserver.observe(this.$refs.plotWrapper);
|
||||
}
|
||||
},
|
||||
updateColors() {
|
||||
const colors = [];
|
||||
const indices = [];
|
||||
this.data.forEach((item, index) => {
|
||||
const colorExists = this.domainObject.configuration.styles.color;
|
||||
indices.push(index);
|
||||
if (colorExists) {
|
||||
colors.push(this.domainObject.configuration.styles.color);
|
||||
} else {
|
||||
colors.push(item.marker.color);
|
||||
}
|
||||
});
|
||||
const plotUpdate = {
|
||||
'marker.color': colors
|
||||
};
|
||||
|
||||
Plotly.restyle(this.$refs.plot, plotUpdate, indices);
|
||||
},
|
||||
reset() {
|
||||
this.isZoomed = false;
|
||||
|
||||
this.updatePlot();
|
||||
this.$emit('subscribe');
|
||||
},
|
||||
updateData() {
|
||||
this.updatePlot();
|
||||
},
|
||||
updateLocalControlPosition() {
|
||||
const localControl = this.$refs.localControl;
|
||||
localControl.style.display = 'none';
|
||||
|
||||
const plot = this.$refs.plot;
|
||||
const bgLayer = this.$el.querySelector('.bglayer');
|
||||
|
||||
const plotBoundingRect = plot.getBoundingClientRect();
|
||||
const bgLayerBoundingRect = bgLayer.getBoundingClientRect();
|
||||
|
||||
const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5;
|
||||
const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5;
|
||||
|
||||
localControl.style.top = `${top}px`;
|
||||
localControl.style.left = `${left}px`;
|
||||
localControl.style.display = 'block';
|
||||
},
|
||||
updatePlot() {
|
||||
if (!this.$refs || !this.$refs.plot || this.isZoomed) {
|
||||
return;
|
||||
}
|
||||
|
||||
Plotly.react(this.$refs.plot, Array.from(this.data.concat(this.getShapes(this.shapesData))), this.getLayout());
|
||||
},
|
||||
zoom(eventData) {
|
||||
const autorange = eventData['xaxis.autorange'];
|
||||
const { autosize } = eventData;
|
||||
|
||||
if (autosize || autorange) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isZoomed = true;
|
||||
this.$emit('unsubscribe');
|
||||
},
|
||||
getShapes() {
|
||||
let markerData = {
|
||||
x: [],
|
||||
y: []
|
||||
};
|
||||
const shapes = this.shapesData.map((shapeData, index) => {
|
||||
if (!shapeData.x || !shapeData.y
|
||||
|| !shapeData.x.length || !shapeData.y.length
|
||||
|| shapeData.x.length !== shapeData.y.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let text = [];
|
||||
shapeData.x.forEach((point) => {
|
||||
text.push(`${parseFloat(point).toPrecision(2)}`);
|
||||
});
|
||||
|
||||
markerData.x = markerData.x.concat(shapeData.x);
|
||||
markerData.y = markerData.y.concat(shapeData.y);
|
||||
|
||||
return {
|
||||
x: shapeData.x,
|
||||
y: shapeData.y,
|
||||
mode: 'text',
|
||||
text,
|
||||
textfont: {
|
||||
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
|
||||
size: '12px',
|
||||
color: PATH_COLORS[index]
|
||||
},
|
||||
opacity: 0.5
|
||||
};
|
||||
});
|
||||
|
||||
shapes.push({
|
||||
x: markerData.x,
|
||||
y: markerData.y,
|
||||
mode: "markers",
|
||||
marker: {
|
||||
size: 6,
|
||||
color: MARKER_COLOR
|
||||
}
|
||||
});
|
||||
|
||||
return shapes;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
64
src/plugins/charts/scatter/inspector/PlotOptions.vue
Normal file
64
src/plugins/charts/scatter/inspector/PlotOptions.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<!--
|
||||
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>
|
||||
<div v-if="canEdit">
|
||||
<plot-options-edit />
|
||||
</div>
|
||||
<div v-else>
|
||||
<plot-options-browse />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PlotOptionsBrowse from "./PlotOptionsBrowse.vue";
|
||||
import PlotOptionsEdit from "./PlotOptionsEdit.vue";
|
||||
export default {
|
||||
components: {
|
||||
PlotOptionsBrowse,
|
||||
PlotOptionsEdit
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: this.openmct.editor.isEditing()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canEdit() {
|
||||
return this.isEditing && !this.domainObject.locked;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
},
|
||||
methods: {
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
153
src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue
Normal file
153
src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue
Normal file
@ -0,0 +1,153 @@
|
||||
<!--
|
||||
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="js-plot-options-browse grid-properties">
|
||||
<ul class="l-inspector-part">
|
||||
<h2 title="Object view settings">Settings</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="X axis selection"
|
||||
>X Axis</div>
|
||||
<div class="grid-cell value">{{ xKeyLabel }}</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Y axis selection"
|
||||
>Y Axis</div>
|
||||
<div class="grid-cell value">{{ yKeyLabel }}</div>
|
||||
</li>
|
||||
<ColorSwatch
|
||||
:current-color="currentColor"
|
||||
edit-title="Manually set the color for this plot"
|
||||
view-title="The marker color for this plot"
|
||||
short-label="Color"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorSwatch from "../../../../ui/color/ColorSwatch.vue";
|
||||
import Color from "../../../../ui/color/Color";
|
||||
import ColorPalette from "../../../../ui/color/ColorPalette";
|
||||
|
||||
export default {
|
||||
components: { ColorSwatch },
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
xKeyLabel: '',
|
||||
yKeyLabel: '',
|
||||
currentColor: undefined
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.plotSeries = [];
|
||||
this.colorPalette = new ColorPalette();
|
||||
this.initColor();
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.registerListeners();
|
||||
this.composition.load();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
initColor() {
|
||||
// this is called before the plot is initialized
|
||||
if (!this.domainObject.configuration.styles || !this.domainObject.configuration.styles.color) {
|
||||
const color = this.colorPalette.getNextColor().asHexString();
|
||||
this.domainObject.configuration.styles = {
|
||||
color
|
||||
};
|
||||
}
|
||||
|
||||
this.currentColor = this.domainObject.configuration.styles.color;
|
||||
const colorObject = Color.fromHexString(this.currentColor);
|
||||
|
||||
this.colorPalette.remove(colorObject);
|
||||
},
|
||||
registerListeners() {
|
||||
this.composition.on('add', this.addSeries);
|
||||
this.composition.on('remove', this.removeSeries);
|
||||
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setAxesLabels);
|
||||
},
|
||||
stopListening() {
|
||||
this.composition.off('add', this.addSeries);
|
||||
this.composition.off('remove', this.removeSeries);
|
||||
if (this.unobserve) {
|
||||
this.unobserve();
|
||||
}
|
||||
},
|
||||
addSeries(series, index) {
|
||||
this.$set(this.plotSeries, this.plotSeries.length, series);
|
||||
this.setAxesLabels();
|
||||
},
|
||||
removeSeries(series) {
|
||||
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier));
|
||||
if (index !== undefined) {
|
||||
this.$delete(this.plotSeries, index);
|
||||
this.setAxesLabels();
|
||||
}
|
||||
},
|
||||
setAxesLabels() {
|
||||
let xKeyOptions = [];
|
||||
let yKeyOptions = [];
|
||||
if (this.plotSeries.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const series = this.plotSeries[0];
|
||||
const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']);
|
||||
|
||||
metadataValues.forEach((metadataValue) => {
|
||||
xKeyOptions.push({
|
||||
name: metadataValue.name || metadataValue.key,
|
||||
value: metadataValue.source || metadataValue.key
|
||||
});
|
||||
yKeyOptions.push({
|
||||
name: metadataValue.name || metadataValue.key,
|
||||
value: metadataValue.source || metadataValue.key
|
||||
});
|
||||
});
|
||||
let xKeyOptionIndex;
|
||||
let yKeyOptionIndex;
|
||||
|
||||
if (this.domainObject.configuration.axes.xKey) {
|
||||
xKeyOptionIndex = xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
|
||||
if (xKeyOptionIndex > -1) {
|
||||
this.xKeyLabel = xKeyOptions[xKeyOptionIndex].name;
|
||||
}
|
||||
}
|
||||
|
||||
if (metadataValues.length > 1 && this.domainObject.configuration.axes.yKey) {
|
||||
yKeyOptionIndex = yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
|
||||
if (yKeyOptionIndex > -1) {
|
||||
this.yKeyLabel = yKeyOptions[yKeyOptionIndex].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
262
src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue
Normal file
262
src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue
Normal file
@ -0,0 +1,262 @@
|
||||
<!--
|
||||
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="js-plot-options-edit grid-properties">
|
||||
<ul class="l-inspector-part">
|
||||
<h2 title="Object view settings">Settings</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="X axis selection."
|
||||
>X Axis</div>
|
||||
<div class="grid-cell value">
|
||||
<select
|
||||
v-model="xKey"
|
||||
@change="updateForm('xKey')"
|
||||
>
|
||||
<option
|
||||
v-for="option in xKeyOptions"
|
||||
:key="`xKey-${option.value}`"
|
||||
:value="option.value"
|
||||
:selected="option.value == xKey"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Y axis selection."
|
||||
>Y Axis</div>
|
||||
<div class="grid-cell value">
|
||||
<select
|
||||
v-model="yKey"
|
||||
@change="updateForm('yKey')"
|
||||
>
|
||||
<option
|
||||
v-for="option in yKeyOptions"
|
||||
:key="`yKey-${option.value}`"
|
||||
:value="option.value"
|
||||
:selected="option.value == yKey"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</li>
|
||||
<ColorSwatch
|
||||
:current-color="currentColor"
|
||||
title="Manually set the line and marker color for this plot."
|
||||
edit-title="Manually set the line and marker color for this plot."
|
||||
view-title="The line and marker color for this plot."
|
||||
short-label="Color"
|
||||
@colorSet="setColor"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Color from "../../../../ui/color/Color";
|
||||
import ColorPalette from "../../../../ui/color/ColorPalette";
|
||||
import ColorSwatch from "../../../../ui/color/ColorSwatch.vue";
|
||||
|
||||
export default {
|
||||
components: { ColorSwatch },
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
xKey: undefined,
|
||||
yKey: undefined,
|
||||
xKeyOptions: [],
|
||||
yKeyOptions: [],
|
||||
currentColor: undefined
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.plotSeries = [];
|
||||
this.colorPalette = new ColorPalette();
|
||||
this.initColor();
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.registerListeners();
|
||||
this.composition.load();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
initColor() {
|
||||
// this is called before the plot is initialized
|
||||
if (!this.domainObject.configuration.styles || !this.domainObject.configuration.styles.color) {
|
||||
const color = this.colorPalette.getNextColor().asHexString();
|
||||
this.domainObject.configuration.styles = {
|
||||
color
|
||||
};
|
||||
}
|
||||
|
||||
this.currentColor = this.domainObject.configuration.styles.color;
|
||||
const colorObject = Color.fromHexString(this.currentColor);
|
||||
|
||||
this.colorPalette.remove(colorObject);
|
||||
},
|
||||
setColor(chosenColor) {
|
||||
this.currentColor = chosenColor.asHexString();
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
`configuration.styles.color`,
|
||||
this.currentColor
|
||||
);
|
||||
},
|
||||
registerListeners() {
|
||||
this.composition.on('add', this.addSeries);
|
||||
this.composition.on('remove', this.removeSeries);
|
||||
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setupOptions);
|
||||
},
|
||||
stopListening() {
|
||||
this.composition.off('add', this.addSeries);
|
||||
this.composition.off('remove', this.removeSeries);
|
||||
if (this.unobserve) {
|
||||
this.unobserve();
|
||||
}
|
||||
},
|
||||
addSeries(series, index) {
|
||||
this.$set(this.plotSeries, this.plotSeries.length, series);
|
||||
this.setupOptions();
|
||||
},
|
||||
removeSeries(seriesIdentifier) {
|
||||
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier));
|
||||
if (index >= 0) {
|
||||
this.$delete(this.plotSeries, index);
|
||||
this.setupOptions();
|
||||
}
|
||||
},
|
||||
setupOptions() {
|
||||
this.xKeyOptions = [];
|
||||
this.yKeyOptions = [];
|
||||
if (this.plotSeries.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let update = false;
|
||||
const series = this.plotSeries[0];
|
||||
const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']);
|
||||
metadataValues.forEach((metadataValue) => {
|
||||
this.xKeyOptions.push({
|
||||
name: metadataValue.name || metadataValue.key,
|
||||
value: metadataValue.source || metadataValue.key
|
||||
});
|
||||
this.yKeyOptions.push({
|
||||
name: metadataValue.name || metadataValue.key,
|
||||
value: metadataValue.source || metadataValue.key
|
||||
});
|
||||
});
|
||||
|
||||
let xKeyOptionIndex;
|
||||
let yKeyOptionIndex;
|
||||
|
||||
if (this.domainObject.configuration.axes.xKey) {
|
||||
xKeyOptionIndex = this.xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
|
||||
if (xKeyOptionIndex > -1) {
|
||||
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
|
||||
} else {
|
||||
this.xKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.xKey === undefined) {
|
||||
update = true;
|
||||
xKeyOptionIndex = 0;
|
||||
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
|
||||
}
|
||||
|
||||
if (metadataValues.length > 1) {
|
||||
if (this.domainObject.configuration.axes.yKey) {
|
||||
yKeyOptionIndex = this.yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
|
||||
if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) {
|
||||
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
|
||||
} else {
|
||||
this.yKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.yKey === undefined) {
|
||||
update = true;
|
||||
yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex);
|
||||
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
|
||||
}
|
||||
|
||||
this.yKeyOptions = this.yKeyOptions.map((option, index) => {
|
||||
if (index === xKeyOptionIndex) {
|
||||
option.name = `${option.name} (swap)`;
|
||||
option.swap = yKeyOptionIndex;
|
||||
} else {
|
||||
option.name = option.name.replace(' (swap)', '');
|
||||
option.swap = undefined;
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
}
|
||||
|
||||
this.xKeyOptions = this.xKeyOptions.map((option, index) => {
|
||||
if (index === yKeyOptionIndex) {
|
||||
option.name = `${option.name} (swap)`;
|
||||
option.swap = xKeyOptionIndex;
|
||||
} else {
|
||||
option.name = option.name.replace(' (swap)', '');
|
||||
option.swap = undefined;
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
|
||||
if (update === true) {
|
||||
this.saveConfiguration();
|
||||
}
|
||||
},
|
||||
updateForm(property) {
|
||||
if (property === 'xKey') {
|
||||
const xKeyOption = this.xKeyOptions.find(option => option.value === this.xKey);
|
||||
if (xKeyOption.swap !== undefined) {
|
||||
//swap
|
||||
this.yKey = this.xKeyOptions[xKeyOption.swap].value;
|
||||
}
|
||||
} else if (property === 'yKey') {
|
||||
const yKeyOption = this.yKeyOptions.find(option => option.value === this.yKey);
|
||||
if (yKeyOption.swap !== undefined) {
|
||||
//swap
|
||||
this.xKey = this.yKeyOptions[yKeyOption.swap].value;
|
||||
}
|
||||
}
|
||||
|
||||
this.saveConfiguration();
|
||||
},
|
||||
saveConfiguration() {
|
||||
this.openmct.objects.mutate(this.domainObject, `configuration.axes`, {
|
||||
xKey: this.xKey,
|
||||
yKey: this.yKey
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,48 @@
|
||||
import { SCATTER_PLOT_INSPECTOR_KEY, SCATTER_PLOT_KEY } from '../scatterPlotConstants';
|
||||
import Vue from 'vue';
|
||||
import PlotOptions from "./PlotOptions.vue";
|
||||
|
||||
export default function ScatterPlotInspectorViewProvider(openmct) {
|
||||
return {
|
||||
key: SCATTER_PLOT_INSPECTOR_KEY,
|
||||
name: 'Bar Graph Inspector View',
|
||||
canView: function (selection) {
|
||||
if (selection.length === 0 || selection[0].length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let object = selection[0][0].context.item;
|
||||
|
||||
return object
|
||||
&& object.type === SCATTER_PLOT_KEY;
|
||||
},
|
||||
view: function (selection) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
PlotOptions
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject: selection[0][0].context.item
|
||||
},
|
||||
template: '<plot-options></plot-options>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
if (component) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
127
src/plugins/charts/scatter/plugin.js
Normal file
127
src/plugins/charts/scatter/plugin.js
Normal file
@ -0,0 +1,127 @@
|
||||
/*****************************************************************************
|
||||
* 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 { SCATTER_PLOT_KEY } from './scatterPlotConstants.js';
|
||||
import ScatterPlotViewProvider from './ScatterPlotViewProvider';
|
||||
import ScatterPlotInspectorViewProvider from './inspector/ScatterPlotInspectorViewProvider';
|
||||
import ScatterPlotCompositionPolicy from './ScatterPlotCompositionPolicy';
|
||||
import Vue from "vue";
|
||||
import ScatterPlotForm from "./ScatterPlotForm.vue";
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.forms.addNewFormControl('scatter-plot-form-control', getScatterPlotFormControl(openmct));
|
||||
|
||||
openmct.types.addType(SCATTER_PLOT_KEY, {
|
||||
key: SCATTER_PLOT_KEY,
|
||||
name: "Scatter Plot",
|
||||
cssClass: "icon-plot-scatter",
|
||||
description: "View data as a scatter plot.",
|
||||
creatable: true,
|
||||
initialize: function (domainObject) {
|
||||
domainObject.composition = [];
|
||||
domainObject.configuration = {
|
||||
styles: {},
|
||||
axes: {},
|
||||
ranges: {}
|
||||
};
|
||||
},
|
||||
form: [
|
||||
{
|
||||
name: 'Underlay data (JSON file)',
|
||||
key: 'selectFile',
|
||||
control: 'file-input',
|
||||
text: 'Select File...',
|
||||
type: 'application/json',
|
||||
removable: true,
|
||||
hideFromInspector: true,
|
||||
property: [
|
||||
"selectFile"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Underlay ranges",
|
||||
control: "scatter-plot-form-control",
|
||||
cssClass: "l-input",
|
||||
key: "scatterPlotForm",
|
||||
required: false,
|
||||
hideFromInspector: false,
|
||||
property: [
|
||||
"configuration",
|
||||
"ranges"
|
||||
],
|
||||
validate: ({ value }, callback) => {
|
||||
const { rangeMin, rangeMax, domainMin, domainMax } = value;
|
||||
const valid = {
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
domainMin,
|
||||
domainMax
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
callback(valid);
|
||||
}
|
||||
|
||||
const values = Object.values(valid);
|
||||
const hasAllValues = values.every(rangeValue => rangeValue !== undefined);
|
||||
const hasNoValues = values.every(rangeValue => rangeValue === undefined);
|
||||
|
||||
return hasAllValues || hasNoValues;
|
||||
}
|
||||
}
|
||||
],
|
||||
priority: 891
|
||||
});
|
||||
|
||||
openmct.objectViews.addProvider(new ScatterPlotViewProvider(openmct));
|
||||
|
||||
openmct.inspectorViews.addProvider(new ScatterPlotInspectorViewProvider(openmct));
|
||||
|
||||
openmct.composition.addPolicy(new ScatterPlotCompositionPolicy(openmct).allow);
|
||||
};
|
||||
|
||||
function getScatterPlotFormControl(openmct) {
|
||||
return {
|
||||
show(element, model, onChange) {
|
||||
const rowComponent = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
ScatterPlotForm
|
||||
},
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
model,
|
||||
onChange
|
||||
};
|
||||
},
|
||||
template: `<scatter-plot-form :model="model" @onChange="onChange"></scatter-plot-form>`
|
||||
});
|
||||
|
||||
return rowComponent;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
421
src/plugins/charts/scatter/pluginSpec.js
Normal file
421
src/plugins/charts/scatter/pluginSpec.js
Normal file
@ -0,0 +1,421 @@
|
||||
/*****************************************************************************
|
||||
* 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 {createOpenMct, resetApplicationState} from "utils/testing";
|
||||
import Vue from "vue";
|
||||
import ScatterPlotPlugin from "./plugin";
|
||||
import ScatterPlot from './ScatterPlotView.vue';
|
||||
import EventEmitter from "EventEmitter";
|
||||
import { SCATTER_PLOT_VIEW, SCATTER_PLOT_KEY } from './scatterPlotConstants';
|
||||
|
||||
describe("the plugin", function () {
|
||||
let element;
|
||||
let child;
|
||||
let openmct;
|
||||
let telemetryPromise;
|
||||
let telemetryPromiseResolve;
|
||||
let mockObjectPath;
|
||||
|
||||
beforeEach((done) => {
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock folder',
|
||||
type: 'fake-folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
const testTelemetry = [
|
||||
{
|
||||
'utc': 1,
|
||||
'some-key': 'some-value 1',
|
||||
'some-other-key': 'some-other-value 1'
|
||||
},
|
||||
{
|
||||
'utc': 2,
|
||||
'some-key': 'some-value 2',
|
||||
'some-other-key': 'some-other-value 2'
|
||||
},
|
||||
{
|
||||
'utc': 3,
|
||||
'some-key': 'some-value 3',
|
||||
'some-other-key': 'some-other-value 3'
|
||||
}
|
||||
];
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
telemetryPromise = new Promise((resolve) => {
|
||||
telemetryPromiseResolve = resolve;
|
||||
});
|
||||
|
||||
spyOn(openmct.telemetry, 'request').and.callFake(() => {
|
||||
telemetryPromiseResolve(testTelemetry);
|
||||
|
||||
return telemetryPromise;
|
||||
});
|
||||
|
||||
openmct.install(new ScatterPlotPlugin());
|
||||
|
||||
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.time.timeSystem("utc", {
|
||||
start: 0,
|
||||
end: 4
|
||||
});
|
||||
|
||||
openmct.types.addType("test-object", {
|
||||
creatable: true
|
||||
});
|
||||
|
||||
openmct.on("start", done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
openmct.time.timeSystem('utc', {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
resetApplicationState(openmct).then(done).catch(done);
|
||||
});
|
||||
|
||||
describe("The scatter plot view", () => {
|
||||
let testDomainObject;
|
||||
let scatterPlotObject;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let component;
|
||||
let mockComposition;
|
||||
|
||||
beforeEach(async () => {
|
||||
scatterPlotObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-plot"
|
||||
},
|
||||
type: "telemetry.plot.scatter-plot",
|
||||
name: "Test Scatter Plot",
|
||||
configuration: {
|
||||
axes: {},
|
||||
styles: {}
|
||||
}
|
||||
};
|
||||
|
||||
testDomainObject = {
|
||||
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
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
mockComposition = new EventEmitter();
|
||||
mockComposition.load = () => {
|
||||
mockComposition.emit('add', testDomainObject);
|
||||
|
||||
return [testDomainObject];
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
||||
|
||||
let viewContainer = document.createElement("div");
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
el: viewContainer,
|
||||
components: {
|
||||
ScatterPlot
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: scatterPlotObject,
|
||||
composition: openmct.composition.get(scatterPlotObject)
|
||||
},
|
||||
template: "<ScatterPlot></ScatterPlot>"
|
||||
});
|
||||
|
||||
await Vue.nextTick();
|
||||
});
|
||||
|
||||
it("provides a scatter plot view", () => {
|
||||
const applicableViews = openmct.objectViews.get(scatterPlotObject, mockObjectPath);
|
||||
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === SCATTER_PLOT_VIEW);
|
||||
expect(plotViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it("Renders plotly scatter plot", () => {
|
||||
let scatterPlotElement = element.querySelectorAll(".plotly");
|
||||
expect(scatterPlotElement.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the scatter plot objects", () => {
|
||||
const mockObject = {
|
||||
name: 'A very nice scatter plot',
|
||||
key: SCATTER_PLOT_KEY,
|
||||
creatable: true
|
||||
};
|
||||
|
||||
it('defines a scatter plot object type with the correct key', () => {
|
||||
const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;
|
||||
expect(objectDef.key).toEqual(mockObject.key);
|
||||
});
|
||||
|
||||
it('is creatable', () => {
|
||||
const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;
|
||||
expect(objectDef.creatable).toEqual(mockObject.creatable);
|
||||
});
|
||||
});
|
||||
|
||||
describe("The scatter plot composition policy", () => {
|
||||
it("allows composition for telemetry that contain at least 2 ranges", () => {
|
||||
const parent = {
|
||||
"composition": [],
|
||||
"configuration": {
|
||||
axes: {},
|
||||
styles: {}
|
||||
},
|
||||
"name": "Some Scatter Plot",
|
||||
"type": "telemetry.plot.scatter-plot",
|
||||
"location": "mine",
|
||||
"modified": 1631005183584,
|
||||
"persisted": 1631005183502,
|
||||
"identifier": {
|
||||
"namespace": "",
|
||||
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
|
||||
}
|
||||
};
|
||||
const testTelemetryObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-object"
|
||||
},
|
||||
type: "test-object",
|
||||
name: "Test Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "some-key",
|
||||
name: "Some attribute",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-other-key",
|
||||
name: "Another attribute",
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-other-key2",
|
||||
name: "Another attribute2",
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
const composition = openmct.composition.get(parent);
|
||||
expect(() => {
|
||||
composition.add(testTelemetryObject);
|
||||
}).not.toThrow();
|
||||
expect(parent.composition.length).toBe(1);
|
||||
});
|
||||
|
||||
it("disallows composition for telemetry that don't contain at least 2 range hints", () => {
|
||||
const parent = {
|
||||
"composition": [],
|
||||
"configuration": {
|
||||
axes: {},
|
||||
styles: {}
|
||||
},
|
||||
"name": "Some Scatter Plot",
|
||||
"type": "telemetry.plot.scatter-plot",
|
||||
"location": "mine",
|
||||
"modified": 1631005183584,
|
||||
"persisted": 1631005183502,
|
||||
"identifier": {
|
||||
"namespace": "",
|
||||
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
|
||||
}
|
||||
};
|
||||
const testTelemetryObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-object"
|
||||
},
|
||||
type: "test-object",
|
||||
name: "Test Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "some-key",
|
||||
name: "Some attribute",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-other-key",
|
||||
name: "Another attribute",
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
const composition = openmct.composition.get(parent);
|
||||
expect(() => {
|
||||
composition.add(testTelemetryObject);
|
||||
}).toThrow();
|
||||
expect(parent.composition.length).toBe(0);
|
||||
});
|
||||
});
|
||||
describe('the inspector view', () => {
|
||||
let mockComposition;
|
||||
let testDomainObject;
|
||||
let selection;
|
||||
let plotInspectorView;
|
||||
let viewContainer;
|
||||
let optionsElement;
|
||||
beforeEach(async () => {
|
||||
testDomainObject = {
|
||||
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
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
selection = [
|
||||
[
|
||||
{
|
||||
context: {
|
||||
item: {
|
||||
id: "test-object",
|
||||
identifier: {
|
||||
key: "test-object",
|
||||
namespace: ''
|
||||
},
|
||||
type: "telemetry.plot.scatter-plot",
|
||||
configuration: {
|
||||
axes: {},
|
||||
styles: {
|
||||
}
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
key: '~Some~foo.scatter'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
mockComposition = new EventEmitter();
|
||||
mockComposition.load = () => {
|
||||
mockComposition.emit('add', testDomainObject);
|
||||
|
||||
return [testDomainObject];
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
||||
|
||||
viewContainer = document.createElement('div');
|
||||
child.append(viewContainer);
|
||||
|
||||
const applicableViews = openmct.inspectorViews.get(selection);
|
||||
plotInspectorView = applicableViews[0];
|
||||
plotInspectorView.show(viewContainer);
|
||||
|
||||
await Vue.nextTick();
|
||||
optionsElement = element.querySelector('.c-scatter-plot-options');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
plotInspectorView.destroy();
|
||||
});
|
||||
|
||||
it('it renders the options', () => {
|
||||
expect(optionsElement).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
4
src/plugins/charts/scatter/scatterPlotConstants.js
Normal file
4
src/plugins/charts/scatter/scatterPlotConstants.js
Normal file
@ -0,0 +1,4 @@
|
||||
export const SCATTER_PLOT_VIEW = 'scatter-plot.view';
|
||||
export const SCATTER_PLOT_KEY = 'telemetry.plot.scatter-plot';
|
||||
export const SCATTER_PLOT_INSPECTOR_KEY = 'telemetry.plot.scatter-plot.inspector';
|
||||
export const TIME_STRIP_KEY = 'time-strip';
|
@ -27,7 +27,7 @@
|
||||
:href="url"
|
||||
>
|
||||
<div class="c-condition-widget__label">
|
||||
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
|
||||
{{ label }}
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
@ -39,28 +39,112 @@ export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data: function () {
|
||||
return {
|
||||
internalDomainObject: this.domainObject
|
||||
conditionalLabel: '',
|
||||
conditionSetIdentifier: null,
|
||||
domainObjectLabel: '',
|
||||
url: null,
|
||||
urlDefined: false,
|
||||
useConditionSetOutputAsLabel: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
urlDefined() {
|
||||
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
|
||||
},
|
||||
url() {
|
||||
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
|
||||
label() {
|
||||
return this.useConditionSetOutputAsLabel
|
||||
? this.conditionalLabel
|
||||
: this.domainObjectLabel
|
||||
;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
conditionSetIdentifier: {
|
||||
handler(newValue, oldValue) {
|
||||
if (!oldValue || !newValue || !this.openmct.objects.areIdsEqual(newValue, oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenToConditionSetChanges();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
|
||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
|
||||
|
||||
if (this.domainObject) {
|
||||
this.updateDomainObject(this.domainObject);
|
||||
this.listenToConditionSetChanges();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.conditionSetIdentifier = null;
|
||||
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
this.stopListeningToConditionSetChanges();
|
||||
},
|
||||
methods: {
|
||||
updateInternalDomainObject(domainObject) {
|
||||
this.internalDomainObject = domainObject;
|
||||
async listenToConditionSetChanges() {
|
||||
if (!this.conditionSetIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conditionSetDomainObject = await this.openmct.objects.get(this.conditionSetIdentifier);
|
||||
this.stopListeningToConditionSetChanges();
|
||||
|
||||
if (!conditionSetDomainObject) {
|
||||
this.openmct.notifications.alert('Unable to find condition set');
|
||||
}
|
||||
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(conditionSetDomainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
});
|
||||
|
||||
this.telemetryCollection.on('add', this.updateConditionLabel, this);
|
||||
this.telemetryCollection.load();
|
||||
},
|
||||
stopListeningToConditionSetChanges() {
|
||||
if (this.telemetryCollection) {
|
||||
this.telemetryCollection.off('add', this.updateConditionLabel, this);
|
||||
this.telemetryCollection.destroy();
|
||||
this.telemetryCollection = null;
|
||||
}
|
||||
},
|
||||
updateConditionLabel([latestDatum]) {
|
||||
if (!this.conditionSetIdentifier) {
|
||||
this.stopListeningToConditionSetChanges();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.conditionalLabel = latestDatum.output || '';
|
||||
},
|
||||
updateDomainObject(domainObject) {
|
||||
if (this.domainObjectLabel !== domainObject.label) {
|
||||
this.domainObjectLabel = domainObject.label;
|
||||
}
|
||||
|
||||
const urlDefined = domainObject.url && domainObject.url.length > 0;
|
||||
if (this.urlDefined !== urlDefined) {
|
||||
this.urlDefined = urlDefined;
|
||||
}
|
||||
|
||||
const url = this.urlDefined ? sanitizeUrl(domainObject.url) : null;
|
||||
if (this.url !== url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
const conditionSetIdentifier = domainObject.configuration.objectStyles.conditionSetIdentifier;
|
||||
if (this.conditionSetIdentifier !== conditionSetIdentifier) {
|
||||
this.conditionSetIdentifier = conditionSetIdentifier;
|
||||
}
|
||||
|
||||
const useConditionSetOutputAsLabel = this.conditionSetIdentifier && domainObject.configuration.useConditionSetOutputAsLabel;
|
||||
if (this.useConditionSetOutputAsLabel !== useConditionSetOutputAsLabel) {
|
||||
this.useConditionSetOutputAsLabel = useConditionSetOutputAsLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -91,7 +91,7 @@ export default {
|
||||
width: DEFAULT_TELEMETRY_DIMENSIONS[0],
|
||||
height: DEFAULT_TELEMETRY_DIMENSIONS[1],
|
||||
displayMode: 'all',
|
||||
value: metadata.getDefaultDisplayValue(),
|
||||
value: metadata.getDefaultDisplayValue()?.key,
|
||||
stroke: "",
|
||||
fill: "",
|
||||
color: "",
|
||||
@ -222,20 +222,20 @@ export default {
|
||||
.then(this.setObject);
|
||||
}
|
||||
|
||||
this.openmct.time.on("bounds", this.refreshData);
|
||||
|
||||
this.status = this.openmct.status.get(this.item.identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.removeSubscription();
|
||||
this.removeStatusListener();
|
||||
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
this.openmct.time.off("bounds", this.refreshData);
|
||||
this.telemetryCollection.off('add', this.setLatestValues);
|
||||
this.telemetryCollection.off('clear', this.refreshData);
|
||||
|
||||
this.telemetryCollection.destroy();
|
||||
|
||||
if (this.mutablePromise) {
|
||||
this.mutablePromise.then(() => {
|
||||
@ -253,34 +253,9 @@ export default {
|
||||
|
||||
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`;
|
||||
},
|
||||
requestHistoricalData() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
let options = {
|
||||
start: bounds.start,
|
||||
end: bounds.end,
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
};
|
||||
this.openmct.telemetry.request(this.domainObject, options)
|
||||
.then(data => {
|
||||
if (data.length > 0) {
|
||||
this.latestDatum = data[data.length - 1];
|
||||
this.updateView();
|
||||
}
|
||||
});
|
||||
},
|
||||
subscribeToObject() {
|
||||
this.subscription = this.openmct.telemetry.subscribe(this.domainObject, function (datum) {
|
||||
const key = this.openmct.time.timeSystem().key;
|
||||
const datumTimeStamp = datum[key];
|
||||
if (this.openmct.time.clock() !== undefined
|
||||
|| (datumTimeStamp
|
||||
&& (this.openmct.time.bounds().end >= datumTimeStamp))
|
||||
) {
|
||||
this.latestDatum = datum;
|
||||
this.updateView();
|
||||
}
|
||||
}.bind(this));
|
||||
setLatestValues(data) {
|
||||
this.latestDatum = data[data.length - 1];
|
||||
this.updateView();
|
||||
},
|
||||
updateView() {
|
||||
if (!this.updatingView) {
|
||||
@ -291,17 +266,10 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
removeSubscription() {
|
||||
if (this.subscription) {
|
||||
this.subscription();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
},
|
||||
refreshData(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.latestDatum = undefined;
|
||||
this.updateView();
|
||||
this.requestHistoricalData(this.domainObject);
|
||||
}
|
||||
},
|
||||
setObject(domainObject) {
|
||||
@ -315,8 +283,13 @@ export default {
|
||||
const valueMetadata = this.metadata.value(this.item.value);
|
||||
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
|
||||
|
||||
this.requestHistoricalData();
|
||||
this.subscribeToObject();
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
});
|
||||
this.telemetryCollection.on('add', this.setLatestValues);
|
||||
this.telemetryCollection.on('clear', this.refreshData);
|
||||
this.telemetryCollection.load();
|
||||
|
||||
this.currentObjectPath = this.objectPath.slice();
|
||||
this.currentObjectPath.unshift(this.domainObject);
|
||||
|
128
src/plugins/formActions/CreateActionSpec.js
Normal file
128
src/plugins/formActions/CreateActionSpec.js
Normal file
@ -0,0 +1,128 @@
|
||||
/*****************************************************************************
|
||||
* 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 CreateAction from './CreateAction';
|
||||
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
let parentObject;
|
||||
let parentObjectPath;
|
||||
let unObserve;
|
||||
|
||||
describe("The create action plugin", () => {
|
||||
let openmct;
|
||||
|
||||
const TYPES = [
|
||||
'clock',
|
||||
'conditionWidget',
|
||||
'conditionWidget',
|
||||
'example.imagery',
|
||||
'example.state-generator',
|
||||
'flexible-layout',
|
||||
'folder',
|
||||
'generator',
|
||||
'hyperlink',
|
||||
'LadTable',
|
||||
'LadTableSet',
|
||||
'layout',
|
||||
'mmgis',
|
||||
'notebook',
|
||||
'plan',
|
||||
'table',
|
||||
'tabs',
|
||||
'telemetry-mean',
|
||||
'telemetry.plot.bar-graph',
|
||||
'telemetry.plot.overlay',
|
||||
'telemetry.plot.stacked',
|
||||
'time-strip',
|
||||
'timer',
|
||||
'webpage'
|
||||
];
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe('creates new objects for a', () => {
|
||||
beforeEach(() => {
|
||||
parentObject = {
|
||||
name: 'mock folder',
|
||||
type: 'folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
},
|
||||
composition: []
|
||||
};
|
||||
parentObjectPath = [parentObject];
|
||||
|
||||
spyOn(openmct.objects, 'save');
|
||||
openmct.objects.save.and.callThrough();
|
||||
spyOn(openmct.forms, 'showForm');
|
||||
openmct.forms.showForm.and.callFake(formStructure => {
|
||||
return Promise.resolve({
|
||||
name: 'test',
|
||||
notes: 'test notes',
|
||||
location: parentObjectPath
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
parentObject = null;
|
||||
unObserve();
|
||||
});
|
||||
|
||||
TYPES.forEach(type => {
|
||||
it(`type ${type}`, (done) => {
|
||||
function callback(newObject) {
|
||||
const composition = newObject.composition;
|
||||
|
||||
openmct.objects.get(composition[0])
|
||||
.then(object => {
|
||||
expect(object.type).toEqual(type);
|
||||
expect(object.location).toEqual(openmct.objects.makeKeyString(parentObject.identifier));
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
const deBouncedCallback = debounce(callback, 300);
|
||||
unObserve = openmct.objects.observe(parentObject, '*', deBouncedCallback);
|
||||
|
||||
const createAction = new CreateAction(openmct, type, parentObject);
|
||||
createAction.invoke();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -45,7 +45,7 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
this._showEditForm(objectPath);
|
||||
return this._showEditForm(objectPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -86,7 +86,7 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
const formStructure = createWizard.getFormStructure(false);
|
||||
formStructure.title = 'Edit ' + this.domainObject.name;
|
||||
|
||||
this.openmct.forms.showForm(formStructure)
|
||||
return this.openmct.forms.showForm(formStructure)
|
||||
.then(this._onSave.bind(this));
|
||||
}
|
||||
}
|
||||
|
222
src/plugins/formActions/pluginSpec.js
Normal file
222
src/plugins/formActions/pluginSpec.js
Normal file
@ -0,0 +1,222 @@
|
||||
/*****************************************************************************
|
||||
* 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
|
||||
} from 'utils/testing';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
describe('EditPropertiesAction plugin', () => {
|
||||
let editPropertiesAction;
|
||||
let openmct;
|
||||
let element;
|
||||
|
||||
beforeEach((done) => {
|
||||
element = document.createElement('div');
|
||||
element.style.display = 'block';
|
||||
element.style.width = '1920px';
|
||||
element.style.height = '1080px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless(element);
|
||||
|
||||
editPropertiesAction = openmct.actions.getAction('properties');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
editPropertiesAction = null;
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('editPropertiesAction exists', () => {
|
||||
expect(editPropertiesAction.key).toEqual('properties');
|
||||
});
|
||||
|
||||
it('edit properties action applies to only persistable objects', () => {
|
||||
spyOn(openmct.objects, 'isPersistable').and.returnValue(true);
|
||||
|
||||
const domainObject = {
|
||||
name: 'mock folder',
|
||||
type: 'folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
},
|
||||
composition: []
|
||||
};
|
||||
const isApplicableTo = editPropertiesAction.appliesTo([domainObject]);
|
||||
expect(isApplicableTo).toBe(true);
|
||||
});
|
||||
|
||||
it('edit properties action does not apply to non persistable objects', () => {
|
||||
spyOn(openmct.objects, 'isPersistable').and.returnValue(false);
|
||||
|
||||
const domainObject = {
|
||||
name: 'mock folder',
|
||||
type: 'folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
},
|
||||
composition: []
|
||||
};
|
||||
const isApplicableTo = editPropertiesAction.appliesTo([domainObject]);
|
||||
expect(isApplicableTo).toBe(false);
|
||||
});
|
||||
|
||||
it('edit properties action when invoked shows form', (done) => {
|
||||
const domainObject = {
|
||||
name: 'mock folder',
|
||||
notes: 'mock notes',
|
||||
type: 'folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
},
|
||||
modified: 1643065068597,
|
||||
persisted: 1643065068600,
|
||||
composition: []
|
||||
};
|
||||
|
||||
const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
|
||||
openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
|
||||
|
||||
function handleFormPropertyChange(data) {
|
||||
const form = document.querySelector('.js-form');
|
||||
const title = form.querySelector('input');
|
||||
expect(title.value).toEqual(domainObject.name);
|
||||
|
||||
const notes = form.querySelector('textArea');
|
||||
expect(notes.value).toEqual(domainObject.notes);
|
||||
|
||||
const buttons = form.querySelectorAll('button');
|
||||
expect(buttons[0].textContent.trim()).toEqual('OK');
|
||||
expect(buttons[1].textContent.trim()).toEqual('Cancel');
|
||||
|
||||
const clickEvent = createMouseEvent('click');
|
||||
buttons[1].dispatchEvent(clickEvent);
|
||||
|
||||
openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
|
||||
}
|
||||
|
||||
editPropertiesAction.invoke([domainObject])
|
||||
.catch(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('edit properties action saves changes', (done) => {
|
||||
const oldName = 'mock folder';
|
||||
const newName = 'renamed mock folder';
|
||||
const domainObject = {
|
||||
name: oldName,
|
||||
notes: 'mock notes',
|
||||
type: 'folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
},
|
||||
modified: 1643065068597,
|
||||
persisted: 1643065068600,
|
||||
composition: []
|
||||
};
|
||||
let unObserve;
|
||||
|
||||
function callback(newObject) {
|
||||
expect(newObject.name).not.toEqual(oldName);
|
||||
expect(newObject.name).toEqual(newName);
|
||||
|
||||
unObserve();
|
||||
done();
|
||||
}
|
||||
|
||||
const deBouncedCallback = debounce(callback, 300);
|
||||
unObserve = openmct.objects.observe(domainObject, '*', deBouncedCallback);
|
||||
|
||||
let changed = false;
|
||||
const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
|
||||
openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
|
||||
|
||||
function handleFormPropertyChange(data) {
|
||||
const form = document.querySelector('.js-form');
|
||||
const title = form.querySelector('input');
|
||||
const notes = form.querySelector('textArea');
|
||||
|
||||
const buttons = form.querySelectorAll('button');
|
||||
expect(buttons[0].textContent.trim()).toEqual('OK');
|
||||
expect(buttons[1].textContent.trim()).toEqual('Cancel');
|
||||
|
||||
if (!changed) {
|
||||
expect(title.value).toEqual(domainObject.name);
|
||||
expect(notes.value).toEqual(domainObject.notes);
|
||||
|
||||
// change input field value and dispatch event for it
|
||||
title.focus();
|
||||
title.value = newName;
|
||||
title.dispatchEvent(new Event('input'));
|
||||
title.blur();
|
||||
|
||||
changed = true;
|
||||
} else {
|
||||
// click ok to save form changes
|
||||
const clickEvent = createMouseEvent('click');
|
||||
buttons[0].dispatchEvent(clickEvent);
|
||||
|
||||
openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
|
||||
}
|
||||
}
|
||||
|
||||
editPropertiesAction.invoke([domainObject]);
|
||||
});
|
||||
|
||||
it('edit properties action discards changes', (done) => {
|
||||
const name = 'mock folder';
|
||||
const domainObject = {
|
||||
name,
|
||||
notes: 'mock notes',
|
||||
type: 'folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
},
|
||||
modified: 1643065068597,
|
||||
persisted: 1643065068600,
|
||||
composition: []
|
||||
};
|
||||
|
||||
editPropertiesAction.invoke([domainObject])
|
||||
.catch(() => {
|
||||
expect(domainObject.name).toEqual(name);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
const form = document.querySelector('.js-form');
|
||||
const buttons = form.querySelectorAll('button');
|
||||
const clickEvent = createMouseEvent('click');
|
||||
buttons[1].dispatchEvent(clickEvent);
|
||||
});
|
||||
});
|
@ -53,6 +53,8 @@ describe('Gauge plugin', () => {
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
|
||||
openmct.install(openmct.plugins.Gauge());
|
||||
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
@ -190,28 +192,27 @@ describe('Gauge plugin', () => {
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
|
||||
const curveElement = gaugeHolder.querySelector('.c-gauge__curval');
|
||||
const dialElement = gaugeHolder.querySelector('.c-dial');
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
|
||||
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correct min max values', () => {
|
||||
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${minValue} ${maxValue}`);
|
||||
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
|
||||
});
|
||||
|
||||
it('renders correct current value', (done) => {
|
||||
function WatchUpdateValue() {
|
||||
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text');
|
||||
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
|
||||
done();
|
||||
}
|
||||
@ -326,28 +327,27 @@ describe('Gauge plugin', () => {
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
|
||||
const curveElement = gaugeHolder.querySelector('.c-gauge__curval');
|
||||
const dialElement = gaugeHolder.querySelector('.c-dial');
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
|
||||
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correct min max values', () => {
|
||||
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${minValue} ${maxValue}`);
|
||||
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
|
||||
});
|
||||
|
||||
it('renders correct current value', (done) => {
|
||||
function WatchUpdateValue() {
|
||||
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text');
|
||||
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
|
||||
done();
|
||||
}
|
||||
@ -462,28 +462,27 @@ describe('Gauge plugin', () => {
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
|
||||
const curveElement = gaugeHolder.querySelector('.c-meter');
|
||||
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
|
||||
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correct min max values', () => {
|
||||
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${maxValue} ${minValue}`);
|
||||
expect(gaugeHolder.querySelector('.js-gauge-meter-range').textContent).toEqual(`${maxValue} ${minValue}`);
|
||||
});
|
||||
|
||||
it('renders correct current value', (done) => {
|
||||
function WatchUpdateValue() {
|
||||
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text');
|
||||
const textElement = gaugeHolder.querySelector('.js-meter-current-value');
|
||||
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
|
||||
done();
|
||||
}
|
||||
@ -560,17 +559,16 @@ describe('Gauge plugin', () => {
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
|
||||
const curveElement = gaugeHolder.querySelector('.c-meter');
|
||||
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
|
||||
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
@ -643,17 +641,16 @@ describe('Gauge plugin', () => {
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
|
||||
const curveElement = gaugeHolder.querySelector('.c-meter');
|
||||
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
@ -772,28 +769,27 @@ describe('Gauge plugin', () => {
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
|
||||
const curveElement = gaugeHolder.querySelector('.c-gauge__curval');
|
||||
const dialElement = gaugeHolder.querySelector('.c-dial');
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
|
||||
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correct min max values', () => {
|
||||
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${gaugeViewObject.configuration.gaugeController.min} ${gaugeViewObject.configuration.gaugeController.max}`);
|
||||
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${gaugeViewObject.configuration.gaugeController.min} ${gaugeViewObject.configuration.gaugeController.max}`);
|
||||
});
|
||||
|
||||
it('renders correct current value', (done) => {
|
||||
function WatchUpdateValue() {
|
||||
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text');
|
||||
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
|
||||
done();
|
||||
}
|
||||
|
@ -32,7 +32,9 @@ export default function GaugeViewProvider(openmct) {
|
||||
return domainObject.type === 'gauge';
|
||||
},
|
||||
canEdit: function (domainObject) {
|
||||
return false;
|
||||
if (domainObject.type === 'gauge') {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
@ -21,168 +21,259 @@
|
||||
*****************************************************************************/
|
||||
<template>
|
||||
<div
|
||||
class="c-gauge"
|
||||
class="c-gauge__wrapper js-gauge-wrapper"
|
||||
:class="`c-gauge--${gaugeType}`"
|
||||
>
|
||||
<div class="c-gauge__wrapper">
|
||||
<template v-if="typeDial">
|
||||
<svg
|
||||
class="c-gauge__range"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<text
|
||||
v-if="displayMinMax"
|
||||
font-size="35"
|
||||
transform="translate(105 455) rotate(-45)"
|
||||
>{{ rangeLow }}</text>
|
||||
<text
|
||||
v-if="displayMinMax"
|
||||
font-size="35"
|
||||
transform="translate(407 455) rotate(45)"
|
||||
text-anchor="end"
|
||||
>{{ rangeHigh }}</text>
|
||||
</svg>
|
||||
<template v-if="typeDial">
|
||||
<svg
|
||||
width="0"
|
||||
height="0"
|
||||
class="c-dial__clip-paths"
|
||||
>
|
||||
<defs>
|
||||
<clipPath
|
||||
id="gaugeBgMask"
|
||||
clipPathUnits="objectBoundingBox"
|
||||
>
|
||||
<path d="M0.853553 0.853553C0.944036 0.763071 1 0.638071 1 0.5C1 0.223858 0.776142 0 0.5 0C0.223858 0 0 0.223858 0 0.5C0 0.638071 0.0559644 0.763071 0.146447 0.853553L0.285934 0.714066C0.23115 0.659281 0.197266 0.583598 0.197266 0.5C0.197266 0.332804 0.332804 0.197266 0.5 0.197266C0.667196 0.197266 0.802734 0.332804 0.802734 0.5C0.802734 0.583598 0.76885 0.659281 0.714066 0.714066L0.853553 0.853553Z" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
id="gaugeValueMask"
|
||||
clipPathUnits="objectBoundingBox"
|
||||
>
|
||||
<path d="M0.18926 0.81074C0.109735 0.731215 0.0605469 0.621351 0.0605469 0.5C0.0605469 0.257298 0.257298 0.0605469 0.5 0.0605469C0.742702 0.0605469 0.939453 0.257298 0.939453 0.5C0.939453 0.621351 0.890265 0.731215 0.81074 0.81074L0.714066 0.714066C0.76885 0.659281 0.802734 0.583599 0.802734 0.5C0.802734 0.332804 0.667196 0.197266 0.5 0.197266C0.332804 0.197266 0.197266 0.332804 0.197266 0.5C0.197266 0.583599 0.23115 0.659281 0.285934 0.714066L0.18926 0.81074Z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
class="c-dial__range c-gauge__range js-gauge-dial-range"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<text
|
||||
v-if="displayMinMax"
|
||||
font-size="35"
|
||||
transform="translate(105 455) rotate(-45)"
|
||||
>{{ rangeLow }}</text>
|
||||
<text
|
||||
v-if="displayMinMax"
|
||||
font-size="35"
|
||||
transform="translate(407 455) rotate(45)"
|
||||
text-anchor="end"
|
||||
>{{ rangeHigh }}</text>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
class="c-dial__current-value-text-wrapper"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<svg
|
||||
v-if="displayCurVal"
|
||||
class="c-gauge__curval"
|
||||
class="c-dial__current-value-text-sizer"
|
||||
:viewBox="curValViewBox"
|
||||
>
|
||||
<text
|
||||
class="c-gauge__curval-text"
|
||||
class="c-dial__current-value-text js-dial-current-value"
|
||||
lengthAdjust="spacing"
|
||||
text-anchor="middle"
|
||||
style="transform: translate(50%, 70%)"
|
||||
>{{ curVal }}</text>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
<div class="c-dial">
|
||||
<svg
|
||||
class="c-dial__bg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M256,0C114.6,0,0,114.6,0,256S114.6,512,256,512,512,397.4,512,256,397.4,0,256,0Zm0,412A156,156,0,1,1,412,256,155.9,155.9,0,0,1,256,412Z" />
|
||||
</svg>
|
||||
<svg
|
||||
class="c-dial__bg"
|
||||
viewBox="0 0 10 10"
|
||||
>
|
||||
|
||||
<svg
|
||||
v-if="limitHigh && dialHighLimitDeg < 270"
|
||||
class="c-dial__limit-high"
|
||||
viewBox="0 0 512 512"
|
||||
:class="{
|
||||
'c-high-limit-clip--90': dialHighLimitDeg > 90,
|
||||
'c-high-limit-clip--180': dialHighLimitDeg >= 180
|
||||
}"
|
||||
>
|
||||
<path
|
||||
d="M100,256A156,156,0,1,1,366.3,366.3L437,437a255.2,255.2,0,0,0,75-181C512,114.6,397.4,0,256,0S0,114.6,0,256A255.2,255.2,0,0,0,75,437l70.7-70.7A155.5,155.5,0,0,1,100,256Z"
|
||||
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
|
||||
/>
|
||||
</svg>
|
||||
<g
|
||||
v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')"
|
||||
class="c-dial__limit-low"
|
||||
:style="`transform: rotate(${dialLowLimitDeg}deg)`"
|
||||
>
|
||||
<rect
|
||||
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q1')"
|
||||
class="c-dial__low-limit__low"
|
||||
x="5"
|
||||
y="5"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q2')"
|
||||
class="c-dial__low-limit__mid"
|
||||
x="5"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q3')"
|
||||
class="c-dial__low-limit__high"
|
||||
x="0"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<svg
|
||||
v-if="limitLow && dialLowLimitDeg < 270"
|
||||
class="c-dial__limit-low"
|
||||
viewBox="0 0 512 512"
|
||||
:class="{
|
||||
'c-dial-clip--90': dialLowLimitDeg < 90,
|
||||
'c-dial-clip--180': dialLowLimitDeg >= 90 && dialLowLimitDeg < 180
|
||||
}"
|
||||
>
|
||||
<path
|
||||
d="M256,100c86.2,0,156,69.8,156,156s-69.8,156-156,156c-43.1,0-82.1-17.5-110.3-45.7L75,437 c46.3,46.3,110.3,75,181,75c141.4,0,256-114.6,256-256S397.4,0,256,0C185.3,0,121.3,28.7,75,75l70.7,70.7 C173.9,117.5,212.9,100,256,100z"
|
||||
:style="`transform: rotate(${dialLowLimitDeg}deg)`"
|
||||
/>
|
||||
</svg>
|
||||
<g
|
||||
v-if="limitHigh !== null && dialHighLimitDeg < getLimitDegree('high', 'max')"
|
||||
class="c-dial__limit-high"
|
||||
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
|
||||
>
|
||||
<rect
|
||||
v-if="dialHighLimitDeg <= getLimitDegree('high', 'max')"
|
||||
class="c-dial__high-limit__low"
|
||||
x="0"
|
||||
y="5"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q2')"
|
||||
class="c-dial__high-limit__mid"
|
||||
x="0"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q3')"
|
||||
class="c-dial__high-limit__high"
|
||||
x="5"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
class="c-dial__value"
|
||||
viewBox="0 0 512 512"
|
||||
:class="{
|
||||
'c-dial-clip--90': degValue < 90 && typeFilledDial,
|
||||
'c-dial-clip--180': degValue >= 90 && degValue < 180 && typeFilledDial
|
||||
}"
|
||||
>
|
||||
<path
|
||||
v-if="typeFilledDial && degValue > 0"
|
||||
d="M256,31A224.3,224.3,0,0,0,98.3,95.5l48.4,49.2a156,156,0,1,1-1,221.6L96.9,415.1A224.4,224.4,0,0,0,256,481c124.3,0,225-100.7,225-225S380.3,31,256,31Z"
|
||||
:style="`transform: rotate(${degValue}deg)`"
|
||||
/>
|
||||
<path
|
||||
v-if="typeNeedleDial && valueInBounds"
|
||||
d="M256,86c-93.9,0-170,76.1-170,170c0,43.9,16.6,83.9,43.9,114.1l-38.7,38.7c-3.3,3.3-3.3,8.7,0,12s8.7,3.3,12,0 l38.7-38.7C172.1,409.4,212.1,426,256,426c93.9,0,170-76.1,170-170S349.9,86,256,86z M256,411.7c-86,0-155.7-69.7-155.7-155.7 S170,100.3,256,100.3S411.7,170,411.7,256S342,411.7,256,411.7z"
|
||||
:style="`transform: rotate(${degValue}deg)`"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-if="typeFilledDial"
|
||||
class="c-dial__filled-value-wrapper"
|
||||
viewBox="0 0 10 10"
|
||||
>
|
||||
<g
|
||||
class="c-dial__filled-value"
|
||||
:style="`transform: rotate(${degValueFilledDial}deg)`"
|
||||
>
|
||||
<rect
|
||||
v-if="degValue >= getLimitDegree('low', 'q1')"
|
||||
class="c-dial__filled-value__low"
|
||||
x="5"
|
||||
y="5"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="degValue >= getLimitDegree('low', 'q2')"
|
||||
class="c-dial__filled-value__mid"
|
||||
x="5"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="degValue >= getLimitDegree('low', 'q3')"
|
||||
class="c-dial__filled-value__high"
|
||||
x="0"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
v-if="valueInBounds && typeNeedleDial"
|
||||
class="c-dial__needle-value-wrapper"
|
||||
viewBox="0 0 10 10"
|
||||
>
|
||||
<g
|
||||
class="c-dial__needle-value"
|
||||
:style="`transform: rotate(${degValue}deg)`"
|
||||
>
|
||||
<path d="M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<template v-if="typeMeter">
|
||||
<div class="c-meter">
|
||||
<div
|
||||
v-if="displayMinMax"
|
||||
class="c-gauge__range c-meter__range js-gauge-meter-range"
|
||||
>
|
||||
<div class="c-meter__range__high">{{ rangeHigh }}</div>
|
||||
<div class="c-meter__range__low">{{ rangeLow }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="c-meter__bg">
|
||||
<template v-if="typeMeterVertical">
|
||||
<div
|
||||
class="c-meter__value"
|
||||
:style="`transform: translateY(${meterValueToPerc}%)`"
|
||||
></div>
|
||||
|
||||
<template v-if="typeMeter">
|
||||
<div class="c-meter">
|
||||
<div
|
||||
v-if="displayMinMax"
|
||||
class="c-gauge__range c-meter__range"
|
||||
<div
|
||||
v-if="limitHigh !== null && meterHighLimitPerc > 0"
|
||||
class="c-meter__limit-high"
|
||||
:style="`height: ${meterHighLimitPerc}%`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitLow !== null && meterLowLimitPerc > 0"
|
||||
class="c-meter__limit-low"
|
||||
:style="`height: ${meterLowLimitPerc}%`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<template v-if="typeMeterHorizontal">
|
||||
<div
|
||||
class="c-meter__value"
|
||||
:style="`transform: translateX(${meterValueToPerc * -1}%)`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitHigh !== null && meterHighLimitPerc > 0"
|
||||
class="c-meter__limit-high"
|
||||
:style="`width: ${meterHighLimitPerc}%`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitLow !== null && meterLowLimitPerc > 0"
|
||||
class="c-meter__limit-low"
|
||||
:style="`width: ${meterLowLimitPerc}%`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<svg
|
||||
class="c-meter__current-value-text-wrapper"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<div class="c-meter__range__high">{{ rangeHigh }}</div>
|
||||
<div class="c-meter__range__low">{{ rangeLow }}</div>
|
||||
</div>
|
||||
<div class="c-meter__bg">
|
||||
<template v-if="typeMeterVertical">
|
||||
<div
|
||||
class="c-meter__value"
|
||||
:style="`transform: translateY(${meterValueToPerc}%)`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitHigh && meterHighLimitPerc > 0"
|
||||
class="c-meter__limit-high"
|
||||
:style="`height: ${meterHighLimitPerc}%`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitLow && meterLowLimitPerc > 0"
|
||||
class="c-meter__limit-low"
|
||||
:style="`height: ${meterLowLimitPerc}%`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<template v-if="typeMeterHorizontal">
|
||||
<div
|
||||
class="c-meter__value"
|
||||
:style="`transform: translateX(${meterValueToPerc * -1}%)`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitHigh && meterHighLimitPerc > 0"
|
||||
class="c-meter__limit-high"
|
||||
:style="`width: ${meterHighLimitPerc}%`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitLow && meterLowLimitPerc > 0"
|
||||
class="c-meter__limit-low"
|
||||
:style="`width: ${meterLowLimitPerc}%`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<svg
|
||||
v-if="displayCurVal"
|
||||
class="c-gauge__curval"
|
||||
class="c-meter__current-value-text-sizer"
|
||||
:viewBox="curValViewBox"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<text
|
||||
class="c-gauge__curval-text"
|
||||
text-anchor="middle"
|
||||
class="c-dial__current-value-text js-meter-current-value"
|
||||
lengthAdjust="spacing"
|
||||
text-anchor="middle"
|
||||
style="transform: translate(50%, 70%)"
|
||||
>{{ curVal }}</text>
|
||||
</svg>
|
||||
</div>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
|
||||
|
||||
const LIMIT_PADDING_IN_PERCENT = 10;
|
||||
|
||||
export default {
|
||||
@ -209,6 +300,13 @@ export default {
|
||||
degValue() {
|
||||
return this.percentToDegrees(this.valToPercent(this.curVal));
|
||||
},
|
||||
degValueFilledDial() {
|
||||
if (this.curVal > this.rangeHigh) {
|
||||
return this.percentToDegrees(100);
|
||||
}
|
||||
|
||||
return this.percentToDegrees(this.valToPercent(this.curVal));
|
||||
},
|
||||
dialHighLimitDeg() {
|
||||
return this.percentToDegrees(this.valToPercent(this.limitHigh));
|
||||
},
|
||||
@ -299,6 +397,7 @@ export default {
|
||||
this.openmct.time.off('timeSystem', this.setTimeSystem);
|
||||
},
|
||||
methods: {
|
||||
getLimitDegree: getLimitDegree,
|
||||
addTelemetryObjectAndSubscribe(domainObject) {
|
||||
this.telemetryObject = domainObject;
|
||||
this.request();
|
||||
@ -340,7 +439,7 @@ export default {
|
||||
return this.gaugeType.indexOf(str) !== -1;
|
||||
},
|
||||
percentToDegrees(vPercent) {
|
||||
return this.round((vPercent / 100) * 270, 2);
|
||||
return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2);
|
||||
},
|
||||
removeFromComposition(telemetryObject = this.telemetryObject) {
|
||||
let composition = this.domainObject.composition.filter(id =>
|
||||
@ -453,7 +552,7 @@ export default {
|
||||
valToPercent(vValue) {
|
||||
// Used by dial
|
||||
if (vValue >= this.rangeHigh && this.typeFilledDial) {
|
||||
// Don't peg at 100% if the gaugeType isn't a filled shape
|
||||
// For filled dial, clip values over the high range to prevent over-rotation
|
||||
return 100;
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@
|
||||
:class="model.cssClass"
|
||||
>
|
||||
<ToggleSwitch
|
||||
:id="'gaugeToggle'"
|
||||
:checked="isUseTelemetryLimits"
|
||||
label="Use telemetry limits for minimum and maximum ranges"
|
||||
@change="toggleUseTelemetryLimits"
|
||||
|
39
src/plugins/gauge/gauge-limit-util.js
Normal file
39
src/plugins/gauge/gauge-limit-util.js
Normal file
@ -0,0 +1,39 @@
|
||||
const GAUGE_LIMITS = {
|
||||
q1: 0,
|
||||
q2: 90,
|
||||
q3: 180,
|
||||
q4: 270
|
||||
};
|
||||
|
||||
export const DIAL_VALUE_DEG_OFFSET = 45;
|
||||
|
||||
// type: low, high
|
||||
// quadrant: low, mid, high, max
|
||||
export function getLimitDegree(type, quadrant) {
|
||||
if (quadrant === 'max') {
|
||||
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
|
||||
}
|
||||
|
||||
return type === 'low'
|
||||
? getLowLimitDegree(quadrant)
|
||||
: getHighLimitDegree(quadrant)
|
||||
;
|
||||
}
|
||||
|
||||
function getLowLimitDegree(quadrant) {
|
||||
return GAUGE_LIMITS[quadrant] + DIAL_VALUE_DEG_OFFSET;
|
||||
}
|
||||
|
||||
function getHighLimitDegree(quadrant) {
|
||||
if (quadrant === 'q1') {
|
||||
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
|
||||
}
|
||||
|
||||
if (quadrant === 'q2') {
|
||||
return GAUGE_LIMITS.q3 + DIAL_VALUE_DEG_OFFSET;
|
||||
}
|
||||
|
||||
if (quadrant === 'q3') {
|
||||
return GAUGE_LIMITS.q2 + DIAL_VALUE_DEG_OFFSET;
|
||||
}
|
||||
}
|
@ -1,9 +1,3 @@
|
||||
$dialClip: polygon(0 0, 100% 0, 100% 100%, 50% 50%, 0 100%);
|
||||
$dialClip90: polygon(0 0, 50% 50%, 0 100%);
|
||||
$dialClip180: polygon(0 0, 100% 0, 0 100%);
|
||||
$limitHighClip90: polygon(0 0, 100% 0, 100% 100%);
|
||||
$limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
|
||||
|
||||
.is-object-type-gauge {
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -13,10 +7,8 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
|
||||
|
||||
&.invalid,
|
||||
&.invalid.req { @include validationState($glyph-icon-x, $colorFormInvalid); }
|
||||
|
||||
&.valid,
|
||||
&.valid.req { @include validationState($glyph-icon-check, $colorFormValid); }
|
||||
|
||||
&.req { @include validationState($glyph-icon-asterisk, $colorFormRequired); }
|
||||
}
|
||||
|
||||
@ -37,92 +29,47 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
|
||||
@include abs();
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
svg {
|
||||
path {
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
&.c-gauge__curval {
|
||||
@include abs();
|
||||
fill: $colorGaugeTextValue;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
|
||||
.c-gauge__curval-text {
|
||||
font-family: $heroFont;
|
||||
transform: translate(50%, 75%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[class*='dial'] {
|
||||
// Square aspect ratio
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
|
||||
&[class*='meter'] {
|
||||
@include abs();
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************** DIAL GAUGE */
|
||||
.c-dial {
|
||||
// Dial elements
|
||||
@include abs();
|
||||
clip-path: $dialClip;
|
||||
svg[class*='c-dial'] {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
position: absolute;
|
||||
|
||||
svg,
|
||||
&__ticks,
|
||||
&__bg,
|
||||
&[class*='__limit'],
|
||||
&__value {
|
||||
@include abs();
|
||||
}
|
||||
|
||||
.c-high-limit-clip--90 {
|
||||
clip-path: $limitHighClip90;
|
||||
}
|
||||
|
||||
.c-high-limit-clip--180 {
|
||||
clip-path: $limitHighClip180;
|
||||
}
|
||||
|
||||
&__limit-high path { fill: $colorGaugeLimitHigh; }
|
||||
&__limit-low path { fill: $colorGaugeLimitLow; }
|
||||
|
||||
&__value,
|
||||
&__limit-low {
|
||||
&.c-dial-clip--90 {
|
||||
clip-path: $dialClip90;
|
||||
}
|
||||
|
||||
&.c-dial-clip--180 {
|
||||
clip-path: $dialClip180;
|
||||
}
|
||||
}
|
||||
|
||||
&__value {
|
||||
path,
|
||||
polygon {
|
||||
fill: $colorGaugeValue;
|
||||
}
|
||||
}
|
||||
|
||||
&__bg {
|
||||
path {
|
||||
fill: $colorGaugeBg;
|
||||
}
|
||||
g {
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
|
||||
.c-gauge--dial-needle .c-dial__value {
|
||||
path {
|
||||
.c-dial {
|
||||
&__bg {
|
||||
background: $colorGaugeBg;
|
||||
clip-path: url(#gaugeBgMask);
|
||||
}
|
||||
|
||||
&__limit-high rect { fill: $colorGaugeLimitHigh; }
|
||||
&__limit-low rect { fill: $colorGaugeLimitLow; }
|
||||
|
||||
&__filled-value-wrapper {
|
||||
clip-path: url(#gaugeValueMask);
|
||||
}
|
||||
|
||||
&__needle-value-wrapper {
|
||||
clip-path: url(#gaugeValueMask);
|
||||
}
|
||||
|
||||
&__filled-value { fill: $colorGaugeValue; }
|
||||
|
||||
&__needle-value {
|
||||
fill: $colorGaugeValue;
|
||||
transition: transform $transitionTimeGauge;
|
||||
}
|
||||
|
||||
&__current-value-text {
|
||||
fill: $colorGaugeTextValue;
|
||||
font-family: $heroFont;
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************** METER GAUGE */
|
||||
@ -131,6 +78,13 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
|
||||
@include abs();
|
||||
display: flex;
|
||||
|
||||
svg {
|
||||
// current-value-text
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__range {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
@ -55,7 +55,7 @@
|
||||
<div
|
||||
v-if="zoomFactor > 1"
|
||||
class="c-imagery__hints"
|
||||
>Alt-drag to pan</div>
|
||||
>{{ formatImageAltText }}</div>
|
||||
<div
|
||||
ref="focusedImageWrapper"
|
||||
class="image-wrapper"
|
||||
@ -143,13 +143,13 @@
|
||||
<!-- spacecraft position fresh -->
|
||||
<div
|
||||
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
|
||||
class="c-imagery__age icon-check c-imagery--new"
|
||||
class="c-imagery__age icon-check c-imagery--new no-animation"
|
||||
>POS</div>
|
||||
|
||||
<!-- camera position fresh -->
|
||||
<div
|
||||
v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh"
|
||||
class="c-imagery__age icon-check c-imagery--new"
|
||||
class="c-imagery__age icon-check c-imagery--new no-animation"
|
||||
>CAM</div>
|
||||
</div>
|
||||
<div class="h-local-controls">
|
||||
@ -331,6 +331,16 @@ export default {
|
||||
},
|
||||
isImageNew() {
|
||||
let cutoff = FIVE_MINUTES;
|
||||
if (this.imageFreshnessOptions) {
|
||||
const { fadeOutDelayTime, fadeOutDurationTime} = this.imageFreshnessOptions;
|
||||
// convert css duration to IS8601 format for parsing
|
||||
const isoFormattedDuration = 'PT' + fadeOutDurationTime.toUpperCase();
|
||||
const isoFormattedDelay = 'PT' + fadeOutDelayTime.toUpperCase();
|
||||
const parsedDuration = moment.duration(isoFormattedDuration).asMilliseconds();
|
||||
const parsedDelay = moment.duration(isoFormattedDelay).asMilliseconds();
|
||||
cutoff = parsedDuration + parsedDelay;
|
||||
}
|
||||
|
||||
let age = this.numericDuration;
|
||||
|
||||
return age < cutoff && !this.refreshCSS;
|
||||
@ -478,6 +488,16 @@ export default {
|
||||
width: this.sizedImageWidth,
|
||||
height: this.sizedImageHeight
|
||||
};
|
||||
},
|
||||
formatImageAltText() {
|
||||
const regexLinux = /Linux/;
|
||||
const navigator = window.navigator.userAgent;
|
||||
|
||||
if (regexLinux.test(navigator)) {
|
||||
return 'Ctrl+Alt drag to pan';
|
||||
}
|
||||
|
||||
return 'Alt drag to pan';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -514,6 +534,8 @@ export default {
|
||||
if (!this.isPaused) {
|
||||
this.setFocusedImage(imageIndex);
|
||||
this.scrollToRight();
|
||||
} else {
|
||||
this.scrollToFocused();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
@ -865,10 +887,6 @@ export default {
|
||||
this.imageTranslateY = 0;
|
||||
},
|
||||
handlePanZoomUpdate({ newScaleFactor, screenClientX, screenClientY }) {
|
||||
if (!this.isPaused) {
|
||||
this.paused(true);
|
||||
}
|
||||
|
||||
if (!(screenClientX || screenClientY)) {
|
||||
return this.updatePanZoom(newScaleFactor, 0, 0);
|
||||
}
|
||||
@ -977,6 +995,7 @@ export default {
|
||||
|
||||
this.setSizedImageDimensions();
|
||||
this.calculateViewHeight();
|
||||
this.scrollToFocused();
|
||||
},
|
||||
setSizedImageDimensions() {
|
||||
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
|
||||
@ -1017,9 +1036,6 @@ export default {
|
||||
},
|
||||
wheelZoom(e) {
|
||||
e.preventDefault();
|
||||
if (!this.isPaused) {
|
||||
this.paused(true);
|
||||
}
|
||||
|
||||
this.$refs.imageControls.wheelZoom(e);
|
||||
},
|
||||
|
@ -1,3 +1,5 @@
|
||||
@use 'sass:math';
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
background-color: rgba($colorOk, 0.5);
|
||||
@ -138,6 +140,9 @@
|
||||
animation-timing-function: ease-in;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
&.no-animation {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -211,7 +216,7 @@
|
||||
}
|
||||
|
||||
.c-thumb {
|
||||
$w: $imageThumbsD / 2;
|
||||
$w: math.div($imageThumbsD, 2);
|
||||
min-width: $w;
|
||||
width: $w;
|
||||
|
||||
|
@ -46,7 +46,6 @@ export default {
|
||||
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unsubscribe) {
|
||||
@ -169,8 +168,6 @@ export default {
|
||||
// splice array to encourage garbage collection
|
||||
this.imageHistory.splice(0, this.imageHistory.length);
|
||||
|
||||
// requesting history effectively clears imageHistory array
|
||||
return this.requestHistory();
|
||||
},
|
||||
timeSystemChange() {
|
||||
this.timeSystem = this.timeContext.timeSystem();
|
||||
|
@ -91,6 +91,7 @@ describe("The Move Action plugin", () => {
|
||||
});
|
||||
|
||||
describe("when moving an object to a new parent and removing from the old parent", () => {
|
||||
let unObserve;
|
||||
beforeEach((done) => {
|
||||
openmct.router.path = [];
|
||||
|
||||
@ -104,7 +105,7 @@ describe("The Move Action plugin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
openmct.objects.observe(parentObject, '*', (newObject) => {
|
||||
unObserve = openmct.objects.observe(parentObject, '*', (newObject) => {
|
||||
done();
|
||||
});
|
||||
|
||||
@ -113,6 +114,10 @@ describe("The Move Action plugin", () => {
|
||||
moveAction.invoke([childObject, parentObject]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unObserve();
|
||||
});
|
||||
|
||||
it("the child object's identifier should be in the new parent's composition", () => {
|
||||
let newParentChild = anotherParentObject.composition[0];
|
||||
expect(newParentChild).toEqual(childObject.identifier);
|
||||
|
@ -28,12 +28,16 @@
|
||||
@drop.prevent="dropOnEntry"
|
||||
>
|
||||
<div class="c-ne__time-and-content">
|
||||
<div class="c-ne__time">
|
||||
<template v-if="entry.createdBy">
|
||||
<span class="c-icon icon-person">{{ entry.createdBy }}</span>
|
||||
</template>
|
||||
<span>{{ createdOnDate }}</span>
|
||||
<span>{{ createdOnTime }}</span>
|
||||
<div class="c-ne__time-and-creator">
|
||||
<span class="c-ne__created-date">{{ createdOnDate }}</span>
|
||||
<span class="c-ne__created-time">{{ createdOnTime }}</span>
|
||||
|
||||
<span
|
||||
v-if="entry.createdBy"
|
||||
class="c-ne__creator"
|
||||
>
|
||||
<span class="icon-person"></span> {{ entry.createdBy }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="c-ne__content">
|
||||
<template v-if="readOnly && result">
|
||||
|
@ -145,7 +145,7 @@ define([
|
||||
item.size = element.size || DEFAULT_SIZE;
|
||||
item.identifier = telemetryObjects[element.id].identifier;
|
||||
item.displayMode = element.titled ? 'all' : 'value';
|
||||
item.value = openmct.telemetry.getMetadata(telemetryObjects[element.id]).getDefaultDisplayValue();
|
||||
item.value = openmct.telemetry.getMetadata(telemetryObjects[element.id]).getDefaultDisplayValue()?.key;
|
||||
} else if (element.type === 'fixed.box') {
|
||||
item.type = "box-view";
|
||||
item.stroke = element.stroke || DEFAULT_STROKE;
|
||||
|
102
src/plugins/operatorStatus/AbstractStatusIndicator.js
Normal file
102
src/plugins/operatorStatus/AbstractStatusIndicator.js
Normal file
@ -0,0 +1,102 @@
|
||||
/*****************************************************************************
|
||||
* 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 raf from '@/utils/raf';
|
||||
|
||||
export default class AbstractStatusIndicator {
|
||||
#popupComponent;
|
||||
#indicator;
|
||||
#configuration;
|
||||
|
||||
constructor(openmct, configuration) {
|
||||
this.openmct = openmct;
|
||||
this.#configuration = configuration;
|
||||
|
||||
this.showPopup = this.showPopup.bind(this);
|
||||
this.clearPopup = this.clearPopup.bind(this);
|
||||
this.positionBox = this.positionBox.bind(this);
|
||||
this.positionBox = raf(this.positionBox);
|
||||
|
||||
this.#indicator = this.createIndicator();
|
||||
this.#popupComponent = this.createPopupComponent();
|
||||
}
|
||||
|
||||
install() {
|
||||
this.openmct.indicators.add(this.#indicator);
|
||||
}
|
||||
|
||||
showPopup() {
|
||||
const popupElement = this.getPopupElement();
|
||||
|
||||
document.body.appendChild(popupElement.$el);
|
||||
//Use capture so we don't trigger immediately on the same iteration of the event loop
|
||||
document.addEventListener('click', this.clearPopup, {
|
||||
capture: true
|
||||
});
|
||||
|
||||
this.positionBox();
|
||||
|
||||
window.addEventListener('resize', this.positionBox);
|
||||
}
|
||||
|
||||
positionBox() {
|
||||
const popupElement = this.getPopupElement();
|
||||
const indicator = this.getIndicator();
|
||||
|
||||
let indicatorBox = indicator.element.getBoundingClientRect();
|
||||
popupElement.positionX = indicatorBox.left;
|
||||
popupElement.positionY = indicatorBox.bottom;
|
||||
|
||||
const popupRight = popupElement.positionX + popupElement.$el.clientWidth;
|
||||
const offsetLeft = Math.min(window.innerWidth - popupRight, 0);
|
||||
popupElement.positionX = popupElement.positionX + offsetLeft;
|
||||
}
|
||||
|
||||
clearPopup(clickAwayEvent) {
|
||||
const popupElement = this.getPopupElement();
|
||||
|
||||
if (!popupElement.$el.contains(clickAwayEvent.target)) {
|
||||
popupElement.$el.remove();
|
||||
document.removeEventListener('click', this.clearPopup);
|
||||
window.removeEventListener('resize', this.positionBox);
|
||||
}
|
||||
}
|
||||
|
||||
createPopupComponent() {
|
||||
throw new Error('Must override createPopupElement method');
|
||||
}
|
||||
|
||||
getPopupElement() {
|
||||
return this.#popupComponent;
|
||||
}
|
||||
|
||||
createIndicator() {
|
||||
throw new Error('Must override createIndicator method');
|
||||
}
|
||||
|
||||
getIndicator() {
|
||||
return this.#indicator;
|
||||
}
|
||||
|
||||
getConfiguration() {
|
||||
return this.#configuration;
|
||||
}
|
||||
}
|
142
src/plugins/operatorStatus/operator-status.scss
Normal file
142
src/plugins/operatorStatus/operator-status.scss
Normal file
@ -0,0 +1,142 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
$statusCountWidth: 30px;
|
||||
|
||||
.c-status-poll-panel {
|
||||
@include menuOuter();
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $interiorMarginLg;
|
||||
min-width: 350px;
|
||||
max-width: 35%;
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMarginLg;
|
||||
}
|
||||
|
||||
*:before {
|
||||
font-size: 0.8em;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&__section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMarginLg;
|
||||
}
|
||||
}
|
||||
|
||||
&__top {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__user-role,
|
||||
&__updated {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
&__updated {
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__poll-question {
|
||||
background: $colorBodyFg;
|
||||
color: $colorBodyBg;
|
||||
border-radius: $controlCr;
|
||||
font-weight: bold;
|
||||
padding: $interiorMarginSm $interiorMargin;
|
||||
|
||||
.c-status-poll-panel--admin & {
|
||||
background: rgba($colorBodyFg, 0.1);
|
||||
color: $colorBodyFg;
|
||||
}
|
||||
}
|
||||
|
||||
/****** Admin interface */
|
||||
&__content {
|
||||
$m: $interiorMargin;
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-column-gap: $m;
|
||||
grid-row-gap: $m;
|
||||
|
||||
[class*='__label'] {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
[class*='new-question'] {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
> * + * { margin-left: $interiorMargin; }
|
||||
|
||||
input {
|
||||
flex: 1 1 auto;
|
||||
height: $btnStdH;
|
||||
}
|
||||
|
||||
button { flex: 0 0 auto; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-status-poll-report {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
> * + * { margin-left: $interiorMargin; }
|
||||
|
||||
&__count {
|
||||
background: rgba($colorBodyFg, 0.2);
|
||||
border-radius: $controlCr;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 1.25em;
|
||||
align-items: center;
|
||||
padding: $interiorMarginSm $interiorMarginLg;
|
||||
|
||||
&-type {
|
||||
line-height: 1em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-indicator {
|
||||
&:before {
|
||||
// Indicator icon
|
||||
color: $colorKey;
|
||||
}
|
||||
|
||||
&--operator-status {
|
||||
cursor: pointer;
|
||||
max-width: 150px;
|
||||
|
||||
@include hover() {
|
||||
background: $colorIndicatorBgHov;
|
||||
}
|
||||
}
|
||||
}
|
187
src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue
Normal file
187
src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue
Normal file
@ -0,0 +1,187 @@
|
||||
<!--
|
||||
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
|
||||
:style="position"
|
||||
class="c-status-poll-panel c-status-poll-panel--operator"
|
||||
@click.stop="noop"
|
||||
>
|
||||
<div class="c-status-poll-panel__section c-status-poll-panel__top">
|
||||
<div
|
||||
class="c-status-poll-panel__title"
|
||||
>Status Poll</div>
|
||||
<div class="c-status-poll-panel__user-role icon-person">{{ role }}</div>
|
||||
<div class="c-status-poll-panel__updated">{{ pollQuestionUpdated }}</div>
|
||||
</div>
|
||||
|
||||
<div class="c-status-poll-panel__section c-status-poll-panel__poll-question">
|
||||
{{ currentPollQuestion }}
|
||||
</div>
|
||||
|
||||
<div class="c-status-poll-panel__section c-status-poll-panel__bottom">
|
||||
<div class="c-status-poll-panel__set-status-label">My status:</div>
|
||||
<select
|
||||
v-model="selectedStatus"
|
||||
name="setStatus"
|
||||
@change="changeStatus"
|
||||
>
|
||||
<option
|
||||
v-for="status in allStatuses"
|
||||
:key="status.key"
|
||||
:value="status.key"
|
||||
>
|
||||
{{ status.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'indicator', 'configuration'],
|
||||
props: {
|
||||
positionX: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
positionY: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
allRoles: [],
|
||||
role: '--',
|
||||
pollQuestionUpdated: '--',
|
||||
currentPollQuestion: '--',
|
||||
selectedStatus: undefined,
|
||||
allStatuses: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
position() {
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: `${this.positionX}px`,
|
||||
top: `${this.positionY}px`
|
||||
};
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.user.status.off('statusChange', this.setStatus);
|
||||
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
|
||||
},
|
||||
async mounted() {
|
||||
this.unsubscribe = [];
|
||||
await this.fetchUser();
|
||||
await this.findFirstApplicableRole();
|
||||
this.fetchPossibleStatusesForUser();
|
||||
this.fetchCurrentPoll();
|
||||
this.fetchMyStatus();
|
||||
this.subscribeToMyStatus();
|
||||
this.subscribeToPollQuestion();
|
||||
},
|
||||
methods: {
|
||||
async findFirstApplicableRole() {
|
||||
this.role = await this.openmct.user.status.getStatusRoleForCurrentUser();
|
||||
},
|
||||
async fetchUser() {
|
||||
this.user = await this.openmct.user.getCurrentUser();
|
||||
},
|
||||
async fetchCurrentPoll() {
|
||||
const pollQuestion = await this.openmct.user.status.getPollQuestion();
|
||||
if (pollQuestion !== undefined) {
|
||||
this.setPollQuestion(pollQuestion);
|
||||
}
|
||||
},
|
||||
async fetchPossibleStatusesForUser() {
|
||||
this.allStatuses = await this.openmct.user.status.getPossibleStatuses();
|
||||
},
|
||||
setPollQuestion(pollQuestion) {
|
||||
this.currentPollQuestion = pollQuestion.question;
|
||||
this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();
|
||||
|
||||
this.indicator.text(pollQuestion?.question || '');
|
||||
},
|
||||
async fetchMyStatus() {
|
||||
const activeStatusRole = await this.openmct.user.status.getStatusRoleForCurrentUser();
|
||||
const status = await this.openmct.user.status.getStatusForRole(activeStatusRole);
|
||||
|
||||
if (status !== undefined) {
|
||||
this.setStatus({status});
|
||||
}
|
||||
},
|
||||
subscribeToMyStatus() {
|
||||
this.openmct.user.status.on('statusChange', this.setStatus);
|
||||
},
|
||||
subscribeToPollQuestion() {
|
||||
this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
|
||||
},
|
||||
setStatus({role, status}) {
|
||||
status = this.applyStyling(status);
|
||||
this.selectedStatus = status.key;
|
||||
this.indicator.iconClass(status.iconClassPoll);
|
||||
this.indicator.statusClass(status.statusClass);
|
||||
if (this.isDefaultStatus(status)) {
|
||||
this.indicator.text(this.currentPollQuestion);
|
||||
} else {
|
||||
this.indicator.text(status.label);
|
||||
}
|
||||
},
|
||||
isDefaultStatus(status) {
|
||||
return status.key === this.allStatuses[0].key;
|
||||
},
|
||||
findStatusByKey(statusKey) {
|
||||
return this.allStatuses.find(possibleMatch => possibleMatch.key === statusKey);
|
||||
},
|
||||
async changeStatus() {
|
||||
if (this.selectedStatus !== undefined) {
|
||||
const statusObject = this.findStatusByKey(this.selectedStatus);
|
||||
|
||||
const result = await this.openmct.user.status.setStatusForRole(this.role, statusObject);
|
||||
|
||||
if (result === true) {
|
||||
this.openmct.notifications.info("Successfully set operator status");
|
||||
} else {
|
||||
this.openmct.notifications.error("Unable to set operator status");
|
||||
}
|
||||
}
|
||||
},
|
||||
applyStyling(status) {
|
||||
const stylesForStatus = this.configuration?.statusStyles?.[status.label];
|
||||
|
||||
if (stylesForStatus !== undefined) {
|
||||
return {
|
||||
...status,
|
||||
...stylesForStatus
|
||||
};
|
||||
} else {
|
||||
return status;
|
||||
}
|
||||
},
|
||||
noop() {}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,63 @@
|
||||
/*****************************************************************************
|
||||
* 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 Vue from 'vue';
|
||||
|
||||
import AbstractStatusIndicator from '../AbstractStatusIndicator';
|
||||
import OperatorStatusComponent from './OperatorStatus.vue';
|
||||
|
||||
export default class OperatorStatusIndicator extends AbstractStatusIndicator {
|
||||
createPopupComponent() {
|
||||
const indicator = this.getIndicator();
|
||||
const popupElement = new Vue({
|
||||
components: {
|
||||
OperatorStatus: OperatorStatusComponent
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
indicator: indicator,
|
||||
configuration: this.getConfiguration()
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
positionX: 0,
|
||||
positionY: 0
|
||||
};
|
||||
},
|
||||
template: '<operator-status :positionX="positionX" :positionY="positionY" />'
|
||||
}).$mount();
|
||||
|
||||
return popupElement;
|
||||
}
|
||||
|
||||
createIndicator() {
|
||||
const operatorIndicator = this.openmct.indicators.simpleIndicator();
|
||||
|
||||
operatorIndicator.text("My Operator Status");
|
||||
operatorIndicator.description("Set my operator status");
|
||||
operatorIndicator.iconClass('icon-status-poll-question-mark');
|
||||
operatorIndicator.element.classList.add("c-indicator--operator-status");
|
||||
operatorIndicator.element.classList.add("no-minify");
|
||||
operatorIndicator.on('click', this.showPopup);
|
||||
|
||||
return operatorIndicator;
|
||||
}
|
||||
}
|
46
src/plugins/operatorStatus/plugin.js
Normal file
46
src/plugins/operatorStatus/plugin.js
Normal file
@ -0,0 +1,46 @@
|
||||
/*****************************************************************************
|
||||
* 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 OperatorStatusIndicator from './operatorStatus/OperatorStatusIndicator';
|
||||
import PollQuestionIndicator from './pollQuestion/PollQuestionIndicator';
|
||||
|
||||
export default function operatorStatusPlugin(configuration) {
|
||||
return function install(openmct) {
|
||||
|
||||
if (openmct.user.hasProvider()) {
|
||||
openmct.user.status.canProvideStatusForCurrentUser().then(canProvideStatus => {
|
||||
if (canProvideStatus) {
|
||||
const operatorStatusIndicator = new OperatorStatusIndicator(openmct, configuration);
|
||||
|
||||
operatorStatusIndicator.install();
|
||||
}
|
||||
});
|
||||
|
||||
openmct.user.status.canSetPollQuestion().then(canSetPollQuestion => {
|
||||
if (canSetPollQuestion) {
|
||||
const pollQuestionIndicator = new PollQuestionIndicator(openmct, configuration);
|
||||
|
||||
pollQuestionIndicator.install();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
184
src/plugins/operatorStatus/pollQuestion/PollQuestion.vue
Normal file
184
src/plugins/operatorStatus/pollQuestion/PollQuestion.vue
Normal file
@ -0,0 +1,184 @@
|
||||
<!--
|
||||
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
|
||||
:style="position"
|
||||
class="c-status-poll-panel c-status-poll-panel--admin"
|
||||
@click.stop="noop"
|
||||
>
|
||||
<div class="c-status-poll-panel__section c-status-poll-panel__top">
|
||||
<div
|
||||
class="c-status-poll-panel__title"
|
||||
>Manage Status Poll</div>
|
||||
<div class="c-status-poll-panel__updated">Last updated: {{ pollQuestionUpdated }}</div>
|
||||
</div>
|
||||
|
||||
<div class="c-status-poll__section c-status-poll-panel__content c-spq">
|
||||
<!-- Grid layout -->
|
||||
<div class="c-spq__label">Current:</div>
|
||||
<div class="c-spq__value c-status-poll-panel__poll-question">{{ currentPollQuestion }}</div>
|
||||
|
||||
<template v-if="statusCountViewModel.length > 0">
|
||||
<div class="c-spq__label">Reporting:</div>
|
||||
<div class="c-spq__value c-status-poll-panel__poll-reporting c-status-poll-report">
|
||||
<div
|
||||
v-for="entry in statusCountViewModel"
|
||||
:key="entry.status.key"
|
||||
class="c-status-poll-report__count"
|
||||
:style="[{
|
||||
background: entry.status.statusBgColor,
|
||||
color: entry.status.statusFgColor
|
||||
}]"
|
||||
>
|
||||
<div
|
||||
class="c-status-poll-report__count-type"
|
||||
:class="entry.status.iconClass"
|
||||
></div>
|
||||
<div class="c-status-poll-report__count-value">
|
||||
{{ entry.roleCount }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="c-spq__label">New poll:</div>
|
||||
<div class="c-spq__value c-status-poll-panel__poll-new-question">
|
||||
<input
|
||||
v-model="newPollQuestion"
|
||||
type="text"
|
||||
name="newPollQuestion"
|
||||
>
|
||||
<button
|
||||
class="c-button"
|
||||
@click="updatePollQuestion"
|
||||
>Update</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'indicator', 'configuration'],
|
||||
props: {
|
||||
positionX: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
positionY: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pollQuestionUpdated: '--',
|
||||
currentPollQuestion: '--',
|
||||
newPollQuestion: undefined,
|
||||
statusCountViewModel: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
position() {
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: `${this.positionX}px`,
|
||||
top: `${this.positionY}px`
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCurrentPoll();
|
||||
this.subscribeToPollQuestion();
|
||||
this.fetchStatusSummary();
|
||||
this.openmct.user.status.on('statusChange', this.fetchStatusSummary);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.user.status.off('statusChange', this.fetchStatusSummary);
|
||||
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
|
||||
},
|
||||
methods: {
|
||||
async fetchCurrentPoll() {
|
||||
const pollQuestion = await this.openmct.user.status.getPollQuestion();
|
||||
if (pollQuestion !== undefined) {
|
||||
this.setPollQuestion(pollQuestion);
|
||||
}
|
||||
},
|
||||
subscribeToPollQuestion() {
|
||||
this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
|
||||
},
|
||||
setPollQuestion(pollQuestion) {
|
||||
this.currentPollQuestion = pollQuestion.question;
|
||||
this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();
|
||||
this.indicator.text(pollQuestion.question);
|
||||
},
|
||||
async updatePollQuestion() {
|
||||
const result = await this.openmct.user.status.setPollQuestion(this.newPollQuestion);
|
||||
if (result === true) {
|
||||
this.openmct.notifications.info("Successfully set new poll question");
|
||||
} else {
|
||||
this.openmct.notifications.error("Unable to set new poll question.");
|
||||
}
|
||||
|
||||
this.newPollQuestion = undefined;
|
||||
},
|
||||
async fetchStatusSummary() {
|
||||
const allStatuses = await this.openmct.user.status.getPossibleStatuses();
|
||||
const statusCountMap = allStatuses.reduce((statusToCountMap, status) => {
|
||||
statusToCountMap[status.key] = 0;
|
||||
|
||||
return statusToCountMap;
|
||||
}, {});
|
||||
const allStatusRoles = await this.openmct.user.status.getAllStatusRoles();
|
||||
const statusesForRoles = await Promise.all(allStatusRoles.map(role => this.openmct.user.status.getStatusForRole(role)));
|
||||
|
||||
statusesForRoles.forEach((status, i) => {
|
||||
const currentCount = statusCountMap[status.key];
|
||||
statusCountMap[status.key] = currentCount + 1;
|
||||
});
|
||||
|
||||
this.statusCountViewModel = allStatuses.map((status) => {
|
||||
return {
|
||||
status: this.applyStyling(status),
|
||||
roleCount: statusCountMap[status.key]
|
||||
};
|
||||
});
|
||||
},
|
||||
applyStyling(status) {
|
||||
const stylesForStatus = this.configuration?.statusStyles?.[status.label];
|
||||
|
||||
if (stylesForStatus !== undefined) {
|
||||
return {
|
||||
...status,
|
||||
...stylesForStatus
|
||||
};
|
||||
} else {
|
||||
return status;
|
||||
}
|
||||
},
|
||||
noop() {}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user