mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 10:44:21 +00:00
Compare commits
84 Commits
fix-github
...
temp-sourc
Author | SHA1 | Date | |
---|---|---|---|
1a9c845f84 | |||
c46849b166 | |||
6c71fa01f5 | |||
c56d458ecb | |||
f74a35f45a | |||
dfa2e1ef1e | |||
fa4c58a7cb | |||
6c642281e7 | |||
68e46e45c7 | |||
8cd87ff9d1 | |||
d9ac0182c3 | |||
7bb108c36b | |||
77804cff75 | |||
2d73296b36 | |||
405418b9d5 | |||
f999b9e12b | |||
664ba399ea | |||
c6078a234a | |||
17c16eba50 | |||
9f9c69ee68 | |||
037886aa01 | |||
48916564e4 | |||
1ca5271c3e | |||
6521b888d6 | |||
85fce3c456 | |||
8d577a8958 | |||
9c8ee09960 | |||
9568da9d5f | |||
2aa3b810ba | |||
1cdbb34e21 | |||
95299336d0 | |||
b8ff5c7f33 | |||
9ede023cfa | |||
308e621b5d | |||
e6b5870234 | |||
03e7d912be | |||
09da373d1c | |||
b8d9e41c01 | |||
815e7d169c | |||
58387e0902 | |||
0a0826f87e | |||
e063442d8c | |||
6a5823ab5c | |||
0493e5ae3c | |||
24f13b6249 | |||
221fb4d6bf | |||
257742b45b | |||
44edec4f04 | |||
ab4d0dd37f | |||
c089a4760d | |||
b77a4066f2 | |||
20d7e80502 | |||
d63fec51a7 | |||
d30c4fcb53 | |||
fff3ce0acf | |||
db5cb2517f | |||
5236f1c796 | |||
1ed253cb07 | |||
a6553ba010 | |||
cf6bc5be2d | |||
a53a3a0297 | |||
402cd15726 | |||
a5580912e3 | |||
54d1b8991c | |||
7b6acee793 | |||
04e1c60e5c | |||
91bcd78d40 | |||
a3c0e073c8 | |||
21ae9f45c1 | |||
0a40c8dd0b | |||
ef1ea8e712 | |||
5c4fad77ff | |||
dbac9e6cd2 | |||
4b7bcf9c89 | |||
2b42abd495 | |||
1f2102b845 | |||
2ccb90aa41 | |||
525496fbca | |||
47099786cb | |||
3a11291a3b | |||
476f1b2579 | |||
6153ad8e1e | |||
77c0b16050 | |||
d19088cec6 |
@ -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:
|
||||
@ -23,7 +23,7 @@ commands:
|
||||
- node/install:
|
||||
install-npm: true
|
||||
node-version: << parameters.node-version >>
|
||||
- run: npm install
|
||||
- run: npm install --prefer-offline --no-audit --progress=false
|
||||
restore_cache_cmd:
|
||||
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
|
||||
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:
|
||||
@ -128,16 +128,30 @@ jobs:
|
||||
suite:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
parallelism: 4
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npx playwright install
|
||||
- run: npm run test:e2e:<<parameters.suite>>
|
||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
perf-test:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run test:perf
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
@ -149,15 +163,17 @@ workflows:
|
||||
node-version: lts/fermium
|
||||
browser: ChromeHeadless
|
||||
post-steps:
|
||||
- upload_code_covio
|
||||
- upload_code_covio
|
||||
- unit-test:
|
||||
name: node16-chrome
|
||||
node-version: lts/gallium
|
||||
browser: ChromeHeadless
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
browser: ChromeHeadless
|
||||
- e2e-test:
|
||||
name: e2e-ci
|
||||
node-version: lts/gallium
|
||||
suite: ci
|
||||
- perf-test:
|
||||
node-version: lts/gallium
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
@ -176,6 +192,10 @@ workflows:
|
||||
name: node16-chrome-nightly
|
||||
node-version: lts/gallium
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
browser: ChromeHeadless
|
||||
- npm-audit:
|
||||
node-version: lts/gallium
|
||||
- e2e-test:
|
||||
|
@ -29,6 +29,7 @@ module.exports = {
|
||||
"you-dont-need-lodash-underscore/omit": "off",
|
||||
"you-dont-need-lodash-underscore/throttle": "off",
|
||||
"you-dont-need-lodash-underscore/flatten": "off",
|
||||
"you-dont-need-lodash-underscore/get": "off",
|
||||
"no-bitwise": "error",
|
||||
"curly": "error",
|
||||
"eqeqeq": "error",
|
||||
|
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -27,7 +27,7 @@ assignees: ''
|
||||
|
||||
#### Environment
|
||||
<!--- If encountered on local machine, execute the following:
|
||||
<!--- npx envinfo --system --browsers --npmPackages --binaries --languages --markdown -->
|
||||
<!--- npx envinfo --system --browsers --npmPackages --binaries --markdown -->
|
||||
* Open MCT Version: <!--- date of build, version, or SHA -->
|
||||
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
|
||||
* OS:
|
||||
@ -40,6 +40,8 @@ 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?
|
||||
- [ ] Does this have an impact on Performance?
|
||||
|
||||
#### 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
|
||||
|
4
.github/workflows/e2e-pr.yml
vendored
4
.github/workflows/e2e-pr.yml
vendored
@ -30,11 +30,11 @@ 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
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
- name: Test success
|
||||
|
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
|
||||
|
1
.github/workflows/pr-platform.yml
vendored
1
.github/workflows/pr-platform.yml
vendored
@ -18,6 +18,7 @@ jobs:
|
||||
node_version:
|
||||
- 14
|
||||
- 16
|
||||
- 18
|
||||
architecture:
|
||||
- x64
|
||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||
|
@ -1,4 +1,12 @@
|
||||
/* eslint-disable no-undef */
|
||||
module.exports = {
|
||||
"extends": ["plugin:playwright/playwright-test"]
|
||||
"extends": ["plugin:playwright/playwright-test"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["tests/visual/*.spec.js"],
|
||||
"rules": {
|
||||
"playwright/no-wait-for-timeout": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
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,38 +2,41 @@
|
||||
// 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,
|
||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
|
||||
timeout: 60 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
port: 8080,
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
maxFailures: process.env.CI ? 5 : undefined, //Limits failures to 5 to reduce CI Waste
|
||||
workers: 2, //Limit to 2 for CircleCI Agent
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'on',
|
||||
trace: 'on',
|
||||
video: 'on'
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'on-first-retry',
|
||||
video: 'on-first-retry'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
...devices['Desktop Chrome']
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
@ -52,8 +55,11 @@ const config = {
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['html', {
|
||||
open: 'never',
|
||||
outputFolder: '../test-results/html/'
|
||||
}],
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['allure-playwright'],
|
||||
['github']
|
||||
]
|
||||
};
|
||||
|
@ -2,12 +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: 0,
|
||||
testDir: 'tests',
|
||||
testIgnore: '**/*.perf.spec.js',
|
||||
timeout: 30 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
@ -21,20 +23,20 @@ const config = {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: false,
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'on',
|
||||
trace: 'on',
|
||||
video: 'on'
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'retain-on-failure',
|
||||
video: 'retain-on-failure'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
...devices['Desktop Chrome']
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
@ -53,7 +55,10 @@ const config = {
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['allure-playwright']
|
||||
['html', {
|
||||
open: 'on-failure',
|
||||
outputFolder: '../test-results'
|
||||
}]
|
||||
]
|
||||
};
|
||||
|
||||
|
41
e2e/playwright-performance.config.js
Normal file
41
e2e/playwright-performance.config.js
Normal file
@ -0,0 +1,41 @@
|
||||
/* eslint-disable no-undef */
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0,
|
||||
testDir: 'tests/performance/',
|
||||
timeout: 30 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
port: 8080,
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
use: {
|
||||
browserName: "chromium",
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: Boolean(process.env.CI), //Only if running locally
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'off',
|
||||
trace: 'off',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['json', { outputFile: 'test-results/results.json' }]
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
@ -4,10 +4,10 @@
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0,
|
||||
testDir: 'tests',
|
||||
retries: 0, // visual tests should never retry due to snapshot comparison errors
|
||||
testDir: 'tests/visual',
|
||||
timeout: 90 * 1000,
|
||||
workers: 1,
|
||||
workers: 1, // visual tests should never run in parallel due to test pollution
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
port: 8080,
|
||||
@ -17,7 +17,7 @@ const config = {
|
||||
use: {
|
||||
browserName: "chromium",
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
headless: true, // this needs to remain headless to avoid visual changes due to GPU
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'on',
|
||||
trace: 'off',
|
||||
@ -25,8 +25,7 @@ const config = {
|
||||
},
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['allure-playwright']
|
||||
['junit', { outputFile: 'test-results/results.xml' }]
|
||||
]
|
||||
};
|
||||
|
||||
|
1
e2e/test-data/PerformanceDisplayLayout.json
Normal file
1
e2e/test-data/PerformanceDisplayLayout.json
Normal file
@ -0,0 +1 @@
|
||||
{"openmct":{"21338566-d472-4377-aed1-21b79272c8de":{"identifier":{"key":"21338566-d472-4377-aed1-21b79272c8de","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":1,"y":1,"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"5aeb5a71-3149-41ed-9d8a-d34b0a18b053"}],"layoutGrid":[10,10]},"modified":1652228997384,"location":"mine","persisted":1652228997384},"644c2e47-2903-475f-8a4a-6be1588ee02f":{"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1}},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1652228997375,"location":"21338566-d472-4377-aed1-21b79272c8de","persisted":1652228997375}},"rootId":"21338566-d472-4377-aed1-21b79272c8de"}
|
1
e2e/test-data/PerformanceNotebook.json
Normal file
1
e2e/test-data/PerformanceNotebook.json
Normal file
@ -0,0 +1 @@
|
||||
{"openmct":{"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d":{"identifier":{"key":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d","namespace":""},"name":"Performance Notebook","type":"notebook","configuration":{"defaultSort":"oldest","entries":{"3e31c412-33ba-4757-8ade-e9821f6ba321":{"8c8f6035-631c-45af-8c24-786c60295335":[{"id":"entry-1652815305457","createdOn":1652815305457,"createdBy":"","text":"Existing Entry 1","embeds":[]},{"id":"entry-1652815313465","createdOn":1652815313465,"createdBy":"","text":"Existing Entry 2","embeds":[]},{"id":"entry-1652815399955","createdOn":1652815399955,"createdBy":"","text":"Existing Entry 3","embeds":[]}]}},"imageMigrationVer":"v1","pageTitle":"Page","sections":[{"id":"3e31c412-33ba-4757-8ade-e9821f6ba321","isDefault":false,"isSelected":false,"name":"Section1","pages":[{"id":"8c8f6035-631c-45af-8c24-786c60295335","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"36555942-c9aa-439c-bbdb-0aaf50db50f5","isDefault":false,"isSelected":false,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"},{"id":"dab0bd1d-2c5a-405c-987f-107123d6189a","isDefault":false,"isSelected":true,"name":"Section2","pages":[{"id":"f625a86a-cb99-4898-8082-80543c8de534","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"e77ef810-f785-42a7-942e-07e999b79c59","isDefault":false,"isSelected":true,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"}],"sectionTitle":"Section","type":"General","showTime":"0"},"modified":1652815915219,"location":"mine","persisted":1652815915222}},"rootId":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"}
|
77
e2e/tests/api/forms/forms.e2e.spec.js
Normal file
77
e2e/tests/api/forms/forms.e2e.spec.js
Normal file
@ -0,0 +1,77 @@
|
||||
/*****************************************************************************
|
||||
* 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');
|
||||
|
||||
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,123 @@
|
||||
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);
|
||||
|
||||
// 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")');
|
||||
|
||||
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);
|
||||
|
||||
// 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")');
|
||||
|
||||
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 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);
|
||||
|
||||
// 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")');
|
||||
|
||||
await 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 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();
|
||||
});
|
||||
});
|
||||
|
177
e2e/tests/performance/imagery.perf.spec.js
Normal file
177
e2e/tests/performance/imagery.perf.spec.js
Normal file
@ -0,0 +1,177 @@
|
||||
/*****************************************************************************
|
||||
* 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 performance tests to ensure that testability of performance
|
||||
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here
|
||||
|
||||
TODO:
|
||||
- Update resolution of performance config
|
||||
- Add Performance Observer on init to push all performance marks
|
||||
- Move client CDP connection to before or to a fixture
|
||||
-
|
||||
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
|
||||
|
||||
test.describe('Performance tests', () => {
|
||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Click text=Import from JSON
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload Performance Display Layout.json
|
||||
await page.setInputFiles('#fileElem', filePath);
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
|
||||
|
||||
//Create a Chrome Performance Timeline trace to store as a test artifact
|
||||
console.log("\n==== Devtools: startTracing ====\n");
|
||||
await browser.startTracing(page, {
|
||||
path: `${testInfo.outputPath()}-trace.json`,
|
||||
screenshots: true
|
||||
});
|
||||
});
|
||||
test.afterEach(async ({ page, browser}) => {
|
||||
console.log("\n==== Devtools: stopTracing ====\n");
|
||||
await browser.stopTracing();
|
||||
|
||||
/* Measurement Section
|
||||
/ The following section includes a block of performance measurements.
|
||||
*/
|
||||
//Get time difference between viewlarge actionability and evaluate time
|
||||
await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test")));
|
||||
|
||||
//Get StartTime
|
||||
const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
|
||||
console.log('window.performance.timing.navigationStart', startTime);
|
||||
|
||||
//Get All Performance Marks
|
||||
const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
|
||||
const getAllMarks = JSON.parse(getAllMarksJson);
|
||||
console.log('window.performance.getEntriesByType("mark")', getAllMarks);
|
||||
|
||||
//Get All Performance Measures
|
||||
const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
|
||||
const getAllMeasures = JSON.parse(getAllMeasuresJson);
|
||||
console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
|
||||
|
||||
});
|
||||
/* The following test will navigate to a previously created Performance Display Layout and measure the
|
||||
/ following metrics:
|
||||
/ - ElementResourceTiming
|
||||
/ - Interaction Timing
|
||||
*/
|
||||
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
|
||||
const client = await page.context().newCDPSession(page);
|
||||
// Tell the DevTools session to record performance metrics
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
|
||||
await client.send('Performance.enable');
|
||||
// Go to baseURL
|
||||
await page.goto('/');
|
||||
|
||||
// Search Available after Launch
|
||||
await page.locator('input[type="search"]').click();
|
||||
await page.evaluate(() => window.performance.mark("search-available"));
|
||||
// Fill Search input
|
||||
await page.locator('input[type="search"]').fill('Performance Display Layout');
|
||||
await page.evaluate(() => window.performance.mark("search-entered"));
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("Performance Display Layout")').first().click(),
|
||||
page.evaluate(() => window.performance.mark("click-search-result"))
|
||||
]);
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
|
||||
|
||||
//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];
|
||||
});
|
||||
backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre
|
||||
console.log('backgroundImageurl ' + backgroundImageUrl);
|
||||
|
||||
//Get ResourceTiming of background-image jpg
|
||||
const resourceTimingJson = await page.evaluate((bgImageUrl) =>
|
||||
JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()),
|
||||
backgroundImageUrl
|
||||
);
|
||||
console.log('resourceTimingJson ' + resourceTimingJson);
|
||||
|
||||
//Open Large view
|
||||
await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start'
|
||||
await page.evaluate(() => window.performance.mark("viewLarge.start.test")); //This is a mark only to compare evaluate timing
|
||||
|
||||
//Time to Imagery Rendered in Large Frame
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("background-image-frame"));
|
||||
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("background-image-visible"));
|
||||
|
||||
// Get Current number of images in thumbstrip
|
||||
await page.waitForSelector('.c-imagery__thumb');
|
||||
const thumbCount = await page.locator('.c-imagery__thumb').count();
|
||||
console.log('number of thumbs rendered ' + thumbCount);
|
||||
await page.locator('.c-imagery__thumb').last().click();
|
||||
|
||||
//Get ResourceTiming of all jpg resources
|
||||
const resourceTimingJson2 = await page.evaluate(() =>
|
||||
JSON.stringify(window.performance.getEntriesByType('resource'))
|
||||
);
|
||||
const resourceTiming = JSON.parse(resourceTimingJson2);
|
||||
const jpgResourceTiming = resourceTiming.find((element) =>
|
||||
element.name.includes('.jpg')
|
||||
);
|
||||
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
|
||||
|
||||
// Click Close Icon
|
||||
await page.locator('.c-click-icon').click();
|
||||
await page.evaluate(() => window.performance.mark("view-large-close-button"));
|
||||
|
||||
//await client.send('HeapProfiler.enable');
|
||||
await client.send('HeapProfiler.collectGarbage');
|
||||
|
||||
let performanceMetrics = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetrics.metrics);
|
||||
|
||||
});
|
||||
});
|
119
e2e/tests/performance/memleak-imagery.perf.spec.js
Normal file
119
e2e/tests/performance/memleak-imagery.perf.spec.js
Normal file
@ -0,0 +1,119 @@
|
||||
/*****************************************************************************
|
||||
* 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 an initial example for memory leak testing using performance. This configuration and execution must
|
||||
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
|
||||
or profiling playwright and/or the browser.
|
||||
|
||||
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
|
||||
and https://github.com/paulirish/automated-chrome-profiling/issues/3
|
||||
|
||||
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
|
||||
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.describe.skip('Memory Performance tests', () => {
|
||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Click text=Import from JSON
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload Performance Display Layout.json
|
||||
await page.setInputFiles('#fileElem', filePath);
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
|
||||
|
||||
await page.goto('/', {waitUntil: 'networkidle'});
|
||||
|
||||
// To to Search Available after Launch
|
||||
await page.locator('input[type="search"]').click();
|
||||
// Fill Search input
|
||||
await page.locator('input[type="search"]').fill('Performance Display Layout');
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("Performance Display Layout")').first().click()
|
||||
]);
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
|
||||
|
||||
const client = await page.context().newCDPSession(page);
|
||||
await client.send('HeapProfiler.enable');
|
||||
await client.send('HeapProfiler.startSampling');
|
||||
// await client.send('HeapProfiler.collectGarbage');
|
||||
await client.send('Performance.enable');
|
||||
|
||||
let performanceMetricsBefore = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetricsBefore.metrics);
|
||||
|
||||
//await client.send('Performance.disable');
|
||||
|
||||
//Open Large view
|
||||
await page.locator('button:has-text("Large View")').click();
|
||||
await client.send('HeapProfiler.takeHeapSnapshot');
|
||||
|
||||
//Time to Imagery Rendered in Large Frame
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
|
||||
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
|
||||
|
||||
// Click Close Icon
|
||||
await page.locator('.c-click-icon').click();
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
|
||||
|
||||
await client.send('HeapProfiler.collectGarbage');
|
||||
//await client.send('Performance.enable');
|
||||
|
||||
let performanceMetricsAfter = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetricsAfter.metrics);
|
||||
|
||||
//await client.send('Performance.disable');
|
||||
|
||||
});
|
||||
});
|
158
e2e/tests/performance/notebook.perf.spec.js
Normal file
158
e2e/tests/performance/notebook.perf.spec.js
Normal file
@ -0,0 +1,158 @@
|
||||
/*****************************************************************************
|
||||
* 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 performance tests to ensure that testability of performance
|
||||
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here.
|
||||
|
||||
TODO:
|
||||
- Update resolution of performance config
|
||||
- Add Performance Observer on init to push all performance marks
|
||||
- Move client CDP connection to before or to a fixture
|
||||
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json';
|
||||
|
||||
test.describe('Performance tests', () => {
|
||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Click text=Import from JSON
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload Performance Display Layout.json
|
||||
await page.setInputFiles('#fileElem', notebookFilePath);
|
||||
|
||||
// TODO Fix this
|
||||
await page.locator('text=OK >> nth=1').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible();
|
||||
|
||||
//Create a Chrome Performance Timeline trace to store as a test artifact
|
||||
console.log("\n==== Devtools: startTracing ====\n");
|
||||
await browser.startTracing(page, {
|
||||
path: `${testInfo.outputPath()}-trace.json`,
|
||||
screenshots: true
|
||||
});
|
||||
});
|
||||
test.afterEach(async ({ page, browser}) => {
|
||||
console.log("\n==== Devtools: stopTracing ====\n");
|
||||
await browser.stopTracing();
|
||||
|
||||
/* Measurement Section
|
||||
/ The following section includes a block of performance measurements.
|
||||
*/
|
||||
const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
|
||||
console.log('window.performance.timing.navigationStart', startTime);
|
||||
|
||||
//Get All Performance Marks
|
||||
const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
|
||||
const getAllMarks = JSON.parse(getAllMarksJson);
|
||||
console.log('window.performance.getEntriesByType("mark")', getAllMarks);
|
||||
|
||||
//Get All Performance Measures
|
||||
const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
|
||||
const getAllMeasures = JSON.parse(getAllMeasuresJson);
|
||||
console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
|
||||
|
||||
});
|
||||
/* The following test will navigate to a previously created Performance Display Layout and measure the
|
||||
/ following metrics:
|
||||
/ - ElementResourceTiming
|
||||
/ - Interaction Timing
|
||||
*/
|
||||
test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => {
|
||||
const client = await page.context().newCDPSession(page);
|
||||
// Tell the DevTools session to record performance metrics
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
|
||||
await client.send('Performance.enable');
|
||||
// Go to baseURL
|
||||
await page.goto('/');
|
||||
|
||||
// To to Search Available after Launch
|
||||
await page.locator('input[type="search"]').click();
|
||||
await page.evaluate(() => window.performance.mark("search-available"));
|
||||
// Fill Search input
|
||||
await page.locator('input[type="search"]').fill('Performance Notebook');
|
||||
await page.evaluate(() => window.performance.mark("search-entered"));
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("Performance Notebook")').first().click(),
|
||||
page.evaluate(() => window.performance.mark("click-search-result"))
|
||||
]);
|
||||
|
||||
await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {state: 'hidden'});
|
||||
await page.evaluate(() => window.performance.mark("search-spinner-gone"));
|
||||
|
||||
await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("object-title-appears"));
|
||||
|
||||
await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("notebook-entry-appears"));
|
||||
|
||||
// Click Add new Notebook Entry
|
||||
await page.locator('.c-notebook__drag-area').click();
|
||||
await page.evaluate(() => window.performance.mark("new-notebook-entry-created"));
|
||||
|
||||
// Enter Notebook Entry text
|
||||
await page.locator('div.c-ne__text').last().fill('New Entry');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.evaluate(() => window.performance.mark("new-notebook-entry-filled"));
|
||||
|
||||
//Individual Notebook Entry Search
|
||||
await page.evaluate(() => window.performance.mark("notebook-search-start"));
|
||||
await page.locator('.c-notebook__search >> input').fill('Existing Entry');
|
||||
await page.evaluate(() => window.performance.mark("notebook-search-filled"));
|
||||
await page.waitForSelector('text=Search Results (3)', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
|
||||
await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
|
||||
|
||||
//Clear Search
|
||||
await page.locator('.c-search.c-notebook__search .c-search__clear-input').click();
|
||||
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
|
||||
|
||||
// Hover on Last
|
||||
await page.evaluate(() => window.performance.mark("new-notebook-entry-delete"));
|
||||
await page.locator('div.c-ne__time-and-content').last().hover();
|
||||
await page.locator('button[title="Delete this entry"]').last().click();
|
||||
await page.locator('button:has-text("Ok")').click();
|
||||
await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached'});
|
||||
await page.evaluate(() => window.performance.mark("new-notebook-entry-deleted"));
|
||||
|
||||
//await client.send('HeapProfiler.enable');
|
||||
await client.send('HeapProfiler.collectGarbage');
|
||||
|
||||
let performanceMetrics = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetrics.metrics);
|
||||
});
|
||||
});
|
@ -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,167 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
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 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(),
|
||||
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,18 +42,24 @@ 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');
|
||||
});
|
||||
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
const bgImageLocator = page.locator(backgroundImageSelector);
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
await bgImageLocator.hover();
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
@ -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);
|
||||
const bgImageLocator = page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
|
||||
// zoom in
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
await bgImageLocator.hover();
|
||||
@ -91,50 +101,59 @@ 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);
|
||||
|
||||
});
|
||||
|
||||
test('Can use + - buttons to zoom on the image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
const bgImageLocator = page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
const zoomInBtn = await page.locator('.t-btn-zoom-in');
|
||||
const zoomOutBtn = await page.locator('.t-btn-zoom-out');
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in');
|
||||
const zoomOutBtn = page.locator('.t-btn-zoom-out');
|
||||
const initialBoundingBox = await bgImageLocator.boundingBox();
|
||||
|
||||
await zoomInBtn.click();
|
||||
@ -155,21 +174,27 @@ test.describe('Example Imagery', () => {
|
||||
});
|
||||
|
||||
test('Can use the reset button to reset the image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
const bgImageLocator = 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 zoomInBtn = page.locator('.t-btn-zoom-in');
|
||||
const zoomResetBtn = 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,271 @@ 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 }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||
});
|
||||
|
||||
// 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 to minimum value
|
||||
// FIXME: Update the value to 5000 ms when this bug is fixed.
|
||||
// See: https://github.com/nasa/openmct/issues/5265
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
await page.locator('input[type="number"]').fill('0');
|
||||
|
||||
// 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 = page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
|
||||
// Click previous image button
|
||||
const previousImageButton = 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
|
||||
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 = page.locator('.c-nav--next');
|
||||
await nextImageButton.click();
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Select local clock mode
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').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();
|
||||
|
||||
const imageCount = await page.locator('.c-imagery__thumb').count();
|
||||
await expect.poll(async () => {
|
||||
const newImageCount = await page.locator('.c-imagery__thumb').count();
|
||||
|
||||
return newImageCount;
|
||||
}, {
|
||||
message: "verify that new images still stream in",
|
||||
timeout: 6 * 1000
|
||||
}).toBeGreaterThan(imageCount);
|
||||
|
||||
// Verify selected image is still displayed
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Unpause imagery
|
||||
await page.locator('.pause-play').click();
|
||||
|
||||
//Get background-image url from background-image css prop
|
||||
const backgroundImage = 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);
|
||||
|
||||
let backgroundImageUrl2;
|
||||
await expect.poll(async () => {
|
||||
// Verify next image has updated
|
||||
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
|
||||
|
||||
return backgroundImageUrl2;
|
||||
}, {
|
||||
message: "verify next image has updated",
|
||||
timeout: 6 * 1000
|
||||
}).not.toBe(backgroundImageUrl1);
|
||||
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
|
||||
});
|
||||
|
||||
test.describe('Example imagery thumbnails resize in display layouts', () => {
|
||||
|
||||
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// Click li:has-text("Display Layout")
|
||||
await page.locator('li:has-text("Display Layout")').click();
|
||||
const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]');
|
||||
await displayLayoutTitleField.click();
|
||||
|
||||
await displayLayoutTitleField.fill('Thumbnail Display Layout');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
|
||||
// Click text=Save and Finish Editing
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// Click li:has-text("Example Imagery")
|
||||
await page.locator('li:has-text("Example Imagery")').click();
|
||||
|
||||
const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]');
|
||||
// Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
|
||||
await imageryTitleField.click();
|
||||
|
||||
// Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
|
||||
await imageryTitleField.fill('Thumbnail Example Imagery');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click()
|
||||
]);
|
||||
|
||||
// Edit mode
|
||||
await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click();
|
||||
|
||||
// Click on example imagery to expose toolbar
|
||||
await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click();
|
||||
|
||||
// expect thumbnails not be visible when first added
|
||||
expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
|
||||
|
||||
// Resize the example imagery vertically to change the thumbnail visibility
|
||||
/*
|
||||
The following arbitrary values are added to observe the separate visual
|
||||
conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
|
||||
Specifically, height is set to 50px for small thumbs and 100px for regular
|
||||
*/
|
||||
// Click #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').click();
|
||||
|
||||
// Fill #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').fill('50');
|
||||
|
||||
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
|
||||
await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
|
||||
|
||||
// Resize the example imagery vertically to change the thumbnail visibility
|
||||
// Click #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').click();
|
||||
|
||||
// Fill #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').fill('100');
|
||||
|
||||
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
|
||||
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
198
e2e/tests/plugins/plot/autoscale.e2e.spec.js
Normal file
198
e2e/tests/plugins/plot/autoscale.e2e.spec.js
Normal file
@ -0,0 +1,198 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Testsuite for plot autoscale.
|
||||
*/
|
||||
|
||||
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
|
||||
// be used for all platforms.
|
||||
const test = _test.extend({
|
||||
_autoSnapshotSuffix: [
|
||||
async ({}, use, testInfo) => {
|
||||
testInfo.snapshotSuffix = '';
|
||||
await use();
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
});
|
||||
|
||||
test.use({
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 720
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
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);
|
||||
|
||||
await createSinewaveOverlayPlot(page);
|
||||
|
||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
||||
|
||||
await turnOffAutoscale(page);
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
|
||||
// Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
|
||||
await Promise.all([
|
||||
testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']),
|
||||
new Promise(r => setTimeout(r, 100))
|
||||
.then(() => canvas.screenshot())
|
||||
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
|
||||
]);
|
||||
|
||||
await page.keyboard.down('Alt');
|
||||
|
||||
await canvas.dragTo(canvas, {
|
||||
sourcePosition: {
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
targetPosition: {
|
||||
x: 400,
|
||||
y: 400
|
||||
}
|
||||
});
|
||||
|
||||
await page.keyboard.up('Alt');
|
||||
|
||||
// Ensure the drag worked.
|
||||
await Promise.all([
|
||||
testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
|
||||
new Promise(r => setTimeout(r, 100))
|
||||
.then(() => canvas.screenshot())
|
||||
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} start
|
||||
* @param {string} end
|
||||
*/
|
||||
async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '2022-03-29 22:00:30.000Z') {
|
||||
// Set a specific time range for consistency, otherwise it will change
|
||||
// on every test to a range based on the current time.
|
||||
|
||||
const timeInputs = page.locator('input.c-input--datetime');
|
||||
await timeInputs.first().click();
|
||||
await timeInputs.first().fill(start);
|
||||
|
||||
await timeInputs.nth(1).click();
|
||||
await timeInputs.nth(1).fill(end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createSinewaveOverlayPlot(page) {
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
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();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
await Promise.all([
|
||||
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(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function turnOffAutoscale(page) {
|
||||
// enter edit mode
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
|
||||
// uncheck autoscale
|
||||
await page.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 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
|
||||
*/
|
||||
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) {
|
||||
promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
312
e2e/tests/plugins/plot/logPlot.e2e.spec.js
Normal file
312
e2e/tests/plugins/plot/logPlot.e2e.spec.js
Normal file
@ -0,0 +1,312 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
|
||||
necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Log plot tests', () => {
|
||||
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);
|
||||
await enableLogMode(page);
|
||||
await testLogTicks(page);
|
||||
await disableLogMode(page);
|
||||
await testRegularTicks(page);
|
||||
await enableLogMode(page);
|
||||
await testLogTicks(page);
|
||||
await saveOverlayPlot(page);
|
||||
await testLogTicks(page);
|
||||
//await testLogPlotPixels(page);
|
||||
|
||||
// FIXME: Get rid of the waitForTimeout() and lint warning exception.
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1 * 1000);
|
||||
|
||||
// refresh page and wait for charts and ticks to load
|
||||
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);
|
||||
});
|
||||
|
||||
// Leaving test as 'TODO' for now.
|
||||
// NOTE: Not eligible for community contributions.
|
||||
test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
|
||||
await makeOverlayPlot(page);
|
||||
await enableEditMode(page);
|
||||
await enableLogMode(page);
|
||||
await saveOverlayPlot(page);
|
||||
|
||||
// TODO ...export, delete the overlay, then import it...
|
||||
|
||||
//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...
|
||||
// await testLogPlotPixels(page);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function makeOverlayPlot(page) {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Set a specific time range for consistency, otherwise it will change
|
||||
// on every test to a range based on the current time.
|
||||
|
||||
const timeInputs = page.locator('input.c-input--datetime');
|
||||
await timeInputs.first().click();
|
||||
await timeInputs.first().fill('2022-03-29 22:00:00.000Z');
|
||||
|
||||
await timeInputs.nth(1).click();
|
||||
await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
|
||||
|
||||
// create overlay plot
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
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 saveOverlayPlot(page);
|
||||
|
||||
// create a sinewave generator
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
// set amplitude to 6, offset 4, period 2
|
||||
|
||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6');
|
||||
|
||||
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4');
|
||||
|
||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2');
|
||||
|
||||
// Click OK to make generator
|
||||
|
||||
await Promise.all([
|
||||
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(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testRegularTicks(page) {
|
||||
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');
|
||||
await expect(yTicks.nth(2)).toHaveText('2');
|
||||
await expect(yTicks.nth(3)).toHaveText('4');
|
||||
await expect(yTicks.nth(4)).toHaveText('6');
|
||||
await expect(yTicks.nth(5)).toHaveText('8');
|
||||
await expect(yTicks.nth(6)).toHaveText('10');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testLogTicks(page) {
|
||||
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');
|
||||
await expect(yTicks.nth(2)).toHaveText('-2.00');
|
||||
await expect(yTicks.nth(3)).toHaveText('-1.51');
|
||||
await expect(yTicks.nth(4)).toHaveText('-1.20');
|
||||
await expect(yTicks.nth(5)).toHaveText('-1.00');
|
||||
await expect(yTicks.nth(6)).toHaveText('-0.80');
|
||||
await expect(yTicks.nth(7)).toHaveText('-0.58');
|
||||
await expect(yTicks.nth(8)).toHaveText('-0.40');
|
||||
await expect(yTicks.nth(9)).toHaveText('-0.20');
|
||||
await expect(yTicks.nth(10)).toHaveText('-0.00');
|
||||
await expect(yTicks.nth(11)).toHaveText('0.20');
|
||||
await expect(yTicks.nth(12)).toHaveText('0.40');
|
||||
await expect(yTicks.nth(13)).toHaveText('0.58');
|
||||
await expect(yTicks.nth(14)).toHaveText('0.80');
|
||||
await expect(yTicks.nth(15)).toHaveText('1.00');
|
||||
await expect(yTicks.nth(16)).toHaveText('1.20');
|
||||
await expect(yTicks.nth(17)).toHaveText('1.51');
|
||||
await expect(yTicks.nth(18)).toHaveText('2.00');
|
||||
await expect(yTicks.nth(19)).toHaveText('2.50');
|
||||
await expect(yTicks.nth(20)).toHaveText('2.98');
|
||||
await expect(yTicks.nth(21)).toHaveText('3.50');
|
||||
await expect(yTicks.nth(22)).toHaveText('4.00');
|
||||
await expect(yTicks.nth(23)).toHaveText('4.50');
|
||||
await expect(yTicks.nth(24)).toHaveText('5.31');
|
||||
await expect(yTicks.nth(25)).toHaveText('7.00');
|
||||
await expect(yTicks.nth(26)).toHaveText('8.00');
|
||||
await expect(yTicks.nth(27)).toHaveText('9.00');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enableLogMode(page) {
|
||||
// turn on log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function disableLogMode(page) {
|
||||
// turn off log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} 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 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, 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
|
||||
// likely fail, which is what we want.
|
||||
//
|
||||
// I found these pixels by pausing playwright in debug mode at this
|
||||
// point, and using similar code as below to output the pixel data, then
|
||||
// I logged those pixels here.
|
||||
const expectedBluePixels = [
|
||||
// TODO these pixel sets only work with the first test, but not the second test.
|
||||
|
||||
// [60, 35],
|
||||
// [121, 125],
|
||||
// [156, 377],
|
||||
// [264, 73],
|
||||
// [372, 186],
|
||||
// [576, 73],
|
||||
// [659, 439],
|
||||
// [675, 423]
|
||||
|
||||
[60, 35],
|
||||
[120, 125],
|
||||
[156, 375],
|
||||
[264, 73],
|
||||
[372, 185],
|
||||
[575, 72],
|
||||
[659, 437],
|
||||
[675, 421]
|
||||
];
|
||||
|
||||
// The first canvas in the DOM is the one that has the plot point
|
||||
// icons (canvas 2d), which is the one we are testing. The second
|
||||
// one in the DOM is the WebGL canvas with the line. (Why aren't
|
||||
// they both WebGL?)
|
||||
const canvas = document.querySelector('canvas');
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
for (const pixel of expectedBluePixels) {
|
||||
// XXX Possible optimization: call getImageData only once with
|
||||
// area including all pixels to be tested.
|
||||
const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data;
|
||||
|
||||
// #43b0ffff <-- openmct cyanish-blue with 100% opacity
|
||||
// if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) {
|
||||
if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) {
|
||||
// If any pixel is empty, it means we didn't hit a plot point.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(pixelsMatch).toBe(true);
|
||||
}
|
@ -20,11 +20,12 @@
|
||||
* 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.describe('Time conductor operations', () => {
|
||||
test('validate start time does not exceeds end time', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
@ -67,3 +68,168 @@ 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 }) => {
|
||||
const startOffset = {
|
||||
secs: '23'
|
||||
};
|
||||
|
||||
const endOffset = {
|
||||
secs: '31'
|
||||
};
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Switch to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Set start time offset
|
||||
await setStartOffset(page, startOffset);
|
||||
|
||||
// Verify time was updated on time offset button
|
||||
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
|
||||
|
||||
// Set end time offset
|
||||
await setEndOffset(page, endOffset);
|
||||
|
||||
// Verify time was updated on preceding time offset button
|
||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify that offsets and url params are preserved when switching
|
||||
* between fixed timespan and real-time mode.
|
||||
*/
|
||||
test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
|
||||
const startOffset = {
|
||||
mins: '30',
|
||||
secs: '23'
|
||||
};
|
||||
|
||||
const endOffset = {
|
||||
secs: '01'
|
||||
};
|
||||
|
||||
// Convert offsets to milliseconds
|
||||
const startDelta = (30 * 60 * 1000) + (23 * 1000);
|
||||
const endDelta = (1 * 1000);
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Switch to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Set start time offset
|
||||
await setStartOffset(page, startOffset);
|
||||
|
||||
// Set end time offset
|
||||
await setEndOffset(page, endOffset);
|
||||
|
||||
// Switch to fixed timespan mode
|
||||
await setFixedTimeMode(page);
|
||||
|
||||
// Switch back to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Verify updated start time offset persists after mode switch
|
||||
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
|
||||
|
||||
// Verify updated end time offset persists after mode switch
|
||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
|
||||
|
||||
// Verify url parameters persist after mode switch
|
||||
await page.waitForNavigation();
|
||||
expect(page.url()).toContain(`startDelta=${startDelta}`);
|
||||
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} OffsetValues
|
||||
* @property {string | undefined} hours
|
||||
* @property {string | undefined} mins
|
||||
* @property {string | undefined} secs
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setStartOffset(page, offset) {
|
||||
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
|
||||
await setTimeConductorOffset(page, offset, startOffsetButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setEndOffset(page, offset) {
|
||||
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
|
||||
await setTimeConductorOffset(page, offset, endOffsetButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor to fixed timespan mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setFixedTimeMode(page) {
|
||||
await setTimeConductorMode(page, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor to realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setRealTimeMode(page) {
|
||||
await setTimeConductorMode(page, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
* @param {import('@playwright/test').Locator} offsetButton
|
||||
*/
|
||||
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
|
||||
await offsetButton.click();
|
||||
|
||||
if (hours) {
|
||||
await page.fill('.pr-time-controls__hrs', hours);
|
||||
}
|
||||
|
||||
if (mins) {
|
||||
await page.fill('.pr-time-controls__mins', mins);
|
||||
}
|
||||
|
||||
if (secs) {
|
||||
await page.fill('.pr-time-controls__secs', secs);
|
||||
}
|
||||
|
||||
// Click the check button
|
||||
await page.locator('.icon-check').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor mode to either fixed timespan or realtime mode.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
|
||||
*/
|
||||
async function setTimeConductorMode(page, isFixedTimespan = true) {
|
||||
// Click 'mode' button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Switch time conductor mode
|
||||
if (isFixedTimespan) {
|
||||
await page.locator('data-testid=conductor-modeOption-fixed').click();
|
||||
} else {
|
||||
await page.locator('data-testid=conductor-modeOption-realtime').click();
|
||||
}
|
||||
}
|
||||
|
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 }) => {
|
||||
|
@ -47,7 +47,10 @@ test.beforeEach(async ({ context }) => {
|
||||
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
|
||||
});
|
||||
await context.addInitScript(() => {
|
||||
window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
|
||||
window.__clock = sinon.useFakeTimers({
|
||||
now: 0,
|
||||
shouldAdvanceTime: true
|
||||
}); //Set browser clock to UNIX Epoch
|
||||
});
|
||||
});
|
||||
|
||||
@ -56,8 +59,7 @@ test('Visual - Root and About', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Verify that Create button is actionable
|
||||
const createButtonLocator = page.locator('button:has-text("Create")');
|
||||
await expect(createButtonLocator).toBeEnabled();
|
||||
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
|
||||
|
||||
// Take a snapshot of the Dashboard
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
@ -171,3 +173,24 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => {
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'removed amplitude property value');
|
||||
});
|
||||
|
||||
test('Visual - Save Successful Banner', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
//NOTE Something other than example imagery
|
||||
await page.click('text=Timer');
|
||||
|
||||
// Click text=OK
|
||||
await page.click('text=OK');
|
||||
await page.locator('.c-message-banner__message').hover({ trial: true });
|
||||
await percySnapshot(page, 'Banner message shown');
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await percySnapshot(page, 'Banner message gone');
|
||||
|
||||
});
|
@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import createExampleUser from './exampleUserCreator';
|
||||
|
||||
export default class ExampleUserProvider extends EventEmitter {
|
||||
|
@ -77,7 +77,7 @@
|
||||
|
||||
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
|
||||
|
||||
openmct.install(openmct.plugins.example.Generator());
|
||||
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
|
||||
openmct.install(openmct.plugins.example.ExampleImagery());
|
||||
@ -195,6 +195,9 @@
|
||||
));
|
||||
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
|
||||
openmct.install(openmct.plugins.Timer());
|
||||
openmct.install(openmct.plugins.Timelist());
|
||||
openmct.install(openmct.plugins.BarChart());
|
||||
openmct.install(openmct.plugins.ScatterPlot());
|
||||
openmct.start();
|
||||
</script>
|
||||
</html>
|
||||
|
63
package.json
63
package.json
@ -1,32 +1,30 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.0.2-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.0-beta.76",
|
||||
"@percy/playwright": "1.0.1",
|
||||
"@playwright/test": "1.19.2",
|
||||
"@percy/cli": "1.2.1",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.21.1",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
"@types/jasmine": "^4.0.1",
|
||||
"@types/karma": "^6.3.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"allure-playwright": "2.0.0-beta.15",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "10.2.0",
|
||||
"core-js": "3.21.1",
|
||||
"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",
|
||||
"eslint": "8.11.0",
|
||||
"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",
|
||||
@ -36,45 +34,45 @@
|
||||
"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.15",
|
||||
"karma": "6.3.20",
|
||||
"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",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.33",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lighthouse": "9.5.0",
|
||||
"lighthouse": "9.6.1",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"moment": "2.29.1",
|
||||
"moment-duration-format": "2.2.2",
|
||||
"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": "4.0.0",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"sinon": "13.0.1",
|
||||
"sinon": "14.0.0",
|
||||
"style-loader": "^1.0.1",
|
||||
"uuid": "3.3.3",
|
||||
"uuid": "8.3.2",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "8.2.0",
|
||||
"vue-eslint-parser": "8.3.0",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.68.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-middleware": "5.3.1",
|
||||
"webpack-dev-middleware": "5.3.3",
|
||||
"webpack-hot-middleware": "2.25.1",
|
||||
"webpack-merge": "5.8.0",
|
||||
"zepto": "1.2.0"
|
||||
@ -83,8 +81,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 --max-warnings=0",
|
||||
"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",
|
||||
@ -93,10 +91,12 @@
|
||||
"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 branding default condition timeConductor clock exampleImagery notebook persistence performance",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
|
||||
"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",
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
|
||||
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
|
||||
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
|
||||
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",
|
||||
@ -112,6 +112,9 @@
|
||||
"engines": {
|
||||
"node": ">=14.19.1"
|
||||
},
|
||||
"overrides": {
|
||||
"core-js": "3.21.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
"not IE 11",
|
||||
|
@ -241,9 +241,7 @@ define([
|
||||
this.branding = BrandingAPI.default;
|
||||
|
||||
// Plugins that are installed by default
|
||||
|
||||
this.install(this.plugins.Plot());
|
||||
this.install(this.plugins.Chart());
|
||||
this.install(this.plugins.TelemetryTable.default());
|
||||
this.install(PreviewPlugin.default());
|
||||
this.install(LicensesPlugin.default());
|
||||
|
@ -1,5 +1,6 @@
|
||||
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
|
||||
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
|
||||
import CheckBoxField from './components/controls/CheckBoxField.vue';
|
||||
import Datetime from './components/controls/Datetime.vue';
|
||||
import FileInput from './components/controls/FileInput.vue';
|
||||
import Locator from './components/controls/Locator.vue';
|
||||
@ -7,11 +8,13 @@ import NumberField from './components/controls/NumberField.vue';
|
||||
import SelectField from './components/controls/SelectField.vue';
|
||||
import TextAreaField from './components/controls/TextAreaField.vue';
|
||||
import TextField from './components/controls/TextField.vue';
|
||||
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
export const DEFAULT_CONTROLS_MAP = {
|
||||
'autocomplete': AutoCompleteField,
|
||||
'checkbox': CheckBoxField,
|
||||
'composite': ClockDisplayFormatField,
|
||||
'datetime': Datetime,
|
||||
'file-input': FileInput,
|
||||
@ -19,7 +22,8 @@ export const DEFAULT_CONTROLS_MAP = {
|
||||
'numberfield': NumberField,
|
||||
'select': SelectField,
|
||||
'textarea': TextAreaField,
|
||||
'textfield': TextField
|
||||
'textfield': TextField,
|
||||
'toggleSwitch': ToggleSwitchField
|
||||
};
|
||||
|
||||
export default class FormControl {
|
||||
@ -94,4 +98,3 @@ export default class FormControl {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 }}
|
||||
@ -81,7 +81,7 @@
|
||||
|
||||
<script>
|
||||
import FormRow from "@/api/forms/components/FormRow.vue";
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -79,10 +79,12 @@ export default {
|
||||
rowClass() {
|
||||
let cssClass = this.cssClass;
|
||||
|
||||
if (this.row.required) {
|
||||
cssClass = `${cssClass} req`;
|
||||
if (!this.row.required) {
|
||||
return;
|
||||
}
|
||||
|
||||
cssClass = `${cssClass} req`;
|
||||
|
||||
if (this.visited && this.valid !== undefined) {
|
||||
if (this.valid === true) {
|
||||
cssClass = `${cssClass} valid`;
|
||||
|
55
src/api/forms/components/controls/CheckBoxField.vue
Normal file
55
src/api/forms/components/controls/CheckBoxField.vue
Normal file
@ -0,0 +1,55 @@
|
||||
/*****************************************************************************
|
||||
* 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 shell">
|
||||
<span
|
||||
class="field control"
|
||||
:class="model.cssClass"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isChecked"
|
||||
@input="toggleCheckBox"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import toggleMixin from '../../toggle-check-box-mixin';
|
||||
|
||||
export default {
|
||||
mixins: [toggleMixin],
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isChecked: this.model.value
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -58,7 +58,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateText() {
|
||||
console.log('updateText', this.field);
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: this.field
|
||||
|
62
src/api/forms/components/controls/ToggleSwitchField.vue
Normal file
62
src/api/forms/components/controls/ToggleSwitchField.vue
Normal file
@ -0,0 +1,62 @@
|
||||
/*****************************************************************************
|
||||
* 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 shell">
|
||||
<span
|
||||
class="field control"
|
||||
:class="model.cssClass"
|
||||
>
|
||||
<ToggleSwitch
|
||||
id="switchId"
|
||||
:checked="isChecked"
|
||||
@change="toggleCheckBox"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import toggleMixin from '../../toggle-check-box-mixin';
|
||||
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ToggleSwitch
|
||||
},
|
||||
mixins: [toggleMixin],
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
switchId: `toggleSwitch-${uuid}`,
|
||||
isChecked: this.model.value
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
19
src/api/forms/toggle-check-box-mixin.js
Normal file
19
src/api/forms/toggle-check-box-mixin.js
Normal file
@ -0,0 +1,19 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isChecked: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleCheckBox(event) {
|
||||
this.isChecked = !this.isChecked;
|
||||
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: this.isChecked
|
||||
};
|
||||
|
||||
this.$emit('onChange', data);
|
||||
}
|
||||
}
|
||||
};
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -12,6 +12,7 @@
|
||||
:key="action.name"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
>
|
||||
{{ action.name }}
|
||||
@ -37,6 +38,7 @@
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
>
|
||||
{{ action.name }}
|
||||
|
@ -15,6 +15,7 @@
|
||||
:key="action.name"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
@ -45,6 +46,7 @@
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
|
@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
class InMemorySearchProvider {
|
||||
/**
|
||||
@ -36,13 +36,14 @@ class InMemorySearchProvider {
|
||||
*/
|
||||
this.MAX_CONCURRENT_REQUESTS = 100;
|
||||
/**
|
||||
* If max results is not specified in query, use this as default.
|
||||
*/
|
||||
* If max results is not specified in query, use this as default.
|
||||
*/
|
||||
this.DEFAULT_MAX_RESULTS = 100;
|
||||
|
||||
this.openmct = openmct;
|
||||
|
||||
this.indexedIds = {};
|
||||
this.indexedCompositions = {};
|
||||
this.idsToIndex = [];
|
||||
this.pendingIndex = {};
|
||||
this.pendingRequests = 0;
|
||||
@ -58,7 +59,6 @@ class InMemorySearchProvider {
|
||||
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
|
||||
this.onerror = this.onWorkerError.bind(this);
|
||||
this.startIndexing = this.startIndexing.bind(this);
|
||||
this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
|
||||
|
||||
this.openmct.on('start', this.startIndexing);
|
||||
this.openmct.on('destroy', () => {
|
||||
@ -68,6 +68,9 @@ class InMemorySearchProvider {
|
||||
this.worker.port.onmessageerror = null;
|
||||
this.worker.port.close();
|
||||
}
|
||||
|
||||
this.destroyObservers(this.indexedIds);
|
||||
this.destroyObservers(this.indexedCompositions);
|
||||
});
|
||||
}
|
||||
|
||||
@ -137,7 +140,7 @@ class InMemorySearchProvider {
|
||||
};
|
||||
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
|
||||
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||
const domainObject = await this.openmct.objects.get(identifier);
|
||||
|
||||
return domainObject;
|
||||
}));
|
||||
@ -213,29 +216,52 @@ class InMemorySearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
onMutationOfIndexedObject(domainObject) {
|
||||
onNameMutation(domainObject, name) {
|
||||
const provider = this;
|
||||
provider.index(domainObject.identifier, domainObject);
|
||||
|
||||
domainObject.name = name;
|
||||
provider.index(domainObject);
|
||||
}
|
||||
|
||||
onCompositionMutation(domainObject, composition) {
|
||||
const provider = this;
|
||||
const indexedComposition = domainObject.composition;
|
||||
const identifiersToIndex = composition
|
||||
.filter(identifier => !indexedComposition
|
||||
.some(indexedIdentifier => this.openmct.objects
|
||||
.areIdsEqual([identifier, indexedIdentifier])));
|
||||
|
||||
identifiersToIndex.forEach(identifier => {
|
||||
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass an id and model to the worker to be indexed. If the model has
|
||||
* composition, schedule those ids for later indexing.
|
||||
* Pass a domainObject to the worker to be indexed.
|
||||
* If the object has composition, schedule those ids for later indexing.
|
||||
* Watch for object changes and re-index object and children if so
|
||||
*
|
||||
* @private
|
||||
* @param id a model id
|
||||
* @param model a model
|
||||
* @param domainObject a domainObject
|
||||
*/
|
||||
async index(id, domainObject) {
|
||||
async index(domainObject) {
|
||||
const provider = this;
|
||||
const keyString = this.openmct.objects.makeKeyString(id);
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
if (!this.indexedIds[keyString]) {
|
||||
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
|
||||
this.indexedIds[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'name',
|
||||
this.onNameMutation.bind(this, domainObject)
|
||||
);
|
||||
this.indexedCompositions[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'composition',
|
||||
this.onCompositionMutation.bind(this, domainObject)
|
||||
);
|
||||
}
|
||||
|
||||
this.indexedIds[keyString] = true;
|
||||
|
||||
if ((id.key !== 'ROOT')) {
|
||||
if ((keyString !== 'ROOT')) {
|
||||
if (this.worker) {
|
||||
this.worker.port.postMessage({
|
||||
request: 'index',
|
||||
@ -247,15 +273,12 @@ class InMemorySearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const composition = this.openmct.composition.registry.find(foundComposition => {
|
||||
return foundComposition.appliesTo(domainObject);
|
||||
});
|
||||
const composition = this.openmct.composition.get(domainObject);
|
||||
|
||||
if (composition) {
|
||||
const childIdentifiers = await composition.load(domainObject);
|
||||
childIdentifiers.forEach(function (childIdentifier) {
|
||||
provider.scheduleForIndexing(childIdentifier);
|
||||
});
|
||||
if (composition !== undefined) {
|
||||
const children = await composition.load();
|
||||
|
||||
children.forEach(child => provider.scheduleForIndexing(child.identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,12 +294,12 @@ class InMemorySearchProvider {
|
||||
const provider = this;
|
||||
|
||||
this.pendingRequests += 1;
|
||||
const identifier = await this.openmct.objects.parseKeyString(keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||
const domainObject = await this.openmct.objects.get(keyString);
|
||||
delete provider.pendingIndex[keyString];
|
||||
|
||||
try {
|
||||
if (domainObject) {
|
||||
await provider.index(identifier, domainObject);
|
||||
await provider.index(domainObject);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to index domain object ' + keyString, error);
|
||||
@ -305,9 +328,9 @@ class InMemorySearchProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
localIndexItem(keyString, model) {
|
||||
this.localIndexedItems[keyString] = {
|
||||
type: model.type,
|
||||
@ -347,6 +370,16 @@ class InMemorySearchProvider {
|
||||
};
|
||||
this.onWorkerMessage(eventToReturn);
|
||||
}
|
||||
|
||||
destroyObservers(observers) {
|
||||
Object.entries(observers).forEach(([keyString, unobserve]) => {
|
||||
if (typeof unobserve === 'function') {
|
||||
unobserve();
|
||||
}
|
||||
|
||||
delete observers[keyString];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default InMemorySearchProvider;
|
||||
|
@ -105,13 +105,18 @@ describe("The Object API Search Function", () => {
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
const defaultObjectProvider = openmct.objects.getProvider({
|
||||
key: '',
|
||||
namespace: ''
|
||||
});
|
||||
openmct.objects.addProvider('foo', defaultObjectProvider);
|
||||
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
|
||||
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
|
||||
|
||||
openmct.on('start', async () => {
|
||||
mockIdentifier1 = {
|
||||
key: 'some-object',
|
||||
namespace: 'some-namespace'
|
||||
namespace: 'foo'
|
||||
};
|
||||
mockDomainObject1 = {
|
||||
type: 'clock',
|
||||
@ -120,7 +125,7 @@ describe("The Object API Search Function", () => {
|
||||
};
|
||||
mockIdentifier2 = {
|
||||
key: 'some-other-object',
|
||||
namespace: 'some-namespace'
|
||||
namespace: 'foo'
|
||||
};
|
||||
mockDomainObject2 = {
|
||||
type: 'clock',
|
||||
@ -129,16 +134,16 @@ describe("The Object API Search Function", () => {
|
||||
};
|
||||
mockIdentifier3 = {
|
||||
key: 'yet-another-object',
|
||||
namespace: 'some-namespace'
|
||||
namespace: 'foo'
|
||||
};
|
||||
mockDomainObject3 = {
|
||||
type: 'clock',
|
||||
name: 'redBear',
|
||||
identifier: mockIdentifier3
|
||||
};
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
|
||||
done();
|
||||
});
|
||||
openmct.startHeadless();
|
||||
@ -175,9 +180,9 @@ describe("The Object API Search Function", () => {
|
||||
beforeEach(async () => {
|
||||
openmct.objects.inMemorySearchProvider.worker = null;
|
||||
// reindex locally
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
|
||||
});
|
||||
it("calls local search", () => {
|
||||
openmct.objects.search('foo');
|
||||
|
@ -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());
|
||||
@ -172,6 +168,7 @@ export class TelemetryCollection extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
_processNewTelemetry(telemetryData) {
|
||||
performance.mark('tlm:process:start');
|
||||
if (telemetryData === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -266,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;
|
||||
@ -305,7 +306,6 @@ export class TelemetryCollection extends EventEmitter {
|
||||
if (added.length > 0) {
|
||||
this.emit('add', added);
|
||||
}
|
||||
|
||||
} else {
|
||||
// user bounds change, reset
|
||||
this._reset();
|
||||
@ -325,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);
|
||||
|
||||
@ -352,6 +356,7 @@ export class TelemetryCollection extends EventEmitter {
|
||||
* @todo handle subscriptions more granually
|
||||
*/
|
||||
_reset() {
|
||||
performance.mark('tlm:reset');
|
||||
this.boundedTelemetry = [];
|
||||
this.futureBuffer = [];
|
||||
|
||||
@ -400,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();
|
||||
});
|
||||
});
|
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.';
|
@ -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);
|
||||
|
@ -33,7 +33,7 @@ function replaceDotsWithUnderscores(filename) {
|
||||
|
||||
import {saveAs} from 'saveAs';
|
||||
import html2canvas from 'html2canvas';
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
class ImageExporter {
|
||||
constructor(openmct) {
|
||||
@ -51,7 +51,7 @@ class ImageExporter {
|
||||
const overlays = this.openmct.overlays;
|
||||
const dialog = overlays.dialog({
|
||||
iconClass: 'info',
|
||||
message: 'Caputuring an image',
|
||||
message: 'Capturing image, please wait...',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Cancel',
|
||||
|
@ -52,7 +52,6 @@ export default (agent, document) => {
|
||||
if (agent.isMobile()) {
|
||||
const mediaQuery = window.matchMedia("(orientation: landscape)");
|
||||
function eventHandler(event) {
|
||||
console.log("changed");
|
||||
if (event.matches) {
|
||||
body.classList.remove("portrait");
|
||||
body.classList.add("landscape");
|
||||
|
@ -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';
|
@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import TelemetryCriterion from "./criterion/TelemetryCriterion";
|
||||
import { evaluateResults } from './utils/evaluator';
|
||||
import { getLatestTimestamp } from './utils/time';
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
import Condition from "./Condition";
|
||||
import { getLatestTimestamp } from './utils/time';
|
||||
import uuid from "uuid";
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
export default class ConditionManager extends EventEmitter {
|
||||
|
@ -214,7 +214,7 @@
|
||||
import Criterion from './Criterion.vue';
|
||||
import ConditionDescription from "./ConditionDescription.vue";
|
||||
import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -23,7 +23,7 @@ import ConditionSetViewProvider from './ConditionSetViewProvider.js';
|
||||
import ConditionSetCompositionPolicy from "./ConditionSetCompositionPolicy";
|
||||
import ConditionSetMetadataProvider from './ConditionSetMetadataProvider';
|
||||
import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider';
|
||||
import uuid from "uuid";
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export default function ConditionPlugin() {
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -73,7 +73,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import SubobjectView from './SubobjectView.vue';
|
||||
import TelemetryView from './TelemetryView.vue';
|
||||
import BoxView from './BoxView.vue';
|
||||
|
@ -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);
|
||||
|
@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
/**
|
||||
* This class encapsulates the process of duplicating/copying a domain object
|
||||
|
@ -22,7 +22,7 @@
|
||||
import JSONExporter from '/src/exporters/JSONExporter.js';
|
||||
|
||||
import _ from 'lodash';
|
||||
import uuid from "uuid";
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export default class ExportAsJSONAction {
|
||||
constructor(openmct) {
|
||||
|
@ -57,7 +57,7 @@
|
||||
/>
|
||||
|
||||
<drop-hint
|
||||
:key="i"
|
||||
:key="'hint-' + i"
|
||||
class="c-fl-frame__drop-hint"
|
||||
:index="i"
|
||||
:allow-drop="allowDrop"
|
||||
@ -66,7 +66,7 @@
|
||||
|
||||
<resize-handle
|
||||
v-if="(i !== frames.length - 1)"
|
||||
:key="i"
|
||||
:key="'handle-' + i"
|
||||
:index="i"
|
||||
:orientation="rowsLayout ? 'horizontal' : 'vertical'"
|
||||
:is-editing="isEditing"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user