Compare commits

...

28 Commits

Author SHA1 Message Date
5de5ff347c Merge branch 'release/2.0.5' into persistence-errors 2022-07-12 11:44:21 -07:00
2bfe632e7e Fix all of the e2e tests (#5477)
* Fix timer test

* be explicit about the warnings text

* add full suite to CI to enable CircleCI Checks

* add back in devtool=false for CI env so firefox tests run

* add framework suite

* Don't install webpack HMR in CI

* Fix playwright version installs

* exclude HMR if running tests in any environment

- use NODE_ENV=TEST to exclude webpack HMR

- deparameterize some of the playwright configs

* use lower-case 'test'

* timer hover fix

* conditionally skip for firefox due to missing console events

* increase timeouts to give time for mutation

* no need to close save banner

* remove devtool setting

* revert

* update snapshots

* disable video to save some resources

* use one worker

* more timeouts :)

* Remove `browser.close()` and `page.close()` as it was breaking other tests

* Remove unnecessary awaits and fix func call syntax

* Fix image reset test

* fix restrictedNotebook tests

* revert playwright-ci.config settings

* increase timeout for polling imagery test

* remove unnecessary waits

* disable notebook lock test for chrome-beta as its unreliable

- remove some unnecessary 'wait for save banner' logic

- remove unused await

- mark imagery test as slow in chrome-beta

* LINT!! *shakes fist*

* don't run full e2e suite per commit

* disable video in all configs

* add flakey zoom comment

* exclude webpack HMR in non-development modes

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-07-12 11:29:38 -07:00
4ac39a3990 Do not pass onPartialResponse option on to upstream telemetry (#5486) 2022-07-12 11:23:30 -07:00
169d148c58 Revert some changes to minimize risk 2022-07-12 09:08:59 -07:00
40d2f3295f Better handling of persistence errors in general, and conflict errors specifically 2022-07-12 09:04:34 -07:00
0e707150e0 get rid of root (#5483) 2022-07-11 20:02:40 -05:00
2540d96617 Clear data when time bounds are changed (#5482)
* Clear data when time bounds are changed
Also react to clear data action
Ensure that the yKey is set to 'none' if there is no range with array Values

* Refactor trace updates to a method
2022-07-11 17:29:59 -05:00
1c8784fec5 Stacked plot interceptor rename (#5468)
* Rename stacked plot interceptor and move to folder

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-11 17:30:26 +00:00
2943d2b6ec Release 2.0.5 UI and Gauge fixes (#5470)
* Various UI fixes
- Tweak to Gauge properties form for clarity and usability.
- Fix Gauge 'dial' type not obeying "Show units" property setting, closes #5325.
- Tweaks to Operator Status UI label and layout for clarity.
- Changed name and description of Graph object for clarity and consistency.
- Fixed CSS classing that was coloring Export menu items text incorrectly.
- Fixed icon-to-text vertical alignment in `.c-object-label`.
- Fix for broken layout in imagery local controls (brightness, layers, magnification).

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-11 10:26:10 -07:00
4246a597a9 Fix shelved alarms (#5479)
* Fix the logic around shelved alarms

* Remove application router listener
2022-07-11 10:08:34 -07:00
0af7965021 Fix couchdb no response (#5474)
* Update the creation date only when the document is created for the first time

* If there is no response from a bulk get, couch db has issues

* Check the response - if it's null, don't apply interceptors
2022-07-10 10:50:23 -07:00
e9c0909415 Use timeKey for time comparison (#5471) 2022-07-08 22:05:31 +00:00
0f0a3dc48f Remove performance marks (#5465)
* Remove performance marks

* Retain performance mark in view large. It doesn't happen very often and it's needed for an automated performance test
2022-07-08 18:27:55 +00:00
4c82680b87 Added plot interceptor for missing series config (#5422)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-07-08 10:47:05 -07:00
c4734b8ad6 Lock model (#5457)
* Lock event Model to prevent reactification

* de-reactify all the things

* Make API properties writable to allow test mocks to override them

* Fix merge conflict
2022-07-08 09:29:53 -07:00
9786ff5de4 [Remote Clock] Fix requestInterceptor typo (#5462)
* Fix typo in telemetry request interceptor

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-08 09:20:35 -07:00
437154a5c0 removing the call for default import now that TelemetryAPI is an ES6 class (#5461) 2022-07-08 09:16:17 -07:00
2bd38dab9f Fix for missing object for LADTableSet (#5458)
* Handle missing object errors for display layouts

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-08 14:05:34 +00:00
063df721ae [Remote Clock] Wait for first tick and recalculate historical request bounds (#5433)
* Updated to ES6 class
* added request intercept functionality to telemetry api, added a request interceptor for remote clock
* add remoteClock e2e test stub

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-07 23:51:12 +00:00
a09db30b32 Allow endpoints with a single enum metadata value in Bar/Line graphs (#5443)
* If there is only 1 metadata value, set yKey to none. Also, fix bug for determining the name of a metadata value
* Update tests for enum metadata values

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-07 16:44:09 -07:00
9d89bdd6d3 [Static Root] Static Root Plugin not loading (#5455)
* Log if hitting falsy leafValue

* Add some logging

* Remove logs and specify null/undefined

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-07-07 15:00:33 -07:00
ed9ca2829b fix pathing (#5452)
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-07-07 14:33:05 -07:00
eacbac6aad Fix for Fault Management Visual Bugs (#5376)
* Closes #5365
* General visual improvements

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-07 20:56:54 +00:00
69153fe8f0 [CouchDB] Always subscribe to the CouchDB changes feed (#5434)
* Add unknown state, remove maintenance state

* Handle all CouchDB status codes

- Set unknown status if we receive an unhandled code

* Include status code in error messages

* SharedWorker can send unknown status

* Add test for unknown status

* Always subscribe to CouchDB changes feed

- Always subscribe to the CouchDB changes feed, even if there are no observable objects, since we are also checking the status of CouchDB via this feed.

* Update indicator status if not using SharedWorker

* Start listening to changes feed on first request

* fix test

* adjust test to hopefully avoid race condition

* lint

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
2022-07-07 14:30:30 -05:00
51196530fd No gauge (#5451)
* Installed gauge plugin by default
* Make gauge part of standard install in e2e suite and add restrictednotebook

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-07 11:04:50 -07:00
fefa46ce7e Debounce status summary (#5448)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-07-07 09:34:31 -07:00
e08ab8ef24 fix sourcemaps (#5373)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-07-07 08:19:35 -07:00
7011877e64 [Telemetry Collections] Respect "Latest" Strategy Option (#5421)
* Respect latest strategy in Telemetry Collections to limit potential memory growth.
2022-07-06 16:53:41 -07:00
74 changed files with 1414 additions and 959 deletions

View File

@ -30,7 +30,8 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: '16' node-version: '16'
- run: npx playwright@1.21.1 install - run: npx playwright@1.23.0 install
- run: npx playwright install chrome-beta
- run: npm install - run: npm install
- run: npm run test:e2e:full - run: npm run test:e2e:full
- name: Archive test results - name: Archive test results

View File

@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: '16' node-version: '16'
- run: npx playwright@1.21.1 install - run: npx playwright@1.23.0 install
- run: npm install - run: npm install
- name: Run the e2e visual tests - name: Run the e2e visual tests
run: npm run test:e2e:visual run: npm run test:e2e:visual

31
app.js
View File

@ -12,6 +12,7 @@ const express = require('express');
const app = express(); const app = express();
const fs = require('fs'); const fs = require('fs');
const request = require('request'); const request = require('request');
const __DEV__ = process.env.NODE_ENV === 'development';
// Defaults // Defaults
options.port = options.port || options.p || 8080; options.port = options.port || options.p || 8080;
@ -49,14 +50,18 @@ class WatchRunPlugin {
} }
const webpack = require('webpack'); const webpack = require('webpack');
const webpackConfig = process.env.CI ? require('./webpack.coverage.js') : require('./webpack.dev.js'); let webpackConfig;
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); if (__DEV__) {
webpackConfig.plugins.push(new WatchRunPlugin()); webpackConfig = require('./webpack.dev');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.entry.openmct = [ webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true', 'webpack-hot-middleware/client?reload=true',
webpackConfig.entry.openmct webpackConfig.entry.openmct
]; ];
webpackConfig.plugins.push(new WatchRunPlugin());
} else {
webpackConfig = require('./webpack.coverage');
}
const compiler = webpack(webpackConfig); const compiler = webpack(webpackConfig);
@ -68,10 +73,12 @@ app.use(require('webpack-dev-middleware')(
} }
)); ));
app.use(require('webpack-hot-middleware')( if (__DEV__) {
compiler, app.use(require('webpack-hot-middleware')(
{} compiler,
)); {}
));
}
// Expose index.html for development users. // Expose index.html for development users.
app.get('/', function (req, res) { app.get('/', function (req, res) {

View File

@ -4,6 +4,8 @@
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const { devices } = require('@playwright/test'); const { devices } = require('@playwright/test');
const MAX_FAILURES = 5;
const NUM_WORKERS = 2;
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
@ -12,20 +14,20 @@ const config = {
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000, timeout: 60 * 1000,
webServer: { webServer: {
command: 'npm run start', command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: !process.env.CI reuseExistingServer: false
}, },
maxFailures: process.env.CI ? 5 : undefined, //Limits failures to 5 to reduce CI Waste maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
workers: 2, //Limit to 2 for CircleCI Agent workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
use: { use: {
baseURL: 'http://localhost:8080/', baseURL: 'http://localhost:8080/',
headless: true, headless: true,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'only-on-failure', screenshot: 'only-on-failure',
trace: 'on-first-retry', trace: 'on-first-retry',
video: 'on-first-retry' video: 'off'
}, },
projects: [ projects: [
{ {

View File

@ -12,10 +12,10 @@ const config = {
testIgnore: '**/*.perf.spec.js', testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000, timeout: 30 * 1000,
webServer: { webServer: {
command: 'npm run start', command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 120 * 1000, timeout: 120 * 1000,
reuseExistingServer: !process.env.CI reuseExistingServer: true
}, },
workers: 1, workers: 1,
use: { use: {
@ -25,7 +25,7 @@ const config = {
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'only-on-failure', screenshot: 'only-on-failure',
trace: 'retain-on-failure', trace: 'retain-on-failure',
video: 'retain-on-failure' video: 'off'
}, },
projects: [ projects: [
{ {

View File

@ -2,6 +2,8 @@
// playwright.config.js // playwright.config.js
// @ts-check // @ts-check
const CI = process.env.CI === 'true';
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
retries: 1, //Only for debugging purposes because trace is enabled only on first retry retries: 1, //Only for debugging purposes because trace is enabled only on first retry
@ -9,15 +11,15 @@ const config = {
timeout: 60 * 1000, timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker workers: 1, //Only run in serial with 1 worker
webServer: { webServer: {
command: 'npm run start', command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: !process.env.CI reuseExistingServer: !CI
}, },
use: { use: {
browserName: "chromium", browserName: "chromium",
baseURL: 'http://localhost:8080/', baseURL: 'http://localhost:8080/',
headless: Boolean(process.env.CI), //Only if running locally headless: CI, //Only if running locally
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'off', screenshot: 'off',
trace: 'on-first-retry', trace: 'on-first-retry',

View File

@ -9,7 +9,7 @@ const config = {
timeout: 90 * 1000, timeout: 90 * 1000,
workers: 1, // visual tests should never run in parallel due to test pollution workers: 1, // visual tests should never run in parallel due to test pollution
webServer: { webServer: {
command: 'npm run start', command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: !process.env.CI reuseExistingServer: !process.env.CI
@ -21,7 +21,7 @@ const config = {
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'on', screenshot: 'on',
trace: 'off', trace: 'off',
video: 'on' video: 'off'
}, },
reporter: [ reporter: [
['list'], ['list'],

View File

@ -36,7 +36,7 @@ test.describe('Branding tests', () => {
await page.click('.l-shell__app-logo'); await page.click('.l-shell__app-logo');
// Verify that the NASA Logo Appears // Verify that the NASA Logo Appears
await expect(await page.locator('.c-about__image')).toBeVisible(); await expect(page.locator('.c-about__image')).toBeVisible();
// Modify the Build information in 'about' Modal // Modify the Build information in 'about' Modal
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info'); const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');

View 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.
*****************************************************************************/
/*
This test suite is dedicated to testing our use of the playwright framework as it
relates to how we've extended it (i.e. ./e2e/fixtures.js) and assumptions made in our dev environment
(app.js and ./e2e/webpack-dev-middleware.js)
*/
const { test } = require('../fixtures.js');
test.describe('fixtures.js tests', () => {
test('Verify that tests fail if console.error is thrown', async ({ page }) => {
test.fail();
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Verify that ../fixtures.js detects console log errors
await Promise.all([
page.evaluate(() => console.error('This should result in a failure')),
page.waitForEvent('console') // always wait for the event to happen while triggering it!
]);
});
test('Verify that tests pass if console.warn is thrown', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Verify that ../fixtures.js detects console log errors
await Promise.all([
page.evaluate(() => console.warn('This should result in a pass')),
page.waitForEvent('console') // always wait for the event to happen while triggering it!
]);
});
});

View File

@ -55,16 +55,13 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' });
//Set object identifier from url //Set object identifier from url
conditionSetUrl = await page.url(); conditionSetUrl = page.url();
console.log('conditionSetUrl ' + conditionSetUrl); console.log('conditionSetUrl ' + conditionSetUrl);
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0]; getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0];
console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl); console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
await page.close();
});
test.afterAll(async ({ browser }) => {
await browser.close();
}); });
//Load localStorage for subsequent tests //Load localStorage for subsequent tests
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
//Begin suite of tests again localStorage //Begin suite of tests again localStorage
@ -76,7 +73,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector //Assertions on loaded Condition Set in Inspector
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy; expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
//Reload Page //Reload Page
await Promise.all([ await Promise.all([
@ -87,7 +84,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//Re-verify after reload //Re-verify after reload
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector //Assertions on loaded Condition Set in Inspector
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy; expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
}); });
test('condition set object can be modified on @localStorage', async ({ page }) => { test('condition set object can be modified on @localStorage', async ({ page }) => {
@ -113,18 +110,18 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Inspector properties // Verify Inspector properties
// Verify Inspector has updated Name property // Verify Inspector has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property // Verify Inspector Details has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety // Verify Tree reflects updated Name proprety
// Expand Tree // Expand Tree
await page.locator('text=Open MCT My Items >> span >> nth=3').click(); await page.locator('text=Open MCT My Items >> span >> nth=3').click();
// Verify Condition Set Object is renamed in Tree // Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property // Verify Search Tree reflects renamed Name property
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page //Reload Page
await Promise.all([ await Promise.all([
@ -137,18 +134,18 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Inspector properties // Verify Inspector properties
// Verify Inspector has updated Name property // Verify Inspector has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property // Verify Inspector Details has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety // Verify Tree reflects updated Name proprety
// Expand Tree // Expand Tree
await page.locator('text=Open MCT My Items >> span >> nth=3').click(); await page.locator('text=Open MCT My Items >> span >> nth=3').click();
// Verify Condition Set Object is renamed in Tree // Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property // Verify Search Tree reflects renamed Name property
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
}); });
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => { test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
//Navigate to baseURL //Navigate to baseURL

View File

@ -172,7 +172,8 @@ test.describe('Example Imagery Object', () => {
}); });
test('Can use the reset button to reset the image', async ({ page }) => { test('Can use the reset button to reset the image', async ({ page }, testInfo) => {
test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta");
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await page.locator(backgroundImageSelector).hover({trial: true});
@ -191,16 +192,17 @@ test.describe('Example Imagery Object', () => {
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomResetBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); // FIXME: The zoom is flakey, sometimes not returning to original dimensions
// https://github.com/nasa/openmct/issues/5491
await expect.poll(async () => {
await zoomResetBtn.click();
const boundingBox = await page.locator(backgroundImageSelector).boundingBox();
const resetBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); return boundingBox;
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); }, {
expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); timeout: 10 * 1000
}).toEqual(initialBoundingBox);
expect.soft(resetBoundingBox.height).toEqual(initialBoundingBox.height);
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
}); });
test('Using the zoom features does not pause telemetry', async ({ page }) => { test('Using the zoom features does not pause telemetry', async ({ page }) => {

View File

@ -35,7 +35,7 @@ test.describe('Restricted Notebook', () => {
}); });
test('Can be renamed @addInit', async ({ page }) => { test('Can be renamed @addInit', async ({ page }) => {
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
}); });
test('Can be deleted if there are no locked pages @addInit', async ({ page }) => { test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
@ -52,16 +52,15 @@ test.describe('Restricted Notebook', () => {
// Click Remove Text // Click Remove Text
await page.locator('text=Remove').click(); await page.locator('text=Remove').click();
//Wait until Save Banner is gone // Click 'OK' on confirmation window and wait for save banner to appear
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('text=OK').click(),
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
await page.locator('.c-message-banner__close-button').click();
// has been deleted // has been deleted
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0); expect(await restrictedNotebookTreeObject.count()).toEqual(0);
}); });
test('Can be locked if at least one page has one entry @addInit', async ({ page }) => { test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
@ -69,7 +68,7 @@ test.describe('Restricted Notebook', () => {
await enterTextEntry(page); await enterTextEntry(page);
const commitButton = page.locator('button:has-text("Commit Entries")'); const commitButton = page.locator('button:has-text("Commit Entries")');
expect.soft(await commitButton.count()).toEqual(1); expect(await commitButton.count()).toEqual(1);
}); });
}); });
@ -81,11 +80,17 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
await enterTextEntry(page); await enterTextEntry(page);
await lockPage(page); await lockPage(page);
// FIXME: Give ample time for the mutation to happen
// https://github.com/nasa/openmct/issues/5409
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1 * 1000);
// open sidebar // open sidebar
await page.locator('button.c-notebook__toggle-nav-button').click(); await page.locator('button.c-notebook__toggle-nav-button').click();
}); });
test('Locked page should now be in a locked state @addInit', async ({ page }) => { test('Locked page should now be in a locked state @addInit', async ({ page }, testInfo) => {
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
// main lock message on page // main lock message on page
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed'); const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
expect.soft(await lockMessage.count()).toEqual(1); expect.soft(await lockMessage.count()).toEqual(1);
@ -96,11 +101,9 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
// no way to remove a restricted notebook with a locked page // no way to remove a restricted notebook with a locked page
await openContextMenuRestrictedNotebook(page); await openContextMenuRestrictedNotebook(page);
const menuOptions = page.locator('.c-menu ul'); const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).not.toContainText('Remove'); await expect(menuOptions).not.toContainText('Remove');
}); });
test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => { test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => {
@ -139,7 +142,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
// deleted page, should no longer exist // deleted page, should no longer exist
const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`); const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
expect.soft(await deletedPageElement.count()).toEqual(0); expect(await deletedPageElement.count()).toEqual(0);
}); });
}); });
@ -155,7 +158,7 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu'); const embedMenu = page.locator('body >> .c-menu');
await expect.soft(embedMenu).toContainText('Remove This Embed'); await expect(embedMenu).toContainText('Remove This Embed');
}); });
test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => { test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
@ -164,7 +167,7 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu'); const embedMenu = page.locator('body >> .c-menu');
await expect.soft(embedMenu).not.toContainText('Remove This Embed'); await expect(embedMenu).not.toContainText('Remove This Embed');
}); });
}); });
@ -232,28 +235,18 @@ async function lockPage(page) {
await commitButton.click(); await commitButton.click();
//Wait until Lock Banner is visible //Wait until Lock Banner is visible
await Promise.all([ await page.locator('text=Lock Page').click();
page.locator('text=Lock Page').click(),
page.waitForSelector('.c-message-banner__message')
]);
// Close Lock Banner
await page.locator('.c-message-banner__close-button').click();
//artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1 * 1000);
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function openContextMenuRestrictedNotebook(page) { async function openContextMenuRestrictedNotebook(page) {
// Click text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree) const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3);
await page.locator('text=Open MCT My Items >> span').nth(3).click(); const className = await myItemsFolder.getAttribute('class');
if (!className.includes('c-disclosure-triangle--expanded')) {
//artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409 await myItemsFolder.click();
// eslint-disable-next-line playwright/no-wait-for-timeout }
await page.waitForTimeout(1 * 1000);
// Click a:has-text("Unnamed CUSTOM_NAME") // Click a:has-text("Unnamed CUSTOM_NAME")
await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({ await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -28,11 +28,12 @@ const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test'); const { expect } = require('@playwright/test');
test.describe('Handle missing object for plots', () => { test.describe('Handle missing object for plots', () => {
test('Displays empty div for missing stacked plot item', async ({ page }) => { test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed');
const errorLogs = []; const errorLogs = [];
page.on("console", (message) => { page.on("console", (message) => {
if (message.type() === 'warning') { if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
errorLogs.push(message.text()); errorLogs.push(message.text());
} }
}); });
@ -71,7 +72,7 @@ test.describe('Handle missing object for plots', () => {
//Check that there is only one stacked item plot with a plot, the missing one will be empty //Check that there is only one stacked item plot with a plot, the missing one will be empty
await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1); await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1);
//Verify that console.warn is thrown //Verify that console.warn is thrown
await expect(errorLogs).toHaveLength(1); expect(errorLogs).toHaveLength(1);
}); });
}); });
@ -94,10 +95,6 @@ async function makeStackedPlot(page) {
page.waitForSelector('.c-message-banner__message') 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 stacked plot // save the stacked plot
await saveStackedPlot(page); await saveStackedPlot(page);
@ -155,7 +152,4 @@ async function createSineWaveGenerator(page) {
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') 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'});
} }

View File

@ -0,0 +1,41 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const { test } = require('../../../fixtures.js');
// eslint-disable-next-line no-unused-vars
const { expect } = require('@playwright/test');
test.describe('Remote Clock', () => {
// eslint-disable-next-line require-await
test.fixme('blocks historical requests until first tick is received', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5221'
});
// addInitScript to with remote clock
// Switch time conductor mode to 'remote clock'
// Navigate to telemetry
// Verify that the plot renders historical data within the correct bounds
// Refresh the page
// Verify again that the plot renders historical data within the correct bounds
});
});

View File

@ -24,7 +24,7 @@ const { test } = require('../../../fixtures');
const { expect } = require('@playwright/test'); const { expect } = require('@playwright/test');
test.describe('Telemetry Table', () => { test.describe('Telemetry Table', () => {
test('unpauses when paused by button and user changes bounds', async ({ page }) => { test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5113' description: 'https://github.com/nasa/openmct/issues/5113'
@ -71,25 +71,34 @@ test.describe('Telemetry Table', () => {
]); ]);
// Click pause button // Click pause button
const pauseButton = await page.locator('button.c-button.icon-pause'); const pauseButton = page.locator('button.c-button.icon-pause');
await pauseButton.click(); await pauseButton.click();
const tableWrapper = await page.locator('div.c-table-wrapper'); const tableWrapper = page.locator('div.c-table-wrapper');
await expect(tableWrapper).toHaveClass(/is-paused/); await expect(tableWrapper).toHaveClass(/is-paused/);
// Arbitrarily change end date to some time in the future // Subtract 5 minutes from the current end bound datetime and set it
const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1); const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
await endTimeInput.click(); await endTimeInput.click();
let endDate = await endTimeInput.inputValue(); let endDate = await endTimeInput.inputValue();
endDate = new Date(endDate); endDate = new Date(endDate);
endDate.setUTCDate(endDate.getUTCDate() + 1);
endDate = endDate.toISOString().replace(/T.*/, ''); endDate.setUTCMinutes(endDate.getUTCMinutes() - 5);
endDate = endDate.toISOString().replace(/T/, ' ');
await endTimeInput.fill(''); await endTimeInput.fill('');
await endTimeInput.fill(endDate); await endTimeInput.fill(endDate);
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
await expect(tableWrapper).not.toHaveClass(/is-paused/); await expect(tableWrapper).not.toHaveClass(/is-paused/);
// Get the most recent telemetry date
const latestTelemetryDate = await page.locator('table.c-telemetry-table__body > tbody > tr').last().locator('td').nth(1).getAttribute('title');
// Verify that it is <= our new end bound
const latestMilliseconds = Date.parse(latestTelemetryDate);
const endBoundMilliseconds = Date.parse(endDate);
expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
}); });
}); });

View File

@ -140,6 +140,7 @@ async function triggerTimer3dotMenuAction(page, action) {
* @param {TimerViewAction} action * @param {TimerViewAction} action
*/ */
async function triggerTimerViewAction(page, action) { async function triggerTimerViewAction(page, action) {
await page.locator('.c-timer').hover({trial: true});
const buttonTitle = buttonTitleFromAction(action); const buttonTitle = buttonTitleFromAction(action);
await page.click(`button[title="${buttonTitle}"]`); await page.click(`button[title="${buttonTitle}"]`);
assertTimerStateAfterAction(page, action); assertTimerStateAfterAction(page, action);

View File

@ -1,22 +0,0 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "mct-tree-expanded",
"value": "[]"
},
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1656473493306,\"end\":1656475293306},{\"start\":1655769110258,\"end\":1655770910258},{\"start\":1652301954635,\"end\":1652303754635}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"18ba28bf-152e-4e0f-9b9c-638fb2ade0c3\",\"namespace\":\"\"},{\"key\":\"fa64bd6c-9351-4d94-a54e-e062a93be3b6\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1656475294042,\"modified\":1656475294042},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"18ba28bf-152e-4e0f-9b9c-638fb2ade0c3\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"18ba28bf-152e-4e0f-9b9c-638fb2ade0c3\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"43cfb4b1-348c-43c0-a681-c4cf53b5335f\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1655770911020,\"location\":\"mine\",\"persisted\":1655770911020},\"fa64bd6c-9351-4d94-a54e-e062a93be3b6\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"fa64bd6c-9351-4d94-a54e-e062a93be3b6\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"26739ce0-9a56-466c-91dd-f08bd9bfc9d7\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1656475294040,\"location\":\"mine\",\"persisted\":1656475294040}}"
}
]
}
]
}

View File

@ -38,6 +38,8 @@ const sinon = require('sinon');
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
const CUSTOM_NAME = 'CUSTOM_NAME';
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 // Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
// Will replace with cy.clock() equivalent // Will replace with cy.clock() equivalent
test.beforeEach(async ({ context }) => { test.beforeEach(async ({ context }) => {
@ -52,21 +54,23 @@ test.beforeEach(async ({ context }) => {
}); });
}); });
test('Visual - Default Gauge is correct @addInit', async ({ page }) => { test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../plugins/gauge', './addInitGauge.js') }); await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addInitRestrictedNotebook.js') });
//Go to baseURL //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button //Click the Create button
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
// Click text=CUSTOM_NAME
await page.click(`text=${CUSTOM_NAME}`);
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
await page.click('text=Gauge'); // Take a snapshot of the newly created CUSTOM_NAME notebook
await page.click('text=OK');
// Take a snapshot of the newly created Gauge object
await page.waitForTimeout(VISUAL_GRACE_PERIOD); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Gauge'); await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME');
}); });

View File

@ -211,3 +211,22 @@ test('Visual - Display Layout Icon is correct', async ({ page }) => {
await percySnapshot(page, 'Display Layout Create Menu'); await percySnapshot(page, 'Display Layout Create Menu');
}); });
test('Visual - Default Gauge is correct', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
await page.click('text=Gauge');
await page.click('text=OK');
// Take a snapshot of the newly created Gauge object
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Gauge');
});

View File

@ -52,9 +52,6 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
//Wait for Save Banner to appear1 //Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message') 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) // 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=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
@ -69,18 +66,12 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
//Add a 5000 ms Delay //Add a 5000 ms Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("Overlay Plot")');
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('text=OK').click(),
//Wait for Save Banner to appear1 //Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message') 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 // focus the overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click(); await page.locator('text=Open MCT My Items >> span').nth(3).click();

View File

@ -31,7 +31,7 @@ const STATUSES = [{
iconClassPoll: "icon-status-poll-question-mark" iconClassPoll: "icon-status-poll-question-mark"
}, { }, {
key: "GO", key: "GO",
label: "GO", label: "Go",
iconClass: "icon-check", iconClass: "icon-check",
iconClassPoll: "icon-status-poll-question-mark", iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-ok", statusClass: "s-status-ok",
@ -39,7 +39,7 @@ const STATUSES = [{
statusFgColor: "#000" statusFgColor: "#000"
}, { }, {
key: "MAYBE", key: "MAYBE",
label: "MAYBE", label: "Maybe",
iconClass: "icon-alert-triangle", iconClass: "icon-alert-triangle",
iconClassPoll: "icon-status-poll-question-mark", iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-warning", statusClass: "s-status-warning",
@ -47,7 +47,7 @@ const STATUSES = [{
statusFgColor: "#000" statusFgColor: "#000"
}, { }, {
key: "NO_GO", key: "NO_GO",
label: "NO GO", label: "No go",
iconClass: "icon-circle-slash", iconClass: "icon-circle-slash",
iconClassPoll: "icon-status-poll-question-mark", iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-error", statusClass: "s-status-error",

View File

@ -88,8 +88,8 @@
"build:coverage": "webpack --config webpack.coverage.js", "build:coverage": "webpack --config webpack.coverage.js",
"build:watch": "webpack --config webpack.dev.js --watch", "build:watch": "webpack --config webpack.dev.js --watch",
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown", "info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", "test": "cross-env NODE_ENV=test 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:firefox": "cross-env NODE_ENV=test 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:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e": "npx playwright test", "test:e2e": "npx playwright test",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags", "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags",
@ -98,7 +98,7 @@
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js", "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:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
"test:perf": "npx playwright test --config=e2e/playwright-performance.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", "test:watch": "cross-env NODE_ENV=test 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", "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", "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'", "update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'",

View File

@ -96,161 +96,167 @@ define([
}; };
this.destroy = this.destroy.bind(this); this.destroy = this.destroy.bind(this);
/** [
* Tracks current selection state of the application. /**
* @private * Tracks current selection state of the application.
*/ * @private
this.selection = new Selection(this); */
['selection', () => new Selection(this)],
/** /**
* MCT's time conductor, which may be used to synchronize view contents * MCT's time conductor, which may be used to synchronize view contents
* for telemetry- or time-based views. * for telemetry- or time-based views.
* @type {module:openmct.TimeConductor} * @type {module:openmct.TimeConductor}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name conductor * @name conductor
*/ */
this.time = new api.TimeAPI(this); ['time', () => new api.TimeAPI(this)],
/** /**
* An interface for interacting with the composition of domain objects. * An interface for interacting with the composition of domain objects.
* The composition of a domain object is the list of other domain * The composition of a domain object is the list of other domain
* objects it "contains" (for instance, that should be displayed * objects it "contains" (for instance, that should be displayed
* beneath it in the tree.) * beneath it in the tree.)
* *
* `composition` may be called as a function, in which case it acts * `composition` may be called as a function, in which case it acts
* as [`composition.get`]{@link module:openmct.CompositionAPI#get}. * as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
* *
* @type {module:openmct.CompositionAPI} * @type {module:openmct.CompositionAPI}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name composition * @name composition
*/ */
this.composition = new api.CompositionAPI(this); ['composition', () => new api.CompositionAPI(this)],
/** /**
* Registry for views of domain objects which should appear in the * Registry for views of domain objects which should appear in the
* main viewing area. * main viewing area.
* *
* @type {module:openmct.ViewRegistry} * @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name objectViews * @name objectViews
*/ */
this.objectViews = new ViewRegistry(); ['objectViews', () => new ViewRegistry()],
/** /**
* Registry for views which should appear in the Inspector area. * Registry for views which should appear in the Inspector area.
* These views will be chosen based on the selection state. * These views will be chosen based on the selection state.
* *
* @type {module:openmct.InspectorViewRegistry} * @type {module:openmct.InspectorViewRegistry}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name inspectorViews * @name inspectorViews
*/ */
this.inspectorViews = new InspectorViewRegistry(); ['inspectorViews', () => new InspectorViewRegistry()],
/** /**
* Registry for views which should appear in Edit Properties * Registry for views which should appear in Edit Properties
* dialogs, and similar user interface elements used for * dialogs, and similar user interface elements used for
* modifying domain objects external to its regular views. * modifying domain objects external to its regular views.
* *
* @type {module:openmct.ViewRegistry} * @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name propertyEditors * @name propertyEditors
*/ */
this.propertyEditors = new ViewRegistry(); ['propertyEditors', () => new ViewRegistry()],
/** /**
* Registry for views which should appear in the status indicator area. * Registry for views which should appear in the toolbar area while
* @type {module:openmct.ViewRegistry} * editing. These views will be chosen based on the selection state.
* @memberof module:openmct.MCT# *
* @name indicators * @type {module:openmct.ToolbarRegistry}
*/ * @memberof module:openmct.MCT#
this.indicators = new ViewRegistry(); * @name toolbars
*/
['toolbars', () => new ToolbarRegistry()],
/** /**
* Registry for views which should appear in the toolbar area while * Registry for domain object types which may exist within this
* editing. These views will be chosen based on the selection state. * instance of Open MCT.
* *
* @type {module:openmct.ToolbarRegistry} * @type {module:openmct.TypeRegistry}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name toolbars * @name types
*/ */
this.toolbars = new ToolbarRegistry(); ['types', () => new api.TypeRegistry()],
/** /**
* Registry for domain object types which may exist within this * An interface for interacting with domain objects and the domain
* instance of Open MCT. * object hierarchy.
* *
* @type {module:openmct.TypeRegistry} * @type {module:openmct.ObjectAPI}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name types * @name objects
*/ */
this.types = new api.TypeRegistry(); ['objects', () => new api.ObjectAPI.default(this.types, this)],
/** /**
* An interface for interacting with domain objects and the domain * An interface for retrieving and interpreting telemetry data associated
* object hierarchy. * with a domain object.
* *
* @type {module:openmct.ObjectAPI} * @type {module:openmct.TelemetryAPI}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name objects * @name telemetry
*/ */
this.objects = new api.ObjectAPI.default(this.types, this); ['telemetry', () => new api.TelemetryAPI.default(this)],
/** /**
* An interface for retrieving and interpreting telemetry data associated * An interface for creating new indicators and changing them dynamically.
* with a domain object. *
* * @type {module:openmct.IndicatorAPI}
* @type {module:openmct.TelemetryAPI} * @memberof module:openmct.MCT#
* @memberof module:openmct.MCT# * @name indicators
* @name telemetry */
*/ ['indicators', () => new api.IndicatorAPI(this)],
this.telemetry = new api.TelemetryAPI(this);
/** /**
* An interface for creating new indicators and changing them dynamically. * MCT's user awareness management, to enable user and
* * role specific functionality.
* @type {module:openmct.IndicatorAPI} * @type {module:openmct.UserAPI}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name indicators * @name user
*/ */
this.indicators = new api.IndicatorAPI(this); ['user', () => new api.UserAPI(this)],
/** ['notifications', () => new api.NotificationAPI()],
* MCT's user awareness management, to enable user and
* role specific functionality.
* @type {module:openmct.UserAPI}
* @memberof module:openmct.MCT#
* @name user
*/
this.user = new api.UserAPI(this);
this.notifications = new api.NotificationAPI(); ['editor', () => new api.EditorAPI.default(this)],
this.editor = new api.EditorAPI.default(this); ['overlays', () => new OverlayAPI.default()],
this.overlays = new OverlayAPI.default(); ['menus', () => new api.MenuAPI(this)],
this.menus = new api.MenuAPI(this); ['actions', () => new api.ActionsAPI(this)],
this.actions = new api.ActionsAPI(this); ['status', () => new api.StatusAPI(this)],
this.status = new api.StatusAPI(this); ['priority', () => api.PriorityAPI],
this.priority = api.PriorityAPI; ['router', () => new ApplicationRouter(this)],
this.router = new ApplicationRouter(this); ['faults', () => new api.FaultManagementAPI.default(this)],
this.faults = new api.FaultManagementAPI.default(this);
this.forms = new api.FormsAPI.default(this);
this.branding = BrandingAPI.default; ['forms', () => new api.FormsAPI.default(this)],
/** ['branding', () => BrandingAPI.default],
* MCT's annotation API that enables
* human-created comments and categorization linked to data products /**
* @type {module:openmct.AnnotationAPI} * MCT's annotation API that enables
* @memberof module:openmct.MCT# * human-created comments and categorization linked to data products
* @name annotation * @type {module:openmct.AnnotationAPI}
*/ * @memberof module:openmct.MCT#
this.annotation = new api.AnnotationAPI(this); * @name annotation
*/
['annotation', () => new api.AnnotationAPI(this)]
].forEach(apiEntry => {
const apiName = apiEntry[0];
const apiObject = apiEntry[1]();
Object.defineProperty(this, apiName, {
value: apiObject,
enumerable: false,
configurable: false,
writable: true
});
});
// Plugins that are installed by default // Plugins that are installed by default
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
@ -281,6 +287,7 @@ define([
this.install(this.plugins.ObjectInterceptors()); this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.DeviceClassifier()); this.install(this.plugins.DeviceClassifier());
this.install(this.plugins.UserIndicator()); this.install(this.plugins.UserIndicator());
this.install(this.plugins.Gauge());
} }
MCT.prototype = Object.create(EventEmitter.prototype); MCT.prototype = Object.create(EventEmitter.prototype);

View File

@ -230,10 +230,15 @@ export default class ObjectAPI {
return result; return result;
}).catch((result) => { }).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result); console.warn(`Failed to retrieve ${keystring}:`, result);
this.openmct.notifications.error(`Failed to retrieve object ${keystring}`);
delete this.cache[keystring]; delete this.cache[keystring];
result = this.applyGetInterceptors(identifier); if (!result) {
//no result means resource either doesn't exist or is missing
//otherwise it's an error, and we shouldn't apply interceptors
result = this.applyGetInterceptors(identifier);
}
return result; return result;
}); });
@ -383,7 +388,13 @@ export default class ObjectAPI {
} }
} }
return result; return result.catch((error) => {
if (error instanceof this.errors.Conflict) {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
}
throw error;
});
} }
/** /**

View File

@ -20,122 +20,18 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const { TelemetryCollection } = require("./TelemetryCollection"); import TelemetryCollection from './TelemetryCollection';
import TelemetryRequestInterceptorRegistry from './TelemetryRequestInterceptor';
import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter';
import TelemetryMetadataManager from './TelemetryMetadataManager';
import TelemetryValueFormatter from './TelemetryValueFormatter';
import DefaultMetadataProvider from './DefaultMetadataProvider';
import objectUtils from 'objectUtils';
import _ from 'lodash';
define([ export default class TelemetryAPI {
'../../plugins/displayLayout/CustomStringFormatter',
'./TelemetryMetadataManager',
'./TelemetryValueFormatter',
'./DefaultMetadataProvider',
'objectUtils',
'lodash'
], function (
CustomStringFormatter,
TelemetryMetadataManager,
TelemetryValueFormatter,
DefaultMetadataProvider,
objectUtils,
_
) {
/**
* A LimitEvaluator may be used to detect when telemetry values
* have exceeded nominal conditions.
*
* @interface LimitEvaluator
* @memberof module:openmct.TelemetryAPI~
*/
/** constructor(openmct) {
* Check for any limit violations associated with a telemetry datum.
* @method evaluate
* @param {*} datum the telemetry datum to evaluate
* @param {TelemetryProperty} the property to check for limit violations
* @memberof module:openmct.TelemetryAPI~LimitEvaluator
* @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
* the limit violation, or undefined if a value is within limits
*/
/**
* A violation of limits defined for a telemetry property.
* @typedef LimitViolation
* @memberof {module:openmct.TelemetryAPI~}
* @property {string} cssClass the class (or space-separated classes) to
* apply to display elements for values which violate this limit
* @property {string} name the human-readable name for the limit violation
*/
/**
* A TelemetryFormatter converts telemetry values for purposes of
* display as text.
*
* @interface TelemetryFormatter
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Retrieve the 'key' from the datum and format it accordingly to
* telemetry metadata in domain object.
*
* @method format
* @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
*/
/**
* Describes a property which would be found in a datum of telemetry
* associated with a particular domain object.
*
* @typedef TelemetryProperty
* @memberof module:openmct.TelemetryAPI~
* @property {string} key the name of the property in the datum which
* contains this telemetry value
* @property {string} name the human-readable name for this property
* @property {string} [units] the units associated with this property
* @property {boolean} [temporal] true if this property is a timestamp, or
* may be otherwise used to order telemetry in a time-like
* fashion; default is false
* @property {boolean} [numeric] true if the values for this property
* can be interpreted plainly as numbers; default is true
* @property {boolean} [enumerated] true if this property may have only
* certain specific values; default is false
* @property {string} [values] for enumerated states, an ordered list
* of possible values
*/
/**
* Describes and bounds requests for telemetry data.
*
* @typedef TelemetryRequest
* @memberof module:openmct.TelemetryAPI~
* @property {string} sort the key of the property to sort by. This may
* be prefixed with a "+" or a "-" sign to sort in ascending
* or descending order respectively. If no prefix is present,
* ascending order will be used.
* @property {*} start the lower bound for values of the sorting property
* @property {*} end the upper bound for values of the sorting property
* @property {string[]} strategies symbolic identifiers for strategies
* (such as `minmax`) which may be recognized by providers;
* these will be tried in order until an appropriate provider
* is found
*/
/**
* Provides telemetry data. To connect to new data sources, new
* TelemetryProvider implementations should be
* [registered]{@link module:openmct.TelemetryAPI#addProvider}.
*
* @interface TelemetryProvider
* @memberof module:openmct.TelemetryAPI~
*/
/**
* An interface for retrieving telemetry data associated with a domain
* object.
*
* @interface TelemetryAPI
* @augments module:openmct.TelemetryAPI~TelemetryProvider
* @memberof module:openmct
*/
function TelemetryAPI(openmct) {
this.openmct = openmct; this.openmct = openmct;
this.formatMapCache = new WeakMap(); this.formatMapCache = new WeakMap();
@ -148,12 +44,14 @@ define([
this.requestProviders = []; this.requestProviders = [];
this.subscriptionProviders = []; this.subscriptionProviders = [];
this.valueFormatterCache = new WeakMap(); this.valueFormatterCache = new WeakMap();
this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();
} }
TelemetryAPI.prototype.abortAllRequests = function () { abortAllRequests() {
this.requestAbortControllers.forEach((controller) => controller.abort()); this.requestAbortControllers.forEach((controller) => controller.abort());
this.requestAbortControllers.clear(); this.requestAbortControllers.clear();
}; }
/** /**
* Return Custom String Formatter * Return Custom String Formatter
@ -162,9 +60,9 @@ define([
* @param {string} format custom formatter string (eg: %.4f, &lts etc.) * @param {string} format custom formatter string (eg: %.4f, &lts etc.)
* @returns {CustomStringFormatter} * @returns {CustomStringFormatter}
*/ */
TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) { customStringFormatter(valueMetadata, format) {
return new CustomStringFormatter.default(this.openmct, valueMetadata, format); return new CustomStringFormatter(this.openmct, valueMetadata, format);
}; }
/** /**
* Return true if the given domainObject is a telemetry object. A telemetry * Return true if the given domainObject is a telemetry object. A telemetry
@ -174,9 +72,9 @@ define([
* @param {module:openmct.DomainObject} domainObject * @param {module:openmct.DomainObject} domainObject
* @returns {boolean} true if the object is a telemetry object. * @returns {boolean} true if the object is a telemetry object.
*/ */
TelemetryAPI.prototype.isTelemetryObject = function (domainObject) { isTelemetryObject(domainObject) {
return Boolean(this.findMetadataProvider(domainObject)); return Boolean(this.findMetadataProvider(domainObject));
}; }
/** /**
* Check if this provider can supply telemetry data associated with * Check if this provider can supply telemetry data associated with
@ -188,10 +86,10 @@ define([
* @returns {boolean} true if telemetry can be provided * @returns {boolean} true if telemetry can be provided
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) { canProvideTelemetry(domainObject) {
return Boolean(this.findSubscriptionProvider(domainObject)) return Boolean(this.findSubscriptionProvider(domainObject))
|| Boolean(this.findRequestProvider(domainObject)); || Boolean(this.findRequestProvider(domainObject));
}; }
/** /**
* Register a telemetry provider with the telemetry service. This * Register a telemetry provider with the telemetry service. This
@ -201,7 +99,7 @@ define([
* @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new
* telemetry provider * telemetry provider
*/ */
TelemetryAPI.prototype.addProvider = function (provider) { addProvider(provider) {
if (provider.supportsRequest) { if (provider.supportsRequest) {
this.requestProviders.unshift(provider); this.requestProviders.unshift(provider);
} }
@ -217,54 +115,54 @@ define([
if (provider.supportsLimits) { if (provider.supportsLimits) {
this.limitProviders.unshift(provider); this.limitProviders.unshift(provider);
} }
}; }
/** /**
* @private * @private
*/ */
TelemetryAPI.prototype.findSubscriptionProvider = function () { findSubscriptionProvider() {
const args = Array.prototype.slice.apply(arguments); const args = Array.prototype.slice.apply(arguments);
function supportsDomainObject(provider) { function supportsDomainObject(provider) {
return provider.supportsSubscribe.apply(provider, args); return provider.supportsSubscribe.apply(provider, args);
} }
return this.subscriptionProviders.filter(supportsDomainObject)[0]; return this.subscriptionProviders.filter(supportsDomainObject)[0];
}; }
/** /**
* @private * @private
*/ */
TelemetryAPI.prototype.findRequestProvider = function (domainObject) { findRequestProvider(domainObject) {
const args = Array.prototype.slice.apply(arguments); const args = Array.prototype.slice.apply(arguments);
function supportsDomainObject(provider) { function supportsDomainObject(provider) {
return provider.supportsRequest.apply(provider, args); return provider.supportsRequest.apply(provider, args);
} }
return this.requestProviders.filter(supportsDomainObject)[0]; return this.requestProviders.filter(supportsDomainObject)[0];
}; }
/** /**
* @private * @private
*/ */
TelemetryAPI.prototype.findMetadataProvider = function (domainObject) { findMetadataProvider(domainObject) {
return this.metadataProviders.filter(function (p) { return this.metadataProviders.filter(function (p) {
return p.supportsMetadata(domainObject); return p.supportsMetadata(domainObject);
})[0]; })[0];
}; }
/** /**
* @private * @private
*/ */
TelemetryAPI.prototype.findLimitEvaluator = function (domainObject) { findLimitEvaluator(domainObject) {
return this.limitProviders.filter(function (p) { return this.limitProviders.filter(function (p) {
return p.supportsLimits(domainObject); return p.supportsLimits(domainObject);
})[0]; })[0];
}; }
/** /**
* @private * @private
*/ */
TelemetryAPI.prototype.standardizeRequestOptions = function (options) { standardizeRequestOptions(options) {
if (!Object.prototype.hasOwnProperty.call(options, 'start')) { if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
options.start = this.openmct.time.bounds().start; options.start = this.openmct.time.bounds().start;
} }
@ -276,7 +174,47 @@ define([
if (!Object.prototype.hasOwnProperty.call(options, 'domain')) { if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
options.domain = this.openmct.time.timeSystem().key; options.domain = this.openmct.time.timeSystem().key;
} }
}; }
/**
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
* The request will be modifyed when it is received and will be returned in it's modified state
* The request will be transformed only if the interceptor is applicable to that domain object as defined by the RequestInterceptorDef
*
* @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the request interceptor definition to add
* @method addRequestInterceptor
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
addRequestInterceptor(requestInterceptorDef) {
this.requestInterceptorRegistry.addInterceptor(requestInterceptorDef);
}
/**
* Retrieve the request interceptors for a given domain object.
* @private
*/
#getInterceptorsForRequest(identifier, request) {
return this.requestInterceptorRegistry.getInterceptors(identifier, request);
}
/**
* Invoke interceptors if applicable for a given domain object.
*/
async applyRequestInterceptors(domainObject, request) {
const interceptors = this.#getInterceptorsForRequest(domainObject.identifier, request);
if (interceptors.length === 0) {
return request;
}
let modifiedRequest = { ...request };
for (let interceptor of interceptors) {
modifiedRequest = await interceptor.invoke(modifiedRequest);
}
return modifiedRequest;
}
/** /**
* Request telemetry collection for a domain object. * Request telemetry collection for a domain object.
@ -292,13 +230,13 @@ define([
* options for this telemetry collection request * options for this telemetry collection request
* @returns {TelemetryCollection} a TelemetryCollection instance * @returns {TelemetryCollection} a TelemetryCollection instance
*/ */
TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) { requestCollection(domainObject, options = {}) {
return new TelemetryCollection( return new TelemetryCollection(
this.openmct, this.openmct,
domainObject, domainObject,
options options
); );
}; }
/** /**
* Request historical telemetry for a domain object. * Request historical telemetry for a domain object.
@ -315,7 +253,7 @@ define([
* @returns {Promise.<object[]>} a promise for an array of * @returns {Promise.<object[]>} a promise for an array of
* telemetry data * telemetry data
*/ */
TelemetryAPI.prototype.request = function (domainObject) { async request(domainObject) {
if (this.noRequestProviderForAllObjects) { if (this.noRequestProviderForAllObjects) {
return Promise.resolve([]); return Promise.resolve([]);
} }
@ -330,6 +268,7 @@ define([
this.requestAbortControllers.add(abortController); this.requestAbortControllers.add(abortController);
this.standardizeRequestOptions(arguments[1]); this.standardizeRequestOptions(arguments[1]);
const provider = this.findRequestProvider.apply(this, arguments); const provider = this.findRequestProvider.apply(this, arguments);
if (!provider) { if (!provider) {
this.requestAbortControllers.delete(abortController); this.requestAbortControllers.delete(abortController);
@ -337,6 +276,8 @@ define([
return this.handleMissingRequestProvider(domainObject); return this.handleMissingRequestProvider(domainObject);
} }
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
return provider.request.apply(provider, arguments) return provider.request.apply(provider, arguments)
.catch((rejected) => { .catch((rejected) => {
if (rejected.name !== 'AbortError') { if (rejected.name !== 'AbortError') {
@ -348,7 +289,7 @@ define([
}).finally(() => { }).finally(() => {
this.requestAbortControllers.delete(abortController); this.requestAbortControllers.delete(abortController);
}); });
}; }
/** /**
* Subscribe to realtime telemetry for a specific domain object. * Subscribe to realtime telemetry for a specific domain object.
@ -364,7 +305,7 @@ define([
* @returns {Function} a function which may be called to terminate * @returns {Function} a function which may be called to terminate
* the subscription * the subscription
*/ */
TelemetryAPI.prototype.subscribe = function (domainObject, callback, options) { subscribe(domainObject, callback, options) {
const provider = this.findSubscriptionProvider(domainObject); const provider = this.findSubscriptionProvider(domainObject);
if (!this.subscribeCache) { if (!this.subscribeCache) {
@ -401,7 +342,7 @@ define([
delete this.subscribeCache[keyString]; delete this.subscribeCache[keyString];
} }
}.bind(this); }.bind(this);
}; }
/** /**
* Get telemetry metadata for a given domain object. Returns a telemetry * Get telemetry metadata for a given domain object. Returns a telemetry
@ -410,7 +351,7 @@ define([
* *
* @returns {TelemetryMetadataManager} * @returns {TelemetryMetadataManager}
*/ */
TelemetryAPI.prototype.getMetadata = function (domainObject) { getMetadata(domainObject) {
if (!this.metadataCache.has(domainObject)) { if (!this.metadataCache.has(domainObject)) {
const metadataProvider = this.findMetadataProvider(domainObject); const metadataProvider = this.findMetadataProvider(domainObject);
if (!metadataProvider) { if (!metadataProvider) {
@ -426,14 +367,14 @@ define([
} }
return this.metadataCache.get(domainObject); return this.metadataCache.get(domainObject);
}; }
/** /**
* Return an array of valueMetadatas that are common to all supplied * Return an array of valueMetadatas that are common to all supplied
* telemetry objects and match the requested hints. * telemetry objects and match the requested hints.
* *
*/ */
TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) { commonValuesForHints(metadatas, hints) {
const options = metadatas.map(function (metadata) { const options = metadatas.map(function (metadata) {
const values = metadata.valuesForHints(hints); const values = metadata.valuesForHints(hints);
@ -453,14 +394,14 @@ define([
}); });
return _.sortBy(options, sortKeys); return _.sortBy(options, sortKeys);
}; }
/** /**
* Get a value formatter for a given valueMetadata. * Get a value formatter for a given valueMetadata.
* *
* @returns {TelemetryValueFormatter} * @returns {TelemetryValueFormatter}
*/ */
TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) { getValueFormatter(valueMetadata) {
if (!this.valueFormatterCache.has(valueMetadata)) { if (!this.valueFormatterCache.has(valueMetadata)) {
this.valueFormatterCache.set( this.valueFormatterCache.set(
valueMetadata, valueMetadata,
@ -469,7 +410,7 @@ define([
} }
return this.valueFormatterCache.get(valueMetadata); return this.valueFormatterCache.get(valueMetadata);
}; }
/** /**
* Get a value formatter for a given key. * Get a value formatter for a given key.
@ -477,9 +418,9 @@ define([
* *
* @returns {Format} * @returns {Format}
*/ */
TelemetryAPI.prototype.getFormatter = function (key) { getFormatter(key) {
return this.formatters.get(key); return this.formatters.get(key);
}; }
/** /**
* Get a format map of all value formatters for a given piece of telemetry * Get a format map of all value formatters for a given piece of telemetry
@ -487,7 +428,7 @@ define([
* *
* @returns {Object<String, {TelemetryValueFormatter}>} * @returns {Object<String, {TelemetryValueFormatter}>}
*/ */
TelemetryAPI.prototype.getFormatMap = function (metadata) { getFormatMap(metadata) {
if (!metadata) { if (!metadata) {
return {}; return {};
} }
@ -502,14 +443,14 @@ define([
} }
return this.formatMapCache.get(metadata); return this.formatMapCache.get(metadata);
}; }
/** /**
* Error Handling: Missing Request provider * Error Handling: Missing Request provider
* *
* @returns Promise * @returns Promise
*/ */
TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) { handleMissingRequestProvider(domainObject) {
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => { this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments); const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function'; const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
@ -532,15 +473,15 @@ define([
console.warn(detailMessage); console.warn(detailMessage);
return Promise.resolve([]); return Promise.resolve([]);
}; }
/** /**
* Register a new telemetry data formatter. * Register a new telemetry data formatter.
* @param {Format} format the * @param {Format} format the
*/ */
TelemetryAPI.prototype.addFormat = function (format) { addFormat(format) {
this.formatters.set(format.key, format); this.formatters.set(format.key, format);
}; }
/** /**
* Get a limit evaluator for this domain object. * Get a limit evaluator for this domain object.
@ -558,9 +499,9 @@ define([
* @method limitEvaluator * @method limitEvaluator
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.limitEvaluator = function (domainObject) { limitEvaluator(domainObject) {
return this.getLimitEvaluator(domainObject); return this.getLimitEvaluator(domainObject);
}; }
/** /**
* Get a limits for this domain object. * Get a limits for this domain object.
@ -578,9 +519,9 @@ define([
* @method limits * @method limits
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.limitDefinition = function (domainObject) { limitDefinition(domainObject) {
return this.getLimits(domainObject); return this.getLimits(domainObject);
}; }
/** /**
* Get a limit evaluator for this domain object. * Get a limit evaluator for this domain object.
@ -598,7 +539,7 @@ define([
* @method limitEvaluator * @method limitEvaluator
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.getLimitEvaluator = function (domainObject) { getLimitEvaluator(domainObject) {
const provider = this.findLimitEvaluator(domainObject); const provider = this.findLimitEvaluator(domainObject);
if (!provider) { if (!provider) {
return { return {
@ -607,7 +548,7 @@ define([
} }
return provider.getLimitEvaluator(domainObject); return provider.getLimitEvaluator(domainObject);
}; }
/** /**
* Get a limit definitions for this domain object. * Get a limit definitions for this domain object.
@ -636,7 +577,7 @@ define([
* supported colors are purple, red, orange, yellow and cyan * supported colors are purple, red, orange, yellow and cyan
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.getLimits = function (domainObject) { getLimits(domainObject) {
const provider = this.findLimitEvaluator(domainObject); const provider = this.findLimitEvaluator(domainObject);
if (!provider || !provider.getLimits) { if (!provider || !provider.getLimits) {
return { return {
@ -647,7 +588,104 @@ define([
} }
return provider.getLimits(domainObject); return provider.getLimits(domainObject);
}; }
}
return TelemetryAPI; /**
}); * A LimitEvaluator may be used to detect when telemetry values
* have exceeded nominal conditions.
*
* @interface LimitEvaluator
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Check for any limit violations associated with a telemetry datum.
* @method evaluate
* @param {*} datum the telemetry datum to evaluate
* @param {TelemetryProperty} the property to check for limit violations
* @memberof module:openmct.TelemetryAPI~LimitEvaluator
* @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
* the limit violation, or undefined if a value is within limits
*/
/**
* A violation of limits defined for a telemetry property.
* @typedef LimitViolation
* @memberof {module:openmct.TelemetryAPI~}
* @property {string} cssClass the class (or space-separated classes) to
* apply to display elements for values which violate this limit
* @property {string} name the human-readable name for the limit violation
*/
/**
* A TelemetryFormatter converts telemetry values for purposes of
* display as text.
*
* @interface TelemetryFormatter
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Retrieve the 'key' from the datum and format it accordingly to
* telemetry metadata in domain object.
*
* @method format
* @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
*/
/**
* Describes a property which would be found in a datum of telemetry
* associated with a particular domain object.
*
* @typedef TelemetryProperty
* @memberof module:openmct.TelemetryAPI~
* @property {string} key the name of the property in the datum which
* contains this telemetry value
* @property {string} name the human-readable name for this property
* @property {string} [units] the units associated with this property
* @property {boolean} [temporal] true if this property is a timestamp, or
* may be otherwise used to order telemetry in a time-like
* fashion; default is false
* @property {boolean} [numeric] true if the values for this property
* can be interpreted plainly as numbers; default is true
* @property {boolean} [enumerated] true if this property may have only
* certain specific values; default is false
* @property {string} [values] for enumerated states, an ordered list
* of possible values
*/
/**
* Describes and bounds requests for telemetry data.
*
* @typedef TelemetryRequest
* @memberof module:openmct.TelemetryAPI~
* @property {string} sort the key of the property to sort by. This may
* be prefixed with a "+" or a "-" sign to sort in ascending
* or descending order respectively. If no prefix is present,
* ascending order will be used.
* @property {*} start the lower bound for values of the sorting property
* @property {*} end the upper bound for values of the sorting property
* @property {string[]} strategies symbolic identifiers for strategies
* (such as `minmax`) which may be recognized by providers;
* these will be tried in order until an appropriate provider
* is found
*/
/**
* Provides telemetry data. To connect to new data sources, new
* TelemetryProvider implementations should be
* [registered]{@link module:openmct.TelemetryAPI#addProvider}.
*
* @interface TelemetryProvider
* @memberof module:openmct.TelemetryAPI~
*/
/**
* An interface for retrieving telemetry data associated with a domain
* object.
*
* @interface TelemetryAPI
* @augments module:openmct.TelemetryAPI~TelemetryProvider
* @memberof module:openmct
*/

View File

@ -21,7 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing'; import { createOpenMct, resetApplicationState } from 'utils/testing';
import TelemetryAPI from './TelemetryAPI'; import TelemetryAPI from './TelemetryAPI';
const { TelemetryCollection } = require("./TelemetryCollection"); import TelemetryCollection from './TelemetryCollection';
describe('Telemetry API', function () { describe('Telemetry API', function () {
let openmct; let openmct;

View File

@ -26,7 +26,7 @@ import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } fro
/** Class representing a Telemetry Collection. */ /** Class representing a Telemetry Collection. */
export class TelemetryCollection extends EventEmitter { export default class TelemetryCollection extends EventEmitter {
/** /**
* Creates a Telemetry Collection * Creates a Telemetry Collection
* *
@ -49,6 +49,7 @@ export class TelemetryCollection extends EventEmitter {
this.pageState = undefined; this.pageState = undefined;
this.lastBounds = undefined; this.lastBounds = undefined;
this.requestAbort = undefined; this.requestAbort = undefined;
this.isStrategyLatest = this.options.strategy === 'latest';
} }
/** /**
@ -126,7 +127,8 @@ export class TelemetryCollection extends EventEmitter {
this.requestAbort = new AbortController(); this.requestAbort = new AbortController();
options.signal = this.requestAbort.signal; options.signal = this.requestAbort.signal;
this.emit('requestStarted'); this.emit('requestStarted');
historicalData = await historicalProvider.request(this.domainObject, options); const modifiedOptions = await this.openmct.telemetry.applyRequestInterceptors(this.domainObject, options);
historicalData = await historicalProvider.request(this.domainObject, modifiedOptions);
} catch (error) { } catch (error) {
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...'); console.error('Error requesting telemetry data...');
@ -168,17 +170,18 @@ export class TelemetryCollection extends EventEmitter {
* @private * @private
*/ */
_processNewTelemetry(telemetryData) { _processNewTelemetry(telemetryData) {
performance.mark('tlm:process:start');
if (telemetryData === undefined) { if (telemetryData === undefined) {
return; return;
} }
let latestBoundedDatum = this.boundedTelemetry[this.boundedTelemetry.length - 1];
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue; let parsedValue;
let beforeStartOfBounds; let beforeStartOfBounds;
let afterEndOfBounds; let afterEndOfBounds;
let added = []; let added = [];
// loop through, sort and dedupe
for (let datum of data) { for (let datum of data) {
parsedValue = this.parseTime(datum); parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start; beforeStartOfBounds = parsedValue < this.lastBounds.start;
@ -218,7 +221,17 @@ export class TelemetryCollection extends EventEmitter {
} }
if (added.length) { if (added.length) {
this.emit('add', added); // if latest strategy is requested, we need to check if the value is the latest unmitted value
if (this.isStrategyLatest) {
this.boundedTelemetry = [this.boundedTelemetry[this.boundedTelemetry.length - 1]];
// if true, then this value has yet to be emitted
if (this.boundedTelemetry[0] !== latestBoundedDatum) {
this.emit('add', this.boundedTelemetry);
}
} else {
this.emit('add', added);
}
} }
} }
@ -278,13 +291,20 @@ export class TelemetryCollection extends EventEmitter {
if (startChanged) { if (startChanged) {
testDatum[this.timeKey] = bounds.start; testDatum[this.timeKey] = bounds.start;
// Calculate the new index of the first item within the bounds
startIndex = _.sortedIndexBy( // a little more complicated if not latest strategy
this.boundedTelemetry, if (!this.isStrategyLatest) {
testDatum, // Calculate the new index of the first item within the bounds
datum => this.parseTime(datum) startIndex = _.sortedIndexBy(
); this.boundedTelemetry,
discarded = this.boundedTelemetry.splice(0, startIndex); testDatum,
datum => this.parseTime(datum)
);
discarded = this.boundedTelemetry.splice(0, startIndex);
} else if (this.parseTime(testDatum) > this.parseTime(this.boundedTelemetry[0])) {
discarded = this.boundedTelemetry;
this.boundedTelemetry = [];
}
} }
if (endChanged) { if (endChanged) {
@ -296,7 +316,6 @@ export class TelemetryCollection extends EventEmitter {
datum => this.parseTime(datum) datum => this.parseTime(datum)
); );
added = this.futureBuffer.splice(0, endIndex); added = this.futureBuffer.splice(0, endIndex);
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
} }
if (discarded.length > 0) { if (discarded.length > 0) {
@ -304,6 +323,13 @@ export class TelemetryCollection extends EventEmitter {
} }
if (added.length > 0) { if (added.length > 0) {
if (!this.isStrategyLatest) {
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
} else {
added = [added[added.length - 1]];
this.boundedTelemetry = added;
}
this.emit('add', added); this.emit('add', added);
} }
} else { } else {
@ -362,7 +388,6 @@ export class TelemetryCollection extends EventEmitter {
* @todo handle subscriptions more granually * @todo handle subscriptions more granually
*/ */
_reset() { _reset() {
performance.mark('tlm:reset');
this.boundedTelemetry = []; this.boundedTelemetry = [];
this.futureBuffer = []; this.futureBuffer = [];

View File

@ -0,0 +1,68 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class TelemetryRequestInterceptorRegistry {
/**
* A TelemetryRequestInterceptorRegistry maintains the definitions for different interceptors that may be invoked on telemetry
* requests.
* @interface TelemetryRequestInterceptorRegistry
* @memberof module:openmct
*/
constructor() {
this.interceptors = [];
}
/**
* @interface TelemetryRequestInterceptorDef
* @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/request
* @property {function} invoke function that transforms the provided request and returns the transformed request
* @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
* @memberof module:openmct TelemetryRequestInterceptorRegistry#
*/
/**
* Register a new telemetry request interceptor.
*
* @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the interceptor to add
* @method addInterceptor
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
addInterceptor(interceptorDef) {
//TODO: sort by priority
this.interceptors.push(interceptorDef);
}
/**
* Retrieve all interceptors applicable to a domain object/request.
* @method getInterceptors
* @returns [module:openmct.RequestInterceptorDef] the registered interceptors for this identifier/request
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
getInterceptors(identifier, request) {
return this.interceptors.filter(interceptor => {
return typeof interceptor.appliesTo === 'function'
&& interceptor.appliesTo(identifier, request);
});
}
}

View File

@ -197,7 +197,7 @@ export default {
} }
}, },
setUnit() { setUnit() {
this.unit = this.valueMetadata.unit || ''; this.unit = this.valueMetadata ? this.valueMetadata.unit : '';
}, },
firstNonDomainAttribute(metadata) { firstNonDomainAttribute(metadata) {
return metadata return metadata

View File

@ -83,9 +83,12 @@ export default {
for (let ladTable of ladTables) { for (let ladTable of ladTables) {
for (let telemetryObject of ladTable) { for (let telemetryObject of ladTable) {
let metadata = this.openmct.telemetry.getMetadata(telemetryObject.domainObject); let metadata = this.openmct.telemetry.getMetadata(telemetryObject.domainObject);
for (let metadatum of metadata.valueMetadatas) {
if (metadatum.unit) { if (metadata) {
return true; for (let metadatum of metadata.valueMetadatas) {
if (metadatum.unit) {
return true;
}
} }
} }
} }

View File

@ -178,6 +178,26 @@ export default {
this.requestDataFor(telemetryObject); this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject); this.subscribeToObject(telemetryObject);
}, },
setTrace(key, name, axisMetadata, xValues, yValues) {
let trace = {
key,
name: name,
x: xValues,
y: yValues,
xAxisMetadata: {},
yAxisMetadata: axisMetadata.yAxisMetadata,
type: this.domainObject.configuration.useBar ? 'bar' : 'scatter',
mode: 'lines',
line: {
shape: this.domainObject.configuration.useInterpolation
},
marker: {
color: this.domainObject.configuration.barStyles.series[key].color
},
hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y'
};
this.addTrace(trace, key);
},
addTrace(trace, key) { addTrace(trace, key) {
if (!this.trace.length) { if (!this.trace.length) {
this.trace = this.trace.concat([trace]); this.trace = this.trace.concat([trace]);
@ -236,7 +256,15 @@ export default {
refreshData(bounds, isTick) { refreshData(bounds, isTick) {
if (!isTick) { if (!isTick) {
const telemetryObjects = Object.values(this.telemetryObjects); const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.requestDataFor); telemetryObjects.forEach((telemetryObject) => {
//clear existing data
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
const axisMetadata = this.getAxisMetadata(telemetryObject);
this.setTrace(key, telemetryObject.name, axisMetadata, [], []);
//request new data
this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject);
});
} }
}, },
removeAllSubscriptions() { removeAllSubscriptions() {
@ -320,25 +348,7 @@ export default {
}); });
} }
let trace = { this.setTrace(key, telemetryObject.name, axisMetadata, xValues, yValues);
key,
name: telemetryObject.name,
x: xValues,
y: yValues,
xAxisMetadata: xAxisMetadata,
yAxisMetadata: axisMetadata.yAxisMetadata,
type: this.domainObject.configuration.useBar ? 'bar' : 'scatter',
mode: 'lines',
line: {
shape: this.domainObject.configuration.useInterpolation
},
marker: {
color: this.domainObject.configuration.barStyles.series[key].color
},
hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y'
};
this.addTrace(trace, key);
}, },
isDataInTimeRange(datum, key, telemetryObject) { isDataInTimeRange(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key; const timeSystemKey = this.timeContext.timeSystem().key;

View File

@ -66,12 +66,15 @@ export default function BarGraphViewProvider(openmct) {
} }
}; };
}, },
template: '<bar-graph-view :options="options"></bar-graph-view>' template: '<bar-graph-view ref="graphComponent" :options="options"></bar-graph-view>'
}); });
}, },
destroy: function () { destroy: function () {
component.$destroy(); component.$destroy();
component = undefined; component = undefined;
},
onClearData() {
component.$refs.graphComponent.refreshData();
} }
}; };
} }

View File

@ -281,11 +281,11 @@ export default {
this.xKeyOptions.push( this.xKeyOptions.push(
metadataValues.reduce((previousValue, currentValue) => { metadataValues.reduce((previousValue, currentValue) => {
return { return {
name: `${previousValue.name}, ${currentValue.name}`, name: previousValue?.name ? `${previousValue.name}, ${currentValue.name}` : `${currentValue.name}`,
value: currentValue.key, value: currentValue.key,
isArrayValue: currentValue.isArrayValue isArrayValue: currentValue.isArrayValue
}; };
}) }, {name: ''})
); );
} }
@ -316,11 +316,16 @@ export default {
} }
} else { } else {
if (this.yKey === undefined) { if (this.yKey === undefined) {
yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex); if (metadataValues.length && metadataArrayValues.length === 0) {
if (yKeyOptionIndex > -1) {
update = true; update = true;
this.yKey = this.yKeyOptions[yKeyOptionIndex].value; this.yKey = 'none';
this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name; } else {
yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex);
if (yKeyOptionIndex > -1) {
update = true;
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;
}
} }
} }
} }
@ -336,6 +341,8 @@ export default {
return option; return option;
}); });
} else if (this.xKey !== undefined && this.domainObject.configuration.axes.yKey === undefined) {
this.domainObject.configuration.axes.yKey = 'none';
} }
this.xKeyOptions = this.xKeyOptions.map((option, index) => { this.xKeyOptions = this.xKeyOptions.map((option, index) => {

View File

@ -28,9 +28,9 @@ export default function () {
return function install(openmct) { return function install(openmct) {
openmct.types.addType(BAR_GRAPH_KEY, { openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY, key: BAR_GRAPH_KEY,
name: "Graph (Bar or Line)", name: "Graph",
cssClass: "icon-bar-chart", cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.", description: "Visualize data as a bar or line graph.",
creatable: true, creatable: true,
initialize: function (domainObject) { initialize: function (domainObject) {
domainObject.composition = []; domainObject.composition = [];

View File

@ -367,19 +367,26 @@ describe("the plugin", function () {
type: "test-object", type: "test-object",
name: "Test Object", name: "Test Object",
telemetry: { telemetry: {
values: [{ values: [
key: "some-key", {
name: "Some attribute", key: "some-key",
hints: { source: "some-key",
domain: 1 name: "Some attribute",
} format: "enum",
}, { enumerations: [
key: "some-other-key", {
name: "Another attribute", value: 0,
hints: { string: "OFF"
range: 1 },
} {
}] value: 1,
string: "ON"
}
],
hints: {
range: 1
}
}]
} }
}; };
const composition = openmct.composition.get(parent); const composition = openmct.composition.get(parent);

View File

@ -300,8 +300,11 @@ export default class ConditionManager extends EventEmitter {
return this.compositionLoad.then(() => { return this.compositionLoad.then(() => {
let latestTimestamp; let latestTimestamp;
let conditionResults = {}; let conditionResults = {};
let nextLegOptions = {...options};
delete nextLegOptions.onPartialResponse;
const conditionRequests = this.conditions const conditionRequests = this.conditions
.map(condition => condition.requestLADConditionResult(options)); .map(condition => condition.requestLADConditionResult(nextLegOptions));
return Promise.all(conditionRequests) return Promise.all(conditionRequests)
.then((results) => { .then((results) => {

View File

@ -21,23 +21,25 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div class="c-fault-mgmt__list-header c-fault-mgmt__list"> <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header c-fault-mgmt__list">
<div class="c-fault-mgmt__checkbox"> <div class="c-fault-mgmt-item-header c-fault-mgmt__checkbox">
<input <input
type="checkbox" type="checkbox"
:checked="isSelectAll" :checked="isSelectAll"
@input="selectAll" @input="selectAll"
> >
</div> </div>
<div class="c-fault-mgmt__list-content"> <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-results c-fault-mgmt__list-severity">
<div class="c-fault-mgmt__list-header-results"> {{ totalFaultsCount }} Results </div> {{ totalFaultsCount }} Results
</div>
<div class="c-fault-mgmt__list-header-content">
<div class="c-fault-mgmt__list-content-right"> <div class="c-fault-mgmt__list-content-right">
<div class="c-fault-mgmt__list-header-tripVal c-fault-mgmt__list-trigVal">Trip Value</div> <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-tripVal">Trip Value</div>
<div class="c-fault-mgmt__list-header-liveVal c-fault-mgmt__list-curVal">Live Value</div> <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-liveVal">Live Value</div>
<div class="c-fault-mgmt__list-header-trigTime c-fault-mgmt__list-trigTime">Trigger Time</div> <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-trigTime">Trigger Time</div>
</div> </div>
</div> </div>
<div class="c-fault-mgmt__list-action-wrapper"> <div class=" c-fault-mgmt-item-header c-fault-mgmt__list-header-action-wrapper">
<div class="c-fault-mgmt__list-header-sortButton c-fault-mgmt__list-action-button"> <div class="c-fault-mgmt__list-header-sortButton c-fault-mgmt__list-action-button">
<SelectField <SelectField
class="c-fault-mgmt-viewButton" class="c-fault-mgmt-viewButton"

View File

@ -23,52 +23,55 @@
<template> <template>
<div <div
class="c-fault-mgmt__list data-selectable" class="c-fault-mgmt__list data-selectable"
:class="[ :class="classesFromState"
{'is-selected': isSelected},
{'is-unacknowledged': !fault.acknowledged},
{'is-shelved': fault.shelved}
]"
> >
<div class="c-fault-mgmt__checkbox"> <div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
<input <input
type="checkbox" type="checkbox"
:checked="isSelected" :checked="isSelected"
@input="toggleSelected" @input="toggleSelected"
> >
</div> </div>
<div <div class="c-fault-mgmt-item">
class="c-fault-mgmt__list-severity" <div
:title="fault.severity" class="c-fault-mgmt__list-severity"
:class="[ :title="fault.severity"
'is-severity-' + severity :class="[
]" 'is-severity-' + severity
> ]"
>
</div>
</div> </div>
<div class="c-fault-mgmt__list-content"> <div class="c-fault-mgmt-item c-fault-mgmt__list-content">
<div class="c-fault-mgmt__list-pathname"> <div class="c-fault-mgmt-item c-fault-mgmt__list-pathname">
<div class="c-fault-mgmt__list-path">{{ fault.namespace }}</div> <div class="c-fault-mgmt__list-path">{{ fault.namespace }}</div>
<div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div> <div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div>
</div> </div>
<div class="c-fault-mgmt__list-content-right"> <div class="c-fault-mgmt__list-content-right">
<div <div class="c-fault-mgmt-item c-fault-mgmt__list-trigVal">
class="c-fault-mgmt__list-trigVal" <div
:class="tripValueClassname" class="c-fault-mgmt-item__value"
title="Trip Value" :class="tripValueClassname"
>{{ fault.triggerValueInfo.value }}</div> title="Trip Value"
<div >{{ fault.triggerValueInfo.value }}</div>
class="c-fault-mgmt__list-curVal"
:class="liveValueClassname"
title="Live Value"
>
{{ fault.currentValueInfo.value }}
</div> </div>
<div <div class="c-fault-mgmt-item c-fault-mgmt__list-curVal">
class="c-fault-mgmt__list-trigTime" <div
>{{ fault.triggerTime }} class="c-fault-mgmt-item__value"
:class="liveValueClassname"
title="Live Value"
>{{ fault.currentValueInfo.value }}</div>
</div>
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigTime">
<div
class="c-fault-mgmt-item__value"
title="Last Trigger Time"
>{{ fault.triggerTime }}
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="c-fault-mgmt__list-action-wrapper"> <div class="c-fault-mgmt-item c-fault-mgmt__list-action-wrapper">
<button <button
class="c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots" class="c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots"
title="Disposition Actions" title="Disposition Actions"
@ -77,7 +80,6 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const RANGE_CONDITION_CLASS = { const RANGE_CONDITION_CLASS = {
@ -106,6 +108,36 @@ export default {
} }
}, },
computed: { computed: {
classesFromState() {
const exclusiveStates = [
{
className: 'is-shelved',
test: () => this.fault.shelved
},
{
className: 'is-unacknowledged',
test: () => !this.fault.acknowledged && !this.fault.shelved
},
{
className: 'is-acknowledged',
test: () => this.fault.acknowledged && !this.fault.shelved
}
];
const classes = [];
if (this.isSelected) {
classes.push('is-selected');
}
const matchingState = exclusiveStates.find(stateDefinition => stateDefinition.test());
if (matchingState !== undefined) {
classes.push(matchingState.className);
}
return classes;
},
liveValueClassname() { liveValueClassname() {
const currentValueInfo = this.fault?.currentValueInfo; const currentValueInfo = this.fault?.currentValueInfo;
if (!currentValueInfo || currentValueInfo.monitoringResult === 'IN_LIMITS') { if (!currentValueInfo || currentValueInfo.monitoringResult === 'IN_LIMITS') {
@ -149,7 +181,7 @@ export default {
const menuItems = [ const menuItems = [
{ {
cssClass: 'icon-bell', cssClass: 'icon-check',
isDisabled: this.fault.acknowledged, isDisabled: this.fault.acknowledged,
name: 'Acknowledge', name: 'Acknowledge',
description: '', description: '',

View File

@ -35,25 +35,31 @@
@shelveSelected="toggleShelveSelected" @shelveSelected="toggleShelveSelected"
/> />
<FaultManagementListHeader <div class="c-faults-list-view-header-item-container-wrapper">
class="header" <div class="c-faults-list-view-header-item-container">
:selected-faults="Object.values(selectedFaults)" <FaultManagementListHeader
:total-faults-count="filteredFaultsList.length" class="header"
@selectAll="selectAll" :selected-faults="Object.values(selectedFaults)"
@sortChanged="sortChanged" :total-faults-count="filteredFaultsList.length"
/> @selectAll="selectAll"
@sortChanged="sortChanged"
/>
<template v-if="filteredFaultsList.length > 0"> <div class="c-faults-list-view-item-body">
<FaultManagementListItem <template v-if="filteredFaultsList.length > 0">
v-for="fault of filteredFaultsList" <FaultManagementListItem
:key="fault.id" v-for="fault of filteredFaultsList"
:fault="fault" :key="fault.id"
:is-selected="isSelected(fault)" :fault="fault"
@toggleSelected="toggleSelected" :is-selected="isSelected(fault)"
@acknowledgeSelected="toggleAcknowledgeSelected" @toggleSelected="toggleSelected"
@shelveSelected="toggleShelveSelected" @acknowledgeSelected="toggleAcknowledgeSelected"
/> @shelveSelected="toggleShelveSelected"
</template> />
</template>
</div>
</div>
</div>
</div> </div>
</template> </template>
@ -90,17 +96,19 @@ export default {
computed: { computed: {
filteredFaultsList() { filteredFaultsList() {
const filterName = FILTER_ITEMS[this.filterIndex]; const filterName = FILTER_ITEMS[this.filterIndex];
let list = this.faultsList.filter(fault => !fault.shelved); let list = this.faultsList;
// Exclude shelved alarms from all views except the Shelved view
if (filterName !== 'Shelved') {
list = list.filter(fault => fault.shelved !== true);
}
if (filterName === 'Acknowledged') { if (filterName === 'Acknowledged') {
list = this.faultsList.filter(fault => fault.acknowledged); list = list.filter(fault => fault.acknowledged);
} } else if (filterName === 'Unacknowledged') {
list = list.filter(fault => !fault.acknowledged);
if (filterName === 'Unacknowledged') { } else if (filterName === 'Shelved') {
list = this.faultsList.filter(fault => !fault.acknowledged); list = list.filter(fault => fault.shelved);
}
if (filterName === 'Shelved') {
list = this.faultsList.filter(fault => fault.shelved);
} }
if (this.searchTerm.length > 0) { if (this.searchTerm.length > 0) {
@ -195,7 +203,7 @@ export default {
{ {
key: 'comment', key: 'comment',
control: 'textarea', control: 'textarea',
name: 'Comment', name: 'Optional comment',
pattern: '\\S+', pattern: '\\S+',
required: false, required: false,
cssClass: 'l-input-lg', cssClass: 'l-input-lg',
@ -237,7 +245,7 @@ export default {
{ {
key: 'comment', key: 'comment',
control: 'textarea', control: 'textarea',
name: 'Comment', name: 'Optional comment',
pattern: '\\S+', pattern: '\\S+',
required: false, required: false,
cssClass: 'l-input-lg', cssClass: 'l-input-lg',
@ -246,7 +254,7 @@ export default {
{ {
key: 'shelveDuration', key: 'shelveDuration',
control: 'select', control: 'select',
name: 'Shelve Duration', name: 'Shelve duration',
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
required: false, required: false,
cssClass: 'l-input-lg', cssClass: 'l-input-lg',

View File

@ -32,7 +32,7 @@ export default function FaultManagementPlugin() {
name: 'Fault Management', name: 'Fault Management',
creatable: false, creatable: false,
description: 'Fault Management View', description: 'Fault Management View',
cssClass: 'icon-telemetry' cssClass: 'icon-bell'
}); });
openmct.objectViews.addProvider(new FaultManagementViewProvider(openmct)); openmct.objectViews.addProvider(new FaultManagementViewProvider(openmct));

View File

@ -23,7 +23,7 @@
<template> <template>
<div class="c-fault-mgmt__toolbar"> <div class="c-fault-mgmt__toolbar">
<button <button
class="c-icon-button icon-bell" class="c-icon-button icon-check"
title="Acknowledge selected faults" title="Acknowledge selected faults"
:disabled="disableAcknowledge" :disabled="disableAcknowledge"
@click="acknowledgeSelected" @click="acknowledgeSelected"

View File

@ -21,14 +21,13 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div class="c-fault-mgmt"> <FaultManagementListView
<FaultManagementListView :faults-list="faultsList"
:faults-list="faultsList" />
/>
</div>
</template> </template>
<script> <script>
import FaultManagementListView from './FaultManagementListView.vue'; import FaultManagementListView from './FaultManagementListView.vue';
import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS } from './constants'; import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS } from './constants';

View File

@ -19,65 +19,32 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
$colorFaultItemFg: $colorBodyFg;
/*********************************************** FAULT PROPERTIES */ $colorFaultItemFgEmphasis: $colorBodyFgEm;
.is-severity-critical{ $colorFaultItemBg: pullForward($colorBodyBg, 5%);
@include glyphBefore($glyph-icon-alert-triangle);
color: $colorStatusError;
}
.is-severity-warning{
@include glyphBefore($glyph-icon-alert-rect);
color: $colorStatusAlert;
}
.is-severity-watch{
@include glyphBefore($glyph-icon-info);
color: $colorCommand;
}
.is-unacknowledged{
.c-fault-mgmt__list-severity{
@include pulse($animName: severityAnim, $dur: 200ms);
}
}
.is-selected {
background: $colorSelectedBg;
}
.is-shelved{
.c-fault-mgmt__list-content{
opacity: 50% !important;
font-style: italic;
}
.c-fault-mgmt__list-severity{
@include pulse($animName: shelvedAnim, $dur: 0ms);
}
}
/*********************************************** SEARCH */ /*********************************************** SEARCH */
.c-fault-mgmt__search-row{ .c-fault-mgmt__search-row {
display: flex; display: flex;
align-items: center; align-items: center;
flex: 0 0 auto;
> * + * { > * + * {
margin-left: 10px; margin-left: 10px;
float: right; float: right;
} }
} }
.c-fault-mgmt-search{ .c-fault-mgmt-search {
width: 95%; width: 95%;
} }
/*********************************************** TOOLBAR */ /*********************************************** TOOLBAR */
.c-fault-mgmt__toolbar{ .c-fault-mgmt__toolbar {
display: flex; display: flex;
justify-content: center; justify-content: center;
> * { flex: 0 0 auto;
font-size: 1.25em; > * + * {
margin-left: $interiorMargin;
} }
} }
@ -85,150 +52,217 @@
.c-faults-list-view { .c-faults-list-view {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
> * + * { > * + * {
margin-top: $interiorMargin; margin-top: $interiorMargin;
} }
} }
.c-faults-list-view-header-item-container {
display: grid;
width: 100%;
grid-template-columns: max-content max-content repeat(5,minmax(max-content, 20%)) max-content;
grid-row-gap: $interiorMargin;
/*********************************************** FAULT ITEM */ &-wrapper {
.c-fault-mgmt__list{ flex: 1 1 auto;
background: rgba($colorBodyFg, 0.1); padding-right: $interiorMargin; // Fend of from scrollbar
margin-bottom: 5px; overflow-y: auto;
padding: 4px;
display: flex;
align-items: center;
> * {
margin-left: $interiorMargin;
} }
&-severity{ .--width-less-than-600 & {
grid-template-columns: max-content max-content 1fr 1fr max-content;
}
}
.c-faults-list-view-item-body {
display: contents;
}
/*********************************************** LIST */
.c-fault-mgmt__list {
display: contents;
color: $colorFaultItemFg;
&-checkbox{
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
&-severity {
font-size: 2em; font-size: 2em;
margin-left: $interiorMarginLg;
}
&-pathname{ &.is-severity-critical {
flex-wrap: wrap; @include glyphBefore($glyph-icon-alert-triangle);
flex: 1 1 auto; color: $colorStatusError;
}
} &.is-severity-warning {
&-path{ @include glyphBefore($glyph-icon-alert-rect);
font-size: .75em; color: $colorStatusAlert;
} }
&-faultname{ &.is-severity-watch {
font-weight: bold; @include glyphBefore($glyph-icon-info);
font-size: 1.3em; color: $colorCommand;
}
&-content{
display: flex;
flex-wrap: wrap;
flex: 1 1 auto;
align-items: center;
}
&-content-right{
margin-left: auto;
display: flex;
flex-wrap: wrap;
}
&-trigVal, &-curVal, &-trigTime{
@include ellipsize;
border-radius: $controlCr;
padding: $interiorMargin;
width: 80px;
margin-right: $interiorMarginLg;
}
&-trigVal {
@include isLimit();
background: rgba($colorBodyFg, 0.25);
}
&-curVal {
@include isLimit();
background: rgba($colorBodyFg, 0.25);
&-alert{
background: $colorWarningHi;
} }
} }
&-trigTime{ &-content {
width: auto; display: contents;
.--width-less-than-600 & {
display: flex;
flex-wrap: wrap;
grid-column: span 2;
}
} }
&-action-wrapper{ &-pathname {
display: flex; padding-right: $interiorMarginLg;
align-content: right; overflow-wrap: anywhere;
width: 100px; min-width: 100px;
}
&-path {
font-size: .85em;
margin-left: $interiorMargin;
} }
&-action-button{ &-faultname{
font-size: 1.3em;
margin-left: $interiorMargin;
}
&-content-right {
display: contents;
}
&-trigTime {
grid-column: 6 / span 2;
}
&-action-wrapper {
text-align: right;
flex: 0 0 auto;
align-items: stretch;
}
&-action-button {
flex: 0 0 auto; flex: 0 0 auto;
margin-left: auto; margin-left: auto;
justify-content: right; text-align: right;
}
// STATES
&.is-unacknowledged {
color: $colorFaultItemFgEmphasis;
.c-fault-mgmt__list-severity {
@include pulse($animName: severityAnim, $dur: 200ms);
}
}
&.is-acknowledged,
&.is-shelved {
.c-fault-mgmt__list-severity {
&:before {
opacity: 60%;
//font-size: 1.5em;
}
&:after {
color: $colorFaultItemFgEmphasis;
display: block;
font-family: symbolsfont;
position: absolute;
//text-shadow: black 0 0 2px;
right: -3px;
bottom: -3px;
transform-origin: right bottom;
transform: scale(0.6);
}
}
}
&.is-shelved {
.c-fault-mgmt__list-pathname {
font-style: italic;
}
}
&.is-acknowledged .c-fault-mgmt__list-severity:after {
content: $glyph-icon-check;
}
&.is-shelved .c-fault-mgmt__list-severity:after {
content: $glyph-icon-timer;
} }
} }
/*********************************************** LIST HEADER */ /*********************************************** LIST HEADER */
.c-fault-mgmt__list-header{ .c-fault-mgmt__list-header {
display: flex; display: contents;
background: rgba($colorBodyFg, .23);
border-radius: $controlCr; border-radius: $controlCr;
align-items: center;
&-tripVal, &-liveVal, &-trigTime{ * {
background: none; margin: 0px;
border-radius: 0px;
} }
&-trigTime{ .--width-less-than-600 & {
width: 160px; .c-fault-mgmt__list-content-right {
} display:none;
&-sortButton{ }
flex: 0 0 auto;
margin-left: auto;
justify-content: right;
display: flex;
align-content: right;
width: 100px;
} }
} &-content {
display: contents;
}
.is-severity-critical{ &-results {
@include glyphBefore($glyph-icon-alert-triangle); grid-column: 2 / span 2;
color: $colorStatusError; font-size: 1em;
} height: auto;
}
.is-severity-warning{ &-action-wrapper {
@include glyphBefore($glyph-icon-alert-rect); grid-column: 7 / span 2;
color: $colorStatusAlert;
}
.is-severity-watch{ .--width-less-than-600 & {
@include glyphBefore($glyph-icon-info); grid-column: 4 / span 2;
color: $colorCommand; }
}
.is-unacknowledged{
.c-fault-mgmt__list-severity{
@include pulse($animName: severityAnim, $dur: 200ms);
} }
} }
.is-selected { /*********************************************** GRID ITEM */
background: $colorSelectedBg; .c-fault-mgmt-item {
} $p: $interiorMargin;
padding: $p;
background: $colorFaultItemBg;
white-space: nowrap;
.is-shelved{ &-header {
.c-fault-mgmt__list-content{ $c: $colorBodyBg;
opacity: 60% !important; background: $c;
font-style: italic; border-bottom: 5px solid $c; // Creates illusion of "space" beneath header
min-height: 30px; // Needed to align cells
padding: $p;
position: sticky;
top: 0;
z-index: 2;
} }
.c-fault-mgmt__list-severity{
@include pulse($animName: shelvedAnim, $dur: 0ms); &__value {
@include isLimit();
background: rgba($colorBodyFg, 0.1);
padding: $p;
border-radius: $controlCr;
display: inline-flex;
}
.is-selected & {
background: $colorSelectedBg;
} }
} }

View File

@ -169,6 +169,7 @@
</g> </g>
<g class="c-dial__text"> <g class="c-dial__text">
<text <text
v-if="displayUnits"
x="50%" x="50%"
y="70%" y="70%"
text-anchor="middle" text-anchor="middle"

View File

@ -40,7 +40,7 @@
<div class="c-form__row"> <div class="c-form__row">
<span class="req-indicator req"> <span class="req-indicator req">
</span> </span>
<label>Range minimum value</label> <label>Minimum value</label>
<input <input
ref="min" ref="min"
v-model.number="min" v-model.number="min"
@ -53,7 +53,7 @@
<div class="c-form__row"> <div class="c-form__row">
<span class="req-indicator"> <span class="req-indicator">
</span> </span>
<label>Range low limit</label> <label>Low limit</label>
<input <input
ref="limitLow" ref="limitLow"
v-model.number="limitLow" v-model.number="limitLow"
@ -64,26 +64,26 @@
</div> </div>
<div class="c-form__row"> <div class="c-form__row">
<span class="req-indicator req"> <span class="req-indicator">
</span> </span>
<label>Range maximum value</label> <label>High limit</label>
<input <input
ref="max" ref="limitHigh"
v-model.number="max" v-model.number="limitHigh"
data-field-name="max" data-field-name="limitHigh"
type="number" type="number"
@input="onChange" @input="onChange"
> >
</div> </div>
<div class="c-form__row"> <div class="c-form__row">
<span class="req-indicator"> <span class="req-indicator req">
</span> </span>
<label>Range high limit</label> <label>Maximum value</label>
<input <input
ref="limitHigh" ref="max"
v-model.number="limitHigh" v-model.number="max"
data-field-name="limitHigh" data-field-name="max"
type="number" type="number"
@input="onChange" @input="onChange"
> >

View File

@ -210,9 +210,10 @@
border-radius: $controlCr; border-radius: $controlCr;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
flex-direction: row;
justify-content: space-between; justify-content: space-between;
padding: $interiorMargin; padding: $interiorMargin;
width: min-content; width: max-content;
> * + * { > * + * {
margin-left: $interiorMargin; margin-left: $interiorMargin;
@ -338,7 +339,6 @@
&__input { &__input {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%;
&:before { &:before {
color: rgba($colorMenuFg, 0.5); color: rgba($colorMenuFg, 0.5);
@ -353,13 +353,16 @@
&--filters { &--filters {
// Styles specific to the brightness and contrast controls // Styles specific to the brightness and contrast controls
.c-image-controls { .c-image-controls {
&__controls {
width: 80px; // About the minimum this element can be; cannot size based on % due to markup structure
}
&__sliders { &__sliders {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
min-width: 80px; width: 100%;
> * + * { > * + * {
margin-top: 11px; margin-top: 11px;

View File

@ -76,7 +76,10 @@ export default {
dataRemoved(dataToRemove) { dataRemoved(dataToRemove) {
this.imageHistory = this.imageHistory.filter(existingDatum => { this.imageHistory = this.imageHistory.filter(existingDatum => {
const shouldKeep = dataToRemove.some(datumToRemove => { const shouldKeep = dataToRemove.some(datumToRemove => {
return (existingDatum.utc !== datumToRemove.utc); const existingDatumTimestamp = this.parseTime(existingDatum);
const datumToRemoveTimestamp = this.parseTime(datumToRemove);
return (existingDatumTimestamp !== datumToRemoveTimestamp);
}); });
return shouldKeep; return shouldKeep;

View File

@ -373,39 +373,30 @@ describe("The Imagery View Layouts", () => {
return Vue.nextTick(); return Vue.nextTick();
}); });
it("on mount should show the the most recent image", () => { it("on mount should show the the most recent image", async () => {
//Looks like we need Vue.nextTick here so that computed properties settle down //Looks like we need Vue.nextTick here so that computed properties settle down
return Vue.nextTick(() => { await Vue.nextTick();
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
}); });
it("on mount should show the any image layers", (done) => { it("on mount should show the any image layers", async () => {
//Looks like we need Vue.nextTick here so that computed properties settle down //Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick().then(() => { await Vue.nextTick();
Vue.nextTick(() => { const layerEls = parent.querySelectorAll('.js-layer-image');
const layerEls = parent.querySelectorAll('.js-layer-image'); console.log(layerEls);
console.log(layerEls); expect(layerEls.length).toEqual(1);
expect(layerEls.length).toEqual(1);
done();
});
});
}); });
it("should show the clicked thumbnail as the main image", (done) => { it("should show the clicked thumbnail as the main image", async () => {
//Looks like we need Vue.nextTick here so that computed properties settle down //Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick(() => { await Vue.nextTick();
const target = imageTelemetry[5].url; const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click(); parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => { await Vue.nextTick();
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
done();
});
});
}); });
xit("should show that an image is new", (done) => { xit("should show that an image is new", (done) => {
@ -424,71 +415,60 @@ describe("The Imagery View Layouts", () => {
}); });
}); });
it("should show that an image is not new", (done) => { it("should show that an image is not new", async () => {
Vue.nextTick(() => { await Vue.nextTick();
const target = imageTelemetry[4].url; const target = imageTelemetry[4].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click(); parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => { await Vue.nextTick();
const imageIsNew = isNew(parent); const imageIsNew = isNew(parent);
expect(imageIsNew).toBeFalse(); expect(imageIsNew).toBeFalse();
done();
});
});
}); });
it("should navigate via arrow keys", (done) => { it("should navigate via arrow keys", async () => {
Vue.nextTick(() => { await Vue.nextTick();
let keyOpts = { const keyOpts = {
element: parent.querySelector('.c-imagery'), element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft', key: 'ArrowLeft',
keyCode: 37, keyCode: 37,
type: 'keyup' type: 'keyup'
}; };
simulateKeyEvent(keyOpts); simulateKeyEvent(keyOpts);
Vue.nextTick(() => { await Vue.nextTick();
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
done();
});
});
}); });
it("should navigate via numerous arrow keys", (done) => { it("should navigate via numerous arrow keys", async () => {
Vue.nextTick(() => { await Vue.nextTick();
let element = parent.querySelector('.c-imagery'); const element = parent.querySelector('.c-imagery');
let type = 'keyup'; const type = 'keyup';
let leftKeyOpts = { const leftKeyOpts = {
element, element,
type, type,
key: 'ArrowLeft', key: 'ArrowLeft',
keyCode: 37 keyCode: 37
}; };
let rightKeyOpts = { const rightKeyOpts = {
element, element,
type, type,
key: 'ArrowRight', key: 'ArrowRight',
keyCode: 39 keyCode: 39
}; };
// left thrice // left thrice
simulateKeyEvent(leftKeyOpts); simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts); simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts); simulateKeyEvent(leftKeyOpts);
// right once // right once
simulateKeyEvent(rightKeyOpts); simulateKeyEvent(rightKeyOpts);
Vue.nextTick(() => { await Vue.nextTick();
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
done();
});
});
}); });
it ('shows an auto scroll button when scroll to left', (done) => { it ('shows an auto scroll button when scroll to left', (done) => {
Vue.nextTick(() => { Vue.nextTick(() => {

View File

@ -80,7 +80,7 @@
&__content { &__content {
$m: $interiorMargin; $m: $interiorMargin;
display: grid; display: grid;
grid-template-columns: min-content 1fr; grid-template-columns: max-content 1fr;
grid-column-gap: $m; grid-column-gap: $m;
grid-row-gap: $m; grid-row-gap: $m;

View File

@ -34,7 +34,7 @@
<div class="c-status-poll__section c-status-poll-panel__content c-spq"> <div class="c-status-poll__section c-status-poll-panel__content c-spq">
<!-- Grid layout --> <!-- Grid layout -->
<div class="c-spq__label">Current:</div> <div class="c-spq__label">Current poll:</div>
<div class="c-spq__value c-status-poll-panel__poll-question">{{ currentPollQuestion }}</div> <div class="c-spq__value c-status-poll-panel__poll-question">{{ currentPollQuestion }}</div>
<template v-if="statusCountViewModel.length > 0"> <template v-if="statusCountViewModel.length > 0">
@ -43,6 +43,7 @@
<div <div
v-for="entry in statusCountViewModel" v-for="entry in statusCountViewModel"
:key="entry.status.key" :key="entry.status.key"
:title="entry.status.label"
class="c-status-poll-report__count" class="c-status-poll-report__count"
:style="[{ :style="[{
background: entry.status.statusBgColor, background: entry.status.statusBgColor,
@ -69,6 +70,7 @@
> >
<button <button
class="c-button" class="c-button"
title="Publish a new poll question and reset previous responses"
@click="updatePollQuestion" @click="updatePollQuestion"
>Update</button> >Update</button>
</div> </div>
@ -78,6 +80,7 @@
</template> </template>
<script> <script>
import _ from 'lodash';
export default { export default {
inject: ['openmct', 'indicator', 'configuration'], inject: ['openmct', 'indicator', 'configuration'],
@ -118,6 +121,9 @@ export default {
this.openmct.user.status.off('statusChange', this.fetchStatusSummary); this.openmct.user.status.off('statusChange', this.fetchStatusSummary);
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion); this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
}, },
created() {
this.fetchStatusSummary = _.debounce(this.fetchStatusSummary);
},
methods: { methods: {
async fetchCurrentPoll() { async fetchCurrentPoll() {
const pollQuestion = await this.openmct.user.status.getPollQuestion(); const pollQuestion = await this.openmct.user.status.getPollQuestion();

View File

@ -45,8 +45,7 @@ export default function CouchDocument(id, model, rev, markDeleted) {
"category": "domain object", "category": "domain object",
"type": model.type, "type": model.type,
"owner": "admin", "owner": "admin",
"name": model.name, "name": model.name
"created": Date.now()
}, },
"model": model "model": model
}; };

View File

@ -199,6 +199,11 @@ class CouchObjectProvider {
} }
let response = null; let response = null;
if (!this.isObservingObjectChanges()) {
this.#observeObjectChanges();
}
try { try {
response = await fetch(this.url + '/' + subPath, fetchOptions); response = await fetch(this.url + '/' + subPath, fetchOptions);
const { status } = response; const { status } = response;
@ -210,9 +215,13 @@ class CouchObjectProvider {
// Network error, CouchDB unreachable. // Network error, CouchDB unreachable.
if (response === null) { if (response === null) {
this.indicator.setIndicatorToState(DISCONNECTED); this.indicator.setIndicatorToState(DISCONNECTED);
} console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
} else {
console.error(error.message);
console.error(error.message); throw error;
}
} }
} }
@ -374,6 +383,8 @@ class CouchObjectProvider {
return this.request(ALL_DOCS, 'POST', query, signal).then((response) => { return this.request(ALL_DOCS, 'POST', query, signal).then((response) => {
if (response && response.rows !== undefined) { if (response && response.rows !== undefined) {
return response.rows.reduce((map, row) => { return response.rows.reduce((map, row) => {
//row.doc === null if the document does not exist.
//row.doc === undefined if the document is not found.
if (row.doc !== undefined) { if (row.doc !== undefined) {
map[row.key] = this.#getModel(row.doc); map[row.key] = this.#getModel(row.doc);
} }
@ -471,9 +482,6 @@ class CouchObjectProvider {
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback); this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
if (this.observers[keyString].length === 0) { if (this.observers[keyString].length === 0) {
delete this.observers[keyString]; delete this.observers[keyString];
if (Object.keys(this.observers).length === 0 && this.isObservingObjectChanges()) {
this.stopObservingObjectChanges();
}
} }
} }
}; };
@ -498,7 +506,6 @@ class CouchObjectProvider {
} else { } else {
this.#initiateSharedWorkerFetchChanges(sseURL.toString()); this.#initiateSharedWorkerFetchChanges(sseURL.toString());
} }
} }
/** /**
@ -525,18 +532,24 @@ class CouchObjectProvider {
onEventError(error) { onEventError(error) {
console.error('Error on feed', error); console.error('Error on feed', error);
if (Object.keys(this.observers).length > 0) { const { readyState } = error.target;
this.#observeObjectChanges(); this.#updateIndicatorStatus(readyState);
} }
onEventOpen(event) {
const { readyState } = event.target;
this.#updateIndicatorStatus(readyState);
} }
onEventMessage(event) { onEventMessage(event) {
const { readyState } = event.target;
const eventData = JSON.parse(event.data); const eventData = JSON.parse(event.data);
const identifier = { const identifier = {
namespace: this.namespace, namespace: this.namespace,
key: eventData.id key: eventData.id
}; };
const keyString = this.openmct.objects.makeKeyString(identifier); const keyString = this.openmct.objects.makeKeyString(identifier);
this.#updateIndicatorStatus(readyState);
let observersForObject = this.observers[keyString]; let observersForObject = this.observers[keyString];
if (observersForObject) { if (observersForObject) {
@ -559,17 +572,18 @@ class CouchObjectProvider {
this.stopObservingObjectChanges = () => { this.stopObservingObjectChanges = () => {
controller.abort(); controller.abort();
couchEventSource.removeEventListener('message', this.onEventMessage); couchEventSource.removeEventListener('message', this.onEventMessage.bind(this));
delete this.stopObservingObjectChanges; delete this.stopObservingObjectChanges;
}; };
console.debug('⇿ Opening CouchDB change feed connection ⇿'); console.debug('⇿ Opening CouchDB change feed connection ⇿');
couchEventSource = new EventSource(url); couchEventSource = new EventSource(url);
couchEventSource.onerror = this.onEventError; couchEventSource.onerror = this.onEventError.bind(this);
couchEventSource.onopen = this.onEventOpen.bind(this);
// start listening for events // start listening for events
couchEventSource.addEventListener('message', this.onEventMessage); couchEventSource.addEventListener('message', this.onEventMessage.bind(this));
console.debug('⇿ Opened connection ⇿'); console.debug('⇿ Opened connection ⇿');
} }
@ -587,6 +601,31 @@ class CouchObjectProvider {
return intermediateResponse; return intermediateResponse;
} }
/**
* Update the indicator status based on the readyState of the EventSource
* @private
*/
#updateIndicatorStatus(readyState) {
let message;
switch (readyState) {
case EventSource.CONNECTING:
message = 'pending';
break;
case EventSource.OPEN:
message = 'open';
break;
case EventSource.CLOSED:
message = 'close';
break;
default:
message = 'unknown';
break;
}
const indicatorState = this.#messageToIndicatorState(message);
this.indicator.setIndicatorToState(indicatorState);
}
/** /**
* @private * @private
*/ */
@ -614,8 +653,8 @@ class CouchObjectProvider {
this.objectQueue[key].pending = true; this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue(); const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model); let document = new CouchDocument(key, queued.model);
document.metadata.created = Date.now();
this.request(key, "PUT", document).then((response) => { this.request(key, "PUT", document).then((response) => {
console.log('create check response', key);
this.#checkResponse(response, queued.intermediateResponse, key); this.#checkResponse(response, queued.intermediateResponse, key);
}).catch(error => { }).catch(error => {
queued.intermediateResponse.reject(error); queued.intermediateResponse.reject(error);

View File

@ -152,7 +152,10 @@ describe('the plugin', () => {
mockDomainObject.id = mockDomainObject.identifier.key; mockDomainObject.id = mockDomainObject.identifier.key;
const fakeUpdateEvent = { const fakeUpdateEvent = {
data: JSON.stringify(mockDomainObject) data: JSON.stringify(mockDomainObject),
target: {
readyState: EventSource.CONNECTED
}
}; };
// eslint-disable-next-line require-await // eslint-disable-next-line require-await
@ -170,7 +173,6 @@ describe('the plugin', () => {
expect(provider.create).toHaveBeenCalled(); expect(provider.create).toHaveBeenCalled();
expect(provider.observe).toHaveBeenCalled(); expect(provider.observe).toHaveBeenCalled();
expect(provider.isObservingObjectChanges).toHaveBeenCalled(); expect(provider.isObservingObjectChanges).toHaveBeenCalled();
expect(provider.isObservingObjectChanges.calls.mostRecent().returnValue).toBe(true);
//Set modified timestamp it detects a change and persists the updated model. //Set modified timestamp it detects a change and persists the updated model.
mockDomainObject.modified = mockDomainObject.persisted + 1; mockDomainObject.modified = mockDomainObject.persisted + 1;
@ -181,6 +183,7 @@ describe('the plugin', () => {
expect(updatedResult).toBeTrue(); expect(updatedResult).toBeTrue();
expect(provider.update).toHaveBeenCalled(); expect(provider.update).toHaveBeenCalled();
expect(provider.fetchChanges).toHaveBeenCalled(); expect(provider.fetchChanges).toHaveBeenCalled();
expect(provider.isObservingObjectChanges.calls.mostRecent().returnValue).toBe(true);
sharedWorkerCallback(fakeUpdateEvent); sharedWorkerCallback(fakeUpdateEvent);
expect(provider.onEventMessage).toHaveBeenCalled(); expect(provider.onEventMessage).toHaveBeenCalled();

View File

@ -25,7 +25,7 @@ const exportPNG = {
name: 'Export as PNG', name: 'Export as PNG',
key: 'export-as-png', key: 'export-as-png',
description: 'Export This View\'s Data as PNG', description: 'Export This View\'s Data as PNG',
cssClass: 'c-icon-button icon-download', cssClass: 'icon-download',
group: 'view', group: 'view',
invoke(objectPath, view) { invoke(objectPath, view) {
view.getViewContext().exportPNG(); view.getViewContext().exportPNG();
@ -36,7 +36,7 @@ const exportJPG = {
name: 'Export as JPG', name: 'Export as JPG',
key: 'export-as-jpg', key: 'export-as-jpg',
description: 'Export This View\'s Data as JPG', description: 'Export This View\'s Data as JPG',
cssClass: 'c-icon-button icon-download', cssClass: 'icon-download',
group: 'view', group: 'view',
invoke(objectPath, view) { invoke(objectPath, view) {
view.getViewContext().exportJPG(); view.getViewContext().exportJPG();

View File

@ -34,6 +34,12 @@ export default class Model extends EventEmitter {
*/ */
constructor(options) { constructor(options) {
super(); super();
Object.defineProperty(this, '_events', {
value: this._events,
enumerable: false,
configurable: false,
writable: true
});
//need to do this as we're already extending EventEmitter //need to do this as we're already extending EventEmitter
eventHelpers.extend(this); eventHelpers.extend(this);

View File

@ -27,6 +27,7 @@ import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPo
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy'; import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
import PlotViewActions from "./actions/ViewActions"; import PlotViewActions from "./actions/ViewActions";
import StackedPlotsInspectorViewProvider from "./inspector/StackedPlotsInspectorViewProvider"; import StackedPlotsInspectorViewProvider from "./inspector/StackedPlotsInspectorViewProvider";
import stackedPlotConfigurationInterceptor from "./stackedPlot/stackedPlotConfigurationInterceptor";
export default function () { export default function () {
return function install(openmct) { return function install(openmct) {
@ -64,6 +65,8 @@ export default function () {
priority: 890 priority: 890
}); });
stackedPlotConfigurationInterceptor(openmct);
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct)); openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct)); openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(openmct)); openmct.objectViews.addProvider(new PlotViewProvider(openmct));

View File

@ -20,11 +20,19 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
// this will be called from the test suite with export default function stackedPlotConfigurationInterceptor(openmct) {
// await page.addInitScript({ path: path.join(__dirname, 'addInitGauge.js') });
// it will install the Gauge since it is not installed by default
document.addEventListener('DOMContentLoaded', () => { openmct.objects.addGetInterceptor({
const openmct = window.openmct; appliesTo: (identifier, domainObject) => {
openmct.install(openmct.plugins.Gauge()); return domainObject && domainObject.type === 'telemetry.plot.stacked';
}); },
invoke: (identifier, object) => {
if (object && object.configuration && object.configuration.series === undefined) {
object.configuration.series = [];
}
return object;
}
});
}

View File

@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import DefaultClock from '../../utils/clock/DefaultClock'; import DefaultClock from '../../utils/clock/DefaultClock';
import remoteClockRequestInterceptor from './requestInterceptor';
/** /**
* A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the
@ -49,6 +50,14 @@ export default class RemoteClock extends DefaultClock {
this.lastTick = 0; this.lastTick = 0;
this.openmct.telemetry.addRequestInterceptor(
remoteClockRequestInterceptor(
this.openmct,
this.identifier,
this.#waitForReady.bind(this)
)
);
this._processDatum = this._processDatum.bind(this); this._processDatum = this._processDatum.bind(this);
} }
@ -129,4 +138,25 @@ export default class RemoteClock extends DefaultClock {
return timeFormatter.parse(datum); return timeFormatter.parse(datum);
}; };
} }
/**
* Waits for the clock to have a non-default tick value.
*
* @private
*/
#waitForReady() {
const waitForInitialTick = (resolve) => {
if (this.lastTick > 0) {
const offsets = this.openmct.time.clockOffsets();
resolve({
start: this.lastTick + offsets.start,
end: this.lastTick + offsets.end
});
} else {
setTimeout(() => waitForInitialTick(resolve), 100);
}
};
return new Promise(waitForInitialTick);
}
} }

View File

@ -0,0 +1,46 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
function remoteClockRequestInterceptor(openmct, remoteClockIdentifier, waitForBounds) {
let remoteClockLoaded = false;
return {
appliesTo: () => {
// Get the activeClock from the Global Time Context
const { activeClock } = openmct.time.getContextForView();
return activeClock !== undefined
&& activeClock.key === 'remote-clock'
&& !remoteClockLoaded;
},
invoke: async (request) => {
const { start, end } = await waitForBounds();
remoteClockLoaded = true;
request.start = start;
request.end = end;
return request;
}
};
}
export default remoteClockRequestInterceptor;

View File

@ -78,7 +78,7 @@ class StaticModelProvider {
} }
parseTreeLeaf(leafKey, leafValue, idMap, namespace) { parseTreeLeaf(leafKey, leafValue, idMap, namespace) {
if (!leafValue) { if (leafValue === null || leafValue === undefined) {
return leafValue; return leafValue;
} }

View File

@ -225,9 +225,7 @@ define(
sortBy(sortOptions) { sortBy(sortOptions) {
if (arguments.length > 0) { if (arguments.length > 0) {
this.sortOptions = sortOptions; this.sortOptions = sortOptions;
performance.mark('table:row:sort:start');
this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction); this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction);
performance.mark('table:row:sort:stop');
this.emit('sort'); this.emit('sort');
} }

View File

@ -612,7 +612,6 @@ export default {
this.calculateScrollbarWidth(); this.calculateScrollbarWidth();
}, },
sortBy(columnKey) { sortBy(columnKey) {
performance.mark('table:sort');
// If sorting by the same column, flip the sort direction. // If sorting by the same column, flip the sort direction.
if (this.sortOptions.key === columnKey) { if (this.sortOptions.key === columnKey) {
if (this.sortOptions.direction === 'asc') { if (this.sortOptions.direction === 'asc') {
@ -669,7 +668,6 @@ export default {
this.setHeight(); this.setHeight();
}, },
rowsAdded(rows) { rowsAdded(rows) {
performance.mark('row:added');
this.setHeight(); this.setHeight();
let sizingRow; let sizingRow;
@ -691,7 +689,6 @@ export default {
this.updateVisibleRows(); this.updateVisibleRows();
}, },
rowsRemoved(rows) { rowsRemoved(rows) {
performance.mark('row:removed');
this.setHeight(); this.setHeight();
this.updateVisibleRows(); this.updateVisibleRows();
}, },

View File

@ -135,7 +135,7 @@ describe("the plugin", () => {
let tableInstance; let tableInstance;
let mockClock; let mockClock;
beforeEach(() => { beforeEach(async () => {
openmct.time.timeSystem('utc', { openmct.time.timeSystem('utc', {
start: 0, start: 0,
end: 4 end: 4
@ -210,16 +210,8 @@ describe("the plugin", () => {
'some-other-key': 'some-other-value 3' 'some-other-key': 'some-other-value 3'
} }
]; ];
let telemetryPromiseResolve;
let telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
historicalProvider.request = () => { historicalProvider.request = () => Promise.resolve(testTelemetry);
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
};
openmct.router.path = [testTelemetryObject]; openmct.router.path = [testTelemetryObject];
@ -230,7 +222,7 @@ describe("the plugin", () => {
tableInstance = tableView.getTable(); tableInstance = tableView.getTable();
return telemetryPromise.then(() => Vue.nextTick()); await Vue.nextTick();
}); });
afterEach(() => { afterEach(() => {
@ -255,13 +247,10 @@ describe("the plugin", () => {
}); });
it("Renders a row for every telemetry datum returned", (done) => { it("Renders a row for every telemetry datum returned", async () => {
let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
Vue.nextTick(() => { await Vue.nextTick();
expect(rows.length).toBe(3); expect(rows.length).toBe(3);
done();
});
}); });
it("Renders a column for every item in telemetry metadata", () => { it("Renders a column for every item in telemetry metadata", () => {
@ -273,7 +262,7 @@ describe("the plugin", () => {
expect(headers[3].innerText).toBe('Another attribute'); expect(headers[3].innerText).toBe('Another attribute');
}); });
it("Supports column reordering via drag and drop", () => { it("Supports column reordering via drag and drop", async () => {
let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th'); let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
let fromColumn = columns[0]; let fromColumn = columns[0];
let toColumn = columns[1]; let toColumn = columns[1];
@ -292,54 +281,43 @@ describe("the plugin", () => {
toColumn.dispatchEvent(dragOverEvent); toColumn.dispatchEvent(dragOverEvent);
toColumn.dispatchEvent(dropEvent); toColumn.dispatchEvent(dropEvent);
return Vue.nextTick().then(() => { await Vue.nextTick();
columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th'); columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
let firstColumn = columns[0]; let firstColumn = columns[0];
let secondColumn = columns[1]; let secondColumn = columns[1];
let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText; let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText; let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
expect(fromColumnText).not.toEqual(firstColumnText);
expect(fromColumnText).not.toEqual(firstColumnText); expect(fromColumnText).toEqual(secondColumnText);
expect(fromColumnText).toEqual(secondColumnText); expect(toColumnText).not.toEqual(secondColumnText);
expect(toColumnText).not.toEqual(secondColumnText); expect(toColumnText).toEqual(firstColumnText);
expect(toColumnText).toEqual(firstColumnText);
});
}); });
it("Supports filtering telemetry by regular text search", () => { it("Supports filtering telemetry by regular text search", async () => {
tableInstance.tableRows.setColumnFilter("some-key", "1"); tableInstance.tableRows.setColumnFilter("some-key", "1");
await Vue.nextTick();
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
return Vue.nextTick().then(() => { expect(filteredRowElements.length).toEqual(1);
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); tableInstance.tableRows.setColumnFilter("some-key", "");
await Vue.nextTick();
expect(filteredRowElements.length).toEqual(1); let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
tableInstance.tableRows.setColumnFilter("some-key", "");
return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
});
});
}); });
it("Supports filtering using Regex", () => { it("Supports filtering using Regex", async () => {
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$"); tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$");
await Vue.nextTick();
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
return Vue.nextTick().then(() => { expect(filteredRowElements.length).toEqual(0);
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(0); tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
await Vue.nextTick();
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value"); expect(allRowElements.length).toEqual(3);
return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
});
});
}); });
it("displays the correct number of column headers when the configuration is mutated", async () => { it("displays the correct number of column headers when the configuration is mutated", async () => {
@ -402,7 +380,7 @@ describe("the plugin", () => {
expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
const currentBounds = openmct.time.bounds(); const currentBounds = openmct.time.bounds();
await Vue.nextTick();
const newBounds = { const newBounds = {
start: currentBounds.start, start: currentBounds.start,
end: currentBounds.end - 3 end: currentBounds.end - 3
@ -410,17 +388,10 @@ describe("the plugin", () => {
// Manually change the time bounds // Manually change the time bounds
openmct.time.bounds(newBounds); openmct.time.bounds(newBounds);
await Vue.nextTick(); await Vue.nextTick();
// Verify table is no longer paused // Verify table is no longer paused
expect(element.querySelector('div.c-table.is-paused')).toBeNull(); expect(element.querySelector('div.c-table.is-paused')).toBeNull();
await Vue.nextTick();
// Verify table displays the correct number of rows within the new bounds
const tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
expect(tableRows.length).toEqual(2);
}); });
it("Unpauses the table on user bounds change if paused by button", async () => { it("Unpauses the table on user bounds change if paused by button", async () => {
@ -428,19 +399,18 @@ describe("the plugin", () => {
// Pause by button // Pause by button
viewContext.togglePauseByButton(); viewContext.togglePauseByButton();
await Vue.nextTick(); await Vue.nextTick();
// Verify table is paused // Verify table is paused
expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
const currentBounds = openmct.time.bounds(); const currentBounds = openmct.time.bounds();
await Vue.nextTick();
const newBounds = { const newBounds = {
start: currentBounds.start, start: currentBounds.start,
end: currentBounds.end - 3 end: currentBounds.end - 1
}; };
// Manually change the time bounds // Manually change the time bounds
openmct.time.bounds(newBounds); openmct.time.bounds(newBounds);
@ -448,12 +418,6 @@ describe("the plugin", () => {
// Verify table is no longer paused // Verify table is no longer paused
expect(element.querySelector('div.c-table.is-paused')).toBeNull(); expect(element.querySelector('div.c-table.is-paused')).toBeNull();
await Vue.nextTick();
// Verify table displays the correct number of rows within the new bounds
const tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
expect(tableRows.length).toEqual(2);
}); });
it("Does not unpause the table on tick", async () => { it("Does not unpause the table on tick", async () => {

View File

@ -223,15 +223,16 @@ export default {
}, },
resizeSoView() { resizeSoView() {
let cW = this.$refs.soView.offsetWidth; let cW = this.$refs.soView.offsetWidth;
let widths = [220, 600];
let wClass = ''; let wClass = '';
if (cW < 220) { for (let width of widths) {
wClass = CSS_WIDTH_LESS_STR + '220'; if (cW < width) {
} else if (cW < 600) { wClass = wClass.concat(' ', CSS_WIDTH_LESS_STR, width);
wClass = CSS_WIDTH_LESS_STR + '600'; }
} }
this.widthClass = wClass; this.widthClass = wClass.trimStart();
} }
} }
}; };

View File

@ -11,6 +11,7 @@
margin-bottom: $interiorMarginSm; margin-bottom: $interiorMarginSm;
overflow: hidden; overflow: hidden;
padding: 3px; padding: 3px;
@include smallerControlButtons; // Make button in frame headers a bit smaller
.c-object-label { .c-object-label {
font-size: 1.05em; font-size: 1.05em;
@ -132,8 +133,6 @@
} }
} }
@include smallerControlButtons;
&.has-complex-content { &.has-complex-content {
> .c-so-view__view-large { display: block; } > .c-so-view__view-large { display: block; }
} }

View File

@ -2,7 +2,7 @@
// <a> tag and draggable element that holds type icon and name. // <a> tag and draggable element that holds type icon and name.
// Used mostly in trees and lists // Used mostly in trees and lists
display: flex; display: flex;
align-items: baseline; // Provides better vertical alignment than center align-items: center;
flex: 0 1 auto; flex: 0 1 auto;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;

View File

@ -107,7 +107,12 @@ export default {
this.preview(); this.preview();
} else { } else {
const objectPath = this.result.originalPath; const objectPath = this.result.originalPath;
const resultUrl = objectPathToUrl(this.openmct, objectPath); let resultUrl = objectPathToUrl(this.openmct, objectPath);
// get rid of ROOT if extant
if (resultUrl.includes('/ROOT')) {
resultUrl = resultUrl.split('/ROOT').join('');
}
this.openmct.router.navigate(resultUrl); this.openmct.router.navigate(resultUrl);
} }
}, },

View File

@ -50,6 +50,10 @@ class ApplicationRouter extends EventEmitter {
this.started = false; this.started = false;
this.setHash = _.debounce(this.setHash.bind(this), 300); this.setHash = _.debounce(this.setHash.bind(this), 300);
openmct.once('destroy', () => {
this.destroy();
});
} }
// Public Methods // Public Methods

View File

@ -7,12 +7,19 @@ const webpack = require('webpack');
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const {VueLoaderPlugin} = require('vue-loader'); const {VueLoaderPlugin} = require('vue-loader');
const gitRevision = require('child_process') let gitRevision = 'error-retrieving-revision';
.execSync('git rev-parse HEAD') let gitBranch = 'error-retrieving-branch';
.toString().trim();
const gitBranch = require('child_process') try {
.execSync('git rev-parse --abbrev-ref HEAD') gitRevision = require('child_process')
.toString().trim(); .execSync('git rev-parse HEAD')
.toString().trim();
gitBranch = require('child_process')
.execSync('git rev-parse --abbrev-ref HEAD')
.toString().trim();
} catch (err) {
console.warn(err);
}
/** @type {import('webpack').Configuration} */ /** @type {import('webpack').Configuration} */
const config = { const config = {

View File

@ -2,12 +2,12 @@
// instrumentation using babel-plugin-istanbul (see babel.coverage.js) // instrumentation using babel-plugin-istanbul (see babel.coverage.js)
const config = require('./webpack.dev'); const config = require('./webpack.dev');
const path = require('path'); const path = require('path');
config.devtool = false;
const vueLoaderRule = config.module.rules.find(r => r.use === 'vue-loader'); const vueLoaderRule = config.module.rules.find(r => r.use === 'vue-loader');
// eslint-disable-next-line no-undef
const CI = process.env.CI === 'true';
config.devtool = CI ? false : undefined;
vueLoaderRule.use = { vueLoaderRule.use = {
loader: 'vue-loader' loader: 'vue-loader'