Compare commits

..

1 Commits

Author SHA1 Message Date
27f5243a56 plot w/ two telemetries in inspector 2022-05-24 14:44:56 -07:00
280 changed files with 3316 additions and 14522 deletions

View File

@ -23,7 +23,7 @@ commands:
- node/install:
install-npm: true
node-version: << parameters.node-version >>
- run: npm install --prefer-offline --no-audit --progress=false
- run: npm install
restore_cache_cmd:
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
parameters:
@ -31,7 +31,7 @@ commands:
type: string
steps:
- when:
condition:
condition:
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
steps:
- restore_cache:
@ -41,7 +41,7 @@ commands:
parameters:
node-version:
type: string
steps:
steps:
- save_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
paths:
@ -61,7 +61,7 @@ commands:
upload_code_covio:
description: "Command to upload code coverage reports to codecov.io"
steps:
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
orbs:
node: circleci/node@4.9.0
browser-tools: circleci/browser-tools@1.3.0
@ -101,7 +101,7 @@ jobs:
equal: [ "FirefoxESR", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox:
version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
- when:
condition:
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
@ -128,42 +128,32 @@ jobs:
suite:
type: string
executor: pw-focal-development
parallelism: 4
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
- run: npx playwright install
- run: npm run test:e2e:<<parameters.suite>>
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- generate_and_store_version_and_filesystem_artifacts
perf-test:
parameters:
node-version:
type: string
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm run test:perf
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- generate_and_store_version_and_filesystem_artifacts
workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- lint:
name: node16-lint
node-version: lts/gallium
- unit-test:
- unit-test:
name: node14-chrome
node-version: lts/fermium
browser: ChromeHeadless
post-steps:
- upload_code_covio
- unit-test:
name: node16-chrome
node-version: lts/gallium
browser: ChromeHeadless
- unit-test:
name: node18-chrome
node-version: "18"
@ -172,8 +162,6 @@ workflows:
name: e2e-ci
node-version: lts/gallium
suite: ci
- perf-test:
node-version: lts/gallium
the-nightly: #These jobs do not run on PRs, but against master at night
jobs:
- unit-test:

View File

@ -29,7 +29,6 @@ module.exports = {
"you-dont-need-lodash-underscore/omit": "off",
"you-dont-need-lodash-underscore/throttle": "off",
"you-dont-need-lodash-underscore/flatten": "off",
"you-dont-need-lodash-underscore/get": "off",
"no-bitwise": "error",
"curly": "error",
"eqeqeq": "error",

View File

@ -27,7 +27,7 @@ assignees: ''
#### Environment
<!--- If encountered on local machine, execute the following:
<!--- npx envinfo --system --browsers --npmPackages --binaries --markdown -->
<!--- npx envinfo --system --browsers --npmPackages --binaries --languages --markdown -->
* Open MCT Version: <!--- date of build, version, or SHA -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
* OS:
@ -41,7 +41,6 @@ assignees: ''
- [ ] Does this impact a critical component?
- [ ] Is this just a visual bug with no functional impact?
- [ ] Does this block the execution of e2e tests?
- [ ] Does this have an impact on Performance?
#### Additional Information
<!--- Include any screenshots, gifs, or logs which will expedite triage -->

View File

@ -1,12 +1,4 @@
/* eslint-disable no-undef */
module.exports = {
"extends": ["plugin:playwright/playwright-test"],
"overrides": [
{
"files": ["tests/visual/*.spec.js"],
"rules": {
"playwright/no-wait-for-timeout": "off"
}
}
]
"extends": ["plugin:playwright/playwright-test"]
};

View File

@ -4,29 +4,15 @@
const base = require('@playwright/test');
const { expect } = require('@playwright/test');
/**
* Takes a `ConsoleMessage` and returns a formatted string
* @param {import('@playwright/test').ConsoleMessage} msg
* @returns {String} formatted string with message type, text, url, and line and column numbers
*/
function consoleMessageToString(msg) {
const { url, lineNumber, columnNumber } = msg.location();
return `[${msg.type()}] ${msg.text()}
at (${url} ${lineNumber}:${columnNumber})`;
}
exports.test = base.test.extend({
page: async ({ baseURL, page }, use) => {
const messages = [];
page.on('console', (msg) => messages.push(msg));
page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`));
await use(page);
messages.forEach(
msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error')
);
await expect.soft(messages.toString()).not.toContain('[error]');
},
browser: async ({ playwright, browser }, use, workerInfo) => {
// Use browserless if configured
// Use browserless if configured
if (workerInfo.project.name.match(/browserless/)) {
const vBrowser = await playwright.chromium.connectOverCDP({
endpointURL: 'ws://localhost:3003'

View File

@ -9,7 +9,6 @@ const { devices } = require('@playwright/test');
const config = {
retries: 1,
testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
webServer: {
command: 'npm run start',
@ -17,15 +16,14 @@ const config = {
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
},
maxFailures: process.env.CI ? 5 : undefined, //Limits failures to 5 to reduce CI Waste
workers: 2, //Limit to 2 for CircleCI Agent
use: {
baseURL: 'http://localhost:8080/',
headless: true,
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'on-first-retry'
screenshot: 'on',
trace: 'on',
video: 'on'
},
projects: [
{
@ -55,11 +53,8 @@ const config = {
],
reporter: [
['list'],
['html', {
open: 'never',
outputFolder: '../test-results/html/'
}],
['junit', { outputFile: 'test-results/results.xml' }],
['allure-playwright'],
['github']
]
};

View File

@ -9,7 +9,6 @@ const { devices } = require('@playwright/test');
const config = {
retries: 0,
testDir: 'tests',
testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
command: 'npm run start',
@ -23,9 +22,9 @@ const config = {
baseURL: 'http://localhost:8080/',
headless: false,
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
video: 'retain-on-failure'
screenshot: 'on',
trace: 'on',
video: 'on'
},
projects: [
{
@ -55,10 +54,7 @@ const config = {
],
reporter: [
['list'],
['html', {
open: 'on-failure',
outputFolder: '../test-results'
}]
['allure-playwright']
]
};

View File

@ -1,41 +0,0 @@
/* eslint-disable no-undef */
// playwright.config.js
// @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1, //Only for debugging purposes
testDir: 'tests/performance/',
timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'npm run start',
port: 8080,
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
},
use: {
browserName: "chromium",
baseURL: 'http://localhost:8080/',
headless: Boolean(process.env.CI), //Only if running locally
ignoreHTTPSErrors: true,
screenshot: 'off',
trace: 'on-first-retry',
video: 'off'
},
projects: [
{
name: 'chrome',
use: {
browserName: 'chromium'
}
}
],
reporter: [
['list'],
['junit', { outputFile: 'test-results/results.xml' }],
['json', { outputFile: 'test-results/results.json' }]
]
};
module.exports = config;

View File

@ -4,10 +4,10 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0, // visual tests should never retry due to snapshot comparison errors
testDir: 'tests/visual',
retries: 0,
testDir: 'tests',
timeout: 90 * 1000,
workers: 1, // visual tests should never run in parallel due to test pollution
workers: 1,
webServer: {
command: 'npm run start',
port: 8080,
@ -17,7 +17,7 @@ const config = {
use: {
browserName: "chromium",
baseURL: 'http://localhost:8080/',
headless: true, // this needs to remain headless to avoid visual changes due to GPU
headless: true,
ignoreHTTPSErrors: true,
screenshot: 'on',
trace: 'off',
@ -25,7 +25,8 @@ const config = {
},
reporter: [
['list'],
['junit', { outputFile: 'test-results/results.xml' }]
['junit', { outputFile: 'test-results/results.xml' }],
['allure-playwright']
]
};

View File

@ -1 +0,0 @@
{"openmct":{"b3cee102-86dd-4c0a-8eec-4d5d276f8691":{"identifier":{"key":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":12,"y":9,"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"23ca351d-a67d-46aa-a762-290eb742d2f1"}],"layoutGrid":[10,10]},"modified":1654299875432,"location":"mine","persisted":1654299878751},"9666e7b4-be0c-47a5-94b8-99accad7155e":{"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[],"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9","visible":false},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe","visible":false},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale","visible":false}]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1},"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9"},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe"},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale"}]},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1654299840077,"location":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","persisted":1654299840078}},"rootId":"b3cee102-86dd-4c0a-8eec-4d5d276f8691"}

View File

@ -1 +0,0 @@
{"openmct":{"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d":{"identifier":{"key":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d","namespace":""},"name":"Performance Notebook","type":"notebook","configuration":{"defaultSort":"oldest","entries":{"3e31c412-33ba-4757-8ade-e9821f6ba321":{"8c8f6035-631c-45af-8c24-786c60295335":[{"id":"entry-1652815305457","createdOn":1652815305457,"createdBy":"","text":"Existing Entry 1","embeds":[]},{"id":"entry-1652815313465","createdOn":1652815313465,"createdBy":"","text":"Existing Entry 2","embeds":[]},{"id":"entry-1652815399955","createdOn":1652815399955,"createdBy":"","text":"Existing Entry 3","embeds":[]}]}},"imageMigrationVer":"v1","pageTitle":"Page","sections":[{"id":"3e31c412-33ba-4757-8ade-e9821f6ba321","isDefault":false,"isSelected":false,"name":"Section1","pages":[{"id":"8c8f6035-631c-45af-8c24-786c60295335","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"36555942-c9aa-439c-bbdb-0aaf50db50f5","isDefault":false,"isSelected":false,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"},{"id":"dab0bd1d-2c5a-405c-987f-107123d6189a","isDefault":false,"isSelected":true,"name":"Section2","pages":[{"id":"f625a86a-cb99-4898-8082-80543c8de534","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"e77ef810-f785-42a7-942e-07e999b79c59","isDefault":false,"isSelected":true,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"}],"sectionTitle":"Section","type":"General","showTime":"0"},"modified":1652815915219,"location":"mine","persisted":1652815915222}},"rootId":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"}

View File

@ -43,6 +43,8 @@ test.describe('forms set', () => {
await page.fill('text=Properties Title Notes >> input[type="text"]', '');
// Press Tab
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
// Click text=OK Cancel
await page.click('text=OK', { force: true });
const okButton = page.locator('text=OK');

View File

@ -39,10 +39,6 @@ test.describe('Move item tests', () => {
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
@ -58,10 +54,6 @@ test.describe('Move item tests', () => {
await page.locator('li.icon-folder').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
@ -80,8 +72,10 @@ test.describe('Move item tests', () => {
});
await page.locator('li.icon-move').click();
await page.locator('form[name="mctForm"] >> text=My Items').click();
await page.locator('text=OK').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Expect that Folder 2 is in My Items, the root folder
expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy();
@ -96,11 +90,10 @@ test.describe('Move item tests', () => {
await page.locator('li:has-text("Telemetry Table")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await page.locator('text=OK').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Finish editing and save Telemetry Table
await page.locator('.c-button--menu.c-button--major.icon-save').click();
@ -121,7 +114,10 @@ test.describe('Move item tests', () => {
// Continue test regardless of assertion and create it in My Items
await page.locator('form[name="mctForm"] >> text=My Items').click();
await page.locator('text=OK').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Open My Items
await page.locator('text=Open MCT My Items >> span').nth(3).click();

View File

@ -1,177 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to performance tests to ensure that testability of performance
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here
TODO:
- Update resolution of performance config
- Add Performance Observer on init to push all performance marks
- Move client CDP connection to before or to a fixture
-
*/
const { test, expect } = require('@playwright/test');
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
test.describe('Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
// Click text=Import from JSON
await page.locator('text=Import from JSON').click();
// Upload Performance Display Layout.json
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('text=OK').click();
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
//Create a Chrome Performance Timeline trace to store as a test artifact
console.log("\n==== Devtools: startTracing ====\n");
await browser.startTracing(page, {
path: `${testInfo.outputPath()}-trace.json`,
screenshots: true
});
});
test.afterEach(async ({ page, browser}) => {
console.log("\n==== Devtools: stopTracing ====\n");
await browser.stopTracing();
/* Measurement Section
/ The following section includes a block of performance measurements.
*/
//Get time difference between viewlarge actionability and evaluate time
await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test")));
//Get StartTime
const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
console.log('window.performance.timing.navigationStart', startTime);
//Get All Performance Marks
const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
const getAllMarks = JSON.parse(getAllMarksJson);
console.log('window.performance.getEntriesByType("mark")', getAllMarks);
//Get All Performance Measures
const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
const getAllMeasures = JSON.parse(getAllMeasuresJson);
console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
});
/* The following test will navigate to a previously created Performance Display Layout and measure the
/ following metrics:
/ - ElementResourceTiming
/ - Interaction Timing
*/
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
const client = await page.context().newCDPSession(page);
// Tell the DevTools session to record performance metrics
// https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
await client.send('Performance.enable');
// Go to baseURL
await page.goto('/');
// Search Available after Launch
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.evaluate(() => window.performance.mark("search-available"));
// Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout');
await page.evaluate(() => window.performance.mark("search-entered"));
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Display Layout")').first().click(),
page.evaluate(() => window.performance.mark("click-search-result"))
]);
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
//Get background-image url from background-image css prop
const backgroundImage = await page.locator('.c-imagery__main-image__background-image');
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre
console.log('backgroundImageurl ' + backgroundImageUrl);
//Get ResourceTiming of background-image jpg
const resourceTimingJson = await page.evaluate((bgImageUrl) =>
JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()),
backgroundImageUrl
);
console.log('resourceTimingJson ' + resourceTimingJson);
//Open Large view
await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start'
await page.evaluate(() => window.performance.mark("viewLarge.start.test")); //This is a mark only to compare evaluate timing
//Time to Imagery Rendered in Large Frame
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
await page.evaluate(() => window.performance.mark("background-image-frame"));
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
await page.evaluate(() => window.performance.mark("background-image-visible"));
// Get Current number of images in thumbstrip
await page.waitForSelector('.c-imagery__thumb');
const thumbCount = await page.locator('.c-imagery__thumb').count();
console.log('number of thumbs rendered ' + thumbCount);
await page.locator('.c-imagery__thumb').last().click();
//Get ResourceTiming of all jpg resources
const resourceTimingJson2 = await page.evaluate(() =>
JSON.stringify(window.performance.getEntriesByType('resource'))
);
const resourceTiming = JSON.parse(resourceTimingJson2);
const jpgResourceTiming = resourceTiming.find((element) =>
element.name.includes('.jpg')
);
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
// Click Close Icon
await page.locator('[aria-label="Close"]').click();
await page.evaluate(() => window.performance.mark("view-large-close-button"));
//await client.send('HeapProfiler.enable');
await client.send('HeapProfiler.collectGarbage');
let performanceMetrics = await client.send('Performance.getMetrics');
console.log(performanceMetrics.metrics);
});
});

View File

@ -1,119 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is an initial example for memory leak testing using performance. This configuration and execution must
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
or profiling playwright and/or the browser.
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
and https://github.com/paulirish/automated-chrome-profiling/issues/3
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
*/
const { test, expect } = require('@playwright/test');
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
// eslint-disable-next-line playwright/no-skipped-test
test.describe.skip('Memory Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
// Click text=Import from JSON
await page.locator('text=Import from JSON').click();
// Upload Performance Display Layout.json
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('text=OK').click();
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
});
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
await page.goto('/', {waitUntil: 'networkidle'});
// To to Search Available after Launch
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout');
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Display Layout")').first().click()
]);
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
const client = await page.context().newCDPSession(page);
await client.send('HeapProfiler.enable');
await client.send('HeapProfiler.startSampling');
// await client.send('HeapProfiler.collectGarbage');
await client.send('Performance.enable');
let performanceMetricsBefore = await client.send('Performance.getMetrics');
console.log(performanceMetricsBefore.metrics);
//await client.send('Performance.disable');
//Open Large view
await page.locator('button:has-text("Large View")').click();
await client.send('HeapProfiler.takeHeapSnapshot');
//Time to Imagery Rendered in Large Frame
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
// Click Close Icon
await page.locator('.c-click-icon').click();
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
await client.send('HeapProfiler.collectGarbage');
//await client.send('Performance.enable');
let performanceMetricsAfter = await client.send('Performance.getMetrics');
console.log(performanceMetricsAfter.metrics);
//await client.send('Performance.disable');
});
});

View File

@ -1,158 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to performance tests to ensure that testability of performance
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here.
TODO:
- Update resolution of performance config
- Add Performance Observer on init to push all performance marks
- Move client CDP connection to before or to a fixture
*/
const { test, expect } = require('@playwright/test');
const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json';
test.describe('Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
// Click text=Import from JSON
await page.locator('text=Import from JSON').click();
// Upload Performance Display Layout.json
await page.setInputFiles('#fileElem', notebookFilePath);
// TODO Fix this
await page.locator('text=OK >> nth=1').click();
await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible();
//Create a Chrome Performance Timeline trace to store as a test artifact
console.log("\n==== Devtools: startTracing ====\n");
await browser.startTracing(page, {
path: `${testInfo.outputPath()}-trace.json`,
screenshots: true
});
});
test.afterEach(async ({ page, browser}) => {
console.log("\n==== Devtools: stopTracing ====\n");
await browser.stopTracing();
/* Measurement Section
/ The following section includes a block of performance measurements.
*/
const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
console.log('window.performance.timing.navigationStart', startTime);
//Get All Performance Marks
const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
const getAllMarks = JSON.parse(getAllMarksJson);
console.log('window.performance.getEntriesByType("mark")', getAllMarks);
//Get All Performance Measures
const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
const getAllMeasures = JSON.parse(getAllMeasuresJson);
console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
});
/* The following test will navigate to a previously created Performance Display Layout and measure the
/ following metrics:
/ - ElementResourceTiming
/ - Interaction Timing
*/
test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => {
const client = await page.context().newCDPSession(page);
// Tell the DevTools session to record performance metrics
// https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
await client.send('Performance.enable');
// Go to baseURL
await page.goto('/');
// To to Search Available after Launch
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.evaluate(() => window.performance.mark("search-available"));
// Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Notebook');
await page.evaluate(() => window.performance.mark("search-entered"));
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Notebook")').first().click(),
page.evaluate(() => window.performance.mark("click-search-result"))
]);
await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {state: 'hidden'});
await page.evaluate(() => window.performance.mark("search-spinner-gone"));
await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible'});
await page.evaluate(() => window.performance.mark("object-title-appears"));
await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible'});
await page.evaluate(() => window.performance.mark("notebook-entry-appears"));
// Click Add new Notebook Entry
await page.locator('.c-notebook__drag-area').click();
await page.evaluate(() => window.performance.mark("new-notebook-entry-created"));
// Enter Notebook Entry text
await page.locator('div.c-ne__text').last().fill('New Entry');
await page.keyboard.press('Enter');
await page.evaluate(() => window.performance.mark("new-notebook-entry-filled"));
//Individual Notebook Entry Search
await page.evaluate(() => window.performance.mark("notebook-search-start"));
await page.locator('.c-notebook__search >> input').fill('Existing Entry');
await page.evaluate(() => window.performance.mark("notebook-search-filled"));
await page.waitForSelector('text=Search Results (3)', { state: 'visible'});
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible'});
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
//Clear Search
await page.locator('.c-search.c-notebook__search .c-search__clear-input').click();
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
// Hover on Last
await page.evaluate(() => window.performance.mark("new-notebook-entry-delete"));
await page.locator('div.c-ne__time-and-content').last().hover();
await page.locator('button[title="Delete this entry"]').last().click();
await page.locator('button:has-text("Ok")').click();
await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached'});
await page.evaluate(() => window.performance.mark("new-notebook-entry-deleted"));
//await client.send('HeapProfiler.enable');
await client.send('HeapProfiler.collectGarbage');
let performanceMetrics = await client.send('Performance.getMetrics');
console.log(performanceMetrics.metrics);
});
});

View File

@ -46,22 +46,22 @@ test.describe('Clock Generator', () => {
// Click .icon-arrow-down
await page.locator('.icon-arrow-down').click();
//verify if the autocomplete dropdown is visible
await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
await expect(page.locator(".optionPreSelected")).toBeVisible();
// Click .icon-arrow-down
await page.locator('.icon-arrow-down').click();
// Verify clicking on the autocomplete arrow collapses the dropdown
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
await expect(page.locator(".optionPreSelected")).not.toBeVisible();
// Click timezone input to open dropdown
await page.locator('.c-input--autocomplete__input').click();
await page.locator('.autocompleteInput').click();
//verify if the autocomplete dropdown is visible
await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
await expect(page.locator(".optionPreSelected")).toBeVisible();
// Verify clicking outside the autocomplete dropdown collapses it
await page.locator('text=Timezone').click();
// Verify clicking on the autocomplete arrow collapses the dropdown
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
await expect(page.locator(".optionPreSelected")).not.toBeVisible();
});
});

View File

@ -32,40 +32,39 @@ const { expect } = require('@playwright/test');
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Condition Set
await page.click('text=Condition Set');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.click('text=OK')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Save localStorage for future test execution
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
//Set object identifier from url
conditionSetUrl = await page.url();
console.log('conditionSetUrl ' + conditionSetUrl);
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
});
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Condition Set
await page.locator('li:has-text("Condition Set")').click();
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.click('text=OK')
]);
//Save localStorage for future test execution
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
//Set object identifier from url
conditionSetUrl = await page.url();
console.log('conditionSetUrl ' + conditionSetUrl);
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
});
test.afterAll(async ({ browser }) => {
await browser.close();
});
//Load localStorage for subsequent tests
test.use({ storageState: './e2e/tests/recycled_storage.json' });
//Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
//Navigate to baseURL with injected localStorage
@ -122,7 +121,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page
@ -146,31 +145,35 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
await page.locator('input[type="search"]').fill('Renamed');
await 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 }) => {
//Navigate to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
//Expect Unnamed Condition Set to be visible in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible();
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
// Search for Unnamed Condition Set
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set');
// Click Search Result
await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click();
// Click hamburger button
await page.locator('[title="More options"]').click();
// Click text=Remove
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Right Click to Open Actions Menu
await page.locator('a:has-text("Unnamed Condition Set")').click({
button: 'right'
});
// Click Remove Action
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
//Expect Unnamed Condition Set to be removed in Main View
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1);
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
await page.locator('.c-search__clear-input').click();
// Search for Unnamed Condition Set
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Expect Unnamed Condition Set to be removed
await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
//Feature?
//Domain Object is still available by direct URL after delete

View File

@ -32,6 +32,7 @@ const { expect } = require('@playwright/test');
test.describe('Example Imagery', () => {
test.beforeEach(async ({ page }) => {
page.on('console', msg => console.log(msg.text()));
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
@ -58,21 +59,21 @@ test.describe('Example Imagery', () => {
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const bgImageLocator = await page.locator(backgroundImageSelector);
const deltaYStep = 100; //equivalent to 1x zoom
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
// zoom in
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
await page.mouse.wheel(0, deltaYStep * 2);
// wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
// zoom out
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
await page.mouse.wheel(0, -deltaYStep);
// wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
@ -86,12 +87,12 @@ test.describe('Example Imagery', () => {
const deltaYStep = 100; //equivalent to 1x zoom
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover({trial: true});
const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover();
// zoom in
await page.mouse.wheel(0, deltaYStep * 2);
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
@ -149,23 +150,23 @@ test.describe('Example Imagery', () => {
});
test('Can use + - buttons to zoom on the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover({trial: true});
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0);
const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover();
const zoomInBtn = await page.locator('.t-btn-zoom-in');
const zoomOutBtn = await page.locator('.t-btn-zoom-out');
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomOutBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
@ -173,20 +174,20 @@ test.describe('Example Imagery', () => {
});
test('Can use the reset button to reset the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const bgImageLocator = await page.locator(backgroundImageSelector);
// wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0);
const zoomInBtn = await page.locator('.t-btn-zoom-in');
const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
@ -194,7 +195,7 @@ test.describe('Example Imagery', () => {
await zoomResetBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const resetBoundingBox = await bgImageLocator.boundingBox();
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
@ -208,18 +209,18 @@ test.describe('Example Imagery', () => {
const bgImageLocator = page.locator(backgroundImageSelector);
const pausePlayButton = page.locator('.c-button.pause-play');
// wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
// open the time conductor drop down
await page.locator('button:has-text("Fixed Timespan")').click();
await page.locator('.c-conductor__controls button.c-mode-button').click();
// Click local clock
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await page.locator('.icon-clock >> text=Local Clock').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
const zoomInBtn = page.locator('.t-btn-zoom-in');
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
});
@ -234,11 +235,6 @@ test.describe('Example Imagery', () => {
// ('If the imagery view is not in pause mode, it should be updated when new images come in');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test('Example Imagery in Display layout', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265'
});
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
@ -248,11 +244,9 @@ test('Example Imagery in Display layout', async ({ page }) => {
// Click text=Example Imagery
await page.click('text=Example Imagery');
// Clear and set Image load delay to minimum value
// FIXME: Update the value to 5000 ms when this bug is fixed.
// See: https://github.com/nasa/openmct/issues/5265
await page.locator('input[type="number"]').fill('');
await page.locator('input[type="number"]').fill('0');
// Clear and set Image load delay (milliseconds)
await page.click('input[type="number"]', {clickCount: 3});
await page.type('input[type="number"]', "20");
// Click text=OK
await Promise.all([
@ -261,15 +255,14 @@ test('Example Imagery in Display layout', async ({ page }) => {
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover({trial: true});
const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover();
// Click previous image button
const previousImageButton = page.locator('.c-nav--prev');
const previousImageButton = await page.locator('.c-nav--prev');
await previousImageButton.click();
// Verify previous image
@ -278,7 +271,7 @@ test('Example Imagery in Display layout', async ({ page }) => {
// Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * 2);
const zoomedBoundingBox = await bgImageLocator.boundingBox();
@ -286,7 +279,7 @@ test('Example Imagery in Display layout', async ({ page }) => {
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// Wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
@ -295,26 +288,27 @@ test('Example Imagery in Display layout', async ({ page }) => {
await page.mouse.move(imageCenterX, imageCenterY);
// Pan Imagery Hints
console.log('process.platform is ' + process.platform);
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
expect(expectedAltText).toEqual(imageryHintsText);
// Click next image button
const nextImageButton = page.locator('.c-nav--next');
const nextImageButton = await page.locator('.c-nav--next');
await nextImageButton.click();
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// Click fixed timespan button
await page.locator('.c-button__label >> text=Fixed Timespan').click();
// Select local clock mode
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// Click local clock
await page.locator('.icon-clock >> text=Local Clock').click();
// Zoom in on next image
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
await page.mouse.wheel(0, deltaYStep * 2);
// Wait for zoom animation to finish
await bgImageLocator.hover({trial: true});
await bgImageLocator.hover();
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
@ -325,44 +319,29 @@ test('Example Imagery in Display layout', async ({ page }) => {
// Verify previous image
await expect(selectedImage).toBeVisible();
const imageCount = await page.locator('.c-imagery__thumb').count();
await expect.poll(async () => {
const newImageCount = await page.locator('.c-imagery__thumb').count();
return newImageCount;
}, {
message: "verify that new images still stream in",
timeout: 6 * 1000
}).toBeGreaterThan(imageCount);
// Verify selected image is still displayed
await expect(selectedImage).toBeVisible();
// Unpause imagery
await page.locator('.pause-play').click();
// Wait 20ms to verify no new image has come in
await page.waitForTimeout(21);
//Get background-image url from background-image css prop
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
const backgroundImage = await page.locator('.c-imagery__main-image__background-image');
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
let backgroundImageUrl2;
await expect.poll(async () => {
// Verify next image has updated
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
// sleep 21ms
await page.waitForTimeout(21);
return backgroundImageUrl2;
}, {
message: "verify next image has updated",
timeout: 6 * 1000
}).not.toBe(backgroundImageUrl1);
// Verify next image has updated
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
let backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
// Expect backgroundImageUrl2 to be greater then backgroundImageUrl1
expect(backgroundImageUrl2 >= backgroundImageUrl1);
});
test.describe('Example imagery thumbnails resize in display layouts', () => {
@ -370,7 +349,7 @@ test.describe('Example imagery thumbnails resize in display layouts', () => {
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
const thumbsWrapperLocator = await page.locator('.c-imagery__thumbs-wrapper');
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
@ -425,7 +404,7 @@ test.describe('Example imagery thumbnails resize in display layouts', () => {
await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click();
// expect thumbnails not be visible when first added
expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
await expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
// Resize the example imagery vertically to change the thumbnail visibility
/*
@ -451,7 +430,9 @@ test.describe('Example imagery thumbnails resize in display layouts', () => {
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
});
});
test.describe('Example Imagery in Flexible layout', () => {

View File

@ -1,30 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
// this will be called from the test suite with
// await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
// it will install the RestrictedNotebook since it is not installed by default
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME'));
});

View File

@ -1,198 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/
const { test } = require('../../../fixtures');
test.describe('Notebook CRUD Operations', () => {
test.fixme('Can create a Notebook Object', async ({ page }) => {
//Create domain object
//Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page'
});
test.fixme('Can update a Notebook Object', async ({ page }) => {});
test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {});
test.fixme('Can Delete a Notebook Object', async ({ page }) => {
// Other than non-persistible objects
});
});
test.describe('Default Notebook', () => {
// General Default Notebook statements
// ## Useful commands:
// 1. - To check default notebook:
// `JSON.parse(localStorage.getItem('notebook-storage'));`
// 1. - Clear default notebook:
// `localStorage.setItem('notebook-storage', null);`
test.fixme('A newly created Notebook is automatically set as the default notebook if no other notebooks exist', async ({ page }) => {
//Create new notebook
//Verify Default Notebook Characteristics
});
test.fixme('A newly created Notebook is automatically set as the default notebook if at least one other notebook exists', async ({ page }) => {
//Create new notebook A
//Create second notebook B
//Verify Non-Default Notebook A Characteristics
//Verify Default Notebook B Characteristics
});
test.fixme('If a default notebook is deleted, the second most recent notebook becomes the default', async ({ page }) => {
//Create new notebook A
//Create second notebook B
//Delete Notebook B
//Verify Default Notebook A Characteristics
});
});
test.describe('Notebook section tests', () => {
//The following test cases are associated with Notebook Sections
test.fixme('New sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => {
//Create new notebook A
//Add section
//Verify new section and new page details
});
test.fixme('Section selection operations and associated behavior', async ({ page }) => {
//Create new notebook A
//Add Sections until 6 total with no default section/page
//Select 3rd section
//Delete 4th section
//3rd section is still selected
//Delete 3rd section
//1st section is selected
//Set 3rd section as default
//Delete 2nd section
//3rd section is still default
//Delete 3rd section
//1st is selected and there is no default notebook
});
});
test.describe('Notebook page tests', () => {
//The following test cases are associated with Notebook Pages
test.fixme('Page selection operations and associated behavior', async ({ page }) => {
//Create new notebook A
//Delete existing Page
//New 'Unnamed Page' automatically created
//Create 6 total Pages without a default page
//Select 3rd
//Delete 3rd
//First is now selected
//Set 3rd as default
//Select 2nd page
//Delete 2nd page
//3rd (default) is now selected
//Set 3rd as default page
//Select 3rd (default) page
//Delete 3rd page
//First is now selected and there is no default notebook
});
});
test.describe('Notebook search tests', () => {
test.fixme('Can search for a single result', async ({ page }) => {});
test.fixme('Can search for many results', async ({ page }) => {});
test.fixme('Can search for new and recently modified entries', async ({ page }) => {});
test.fixme('Can search for section text', async ({ page }) => {});
test.fixme('Can search for page text', async ({ page }) => {});
test.fixme('Can search for entry text', async ({ page }) => {});
});
test.describe('Notebook entry tests', () => {
test.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
test.fixme('When a telemetry object is dropped into a notebook, a new entry is created and it should be focused', async ({ page }) => {
// Drag and drop any telmetry object on 'drop object'
// new entry gets created with telemtry object
});
test.fixme('When a telemetry object is dropped into a notebooks existing entry, it should be focused', async ({ page }) => {
// Drag and drop any telemetry object onto existing entry
// Entry updated with object and snapshot
});
test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
test.fixme('previous and new entries can be deleted', async ({ page }) => {});
});
test.describe('Snapshot Menu tests', () => {
test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => {
// There should be no default notebook
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
// refresh page
// Click on 'Notebook Snaphot Menu'
// 'save to Notebook Snapshots' should be only option there
});
test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
// Create 2a notebooks
// Set Notebook A as Default
// Open Snapshot Menu and note that Notebook A is listed
// Close Snapshot Menu
// Set Default Notebook to Notebook B
// Open Snapshot Notebook and note that Notebook B is listed
// Select Default Notebook Option and verify that Snapshot is added to Notebook B
});
test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
//Note this should be a visual test, too
// Create Telemetry object
// Create A notebook with many pages and sections.
// Set page and section defaults to be between first and last of many. i.e. 3 of 5
// Navigate to Telemetry object
// Select Default Notebook Option and verify that Snapshot is added to Notebook A
// Verify Snapshot Details appear correctly
});
test.fixme('Snapshots adjust time conductor', async ({ page }) => {
// Create Telemetry object
// Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
// Embed Telemetry object into notebook
// Set Time Conductor to Local clock
// Click into embedded telemetry object and verify object appears with same fixed time from record
});
});
test.describe('Snapshot Container tests', () => {
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {});
test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {});
test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});
test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
//Create Notebook
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots
//Navigate to Notebook
//Drag and Drop onto droppable area for new entry
//New Entry created with given snapshot added
//Snapshot removed from container?
});
test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => {
//Create Notebook
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots
//Navigate to Notebook
//Drag and Drop into exiting entry
//Existing Entry updated with given snapshot
//Snapshot removed from container?
});
test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
//Add snapshot to container
//Verify PNG, JPG, and Annotate buttons work correctly
});
});

View File

@ -1,264 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test } = require('../../../fixtures');
const { expect } = require('@playwright/test');
const path = require('path');
const TEST_TEXT = 'Testing text for entries.';
const TEST_TEXT_NAME = 'Test Page';
const CUSTOM_NAME = 'CUSTOM_NAME';
const COMMIT_BUTTON_TEXT = 'button:has-text("Commit Entries")';
const SINE_WAVE_GENERATOR = 'text=Unnamed Sine Wave Generator';
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
/**
* @param {import('@playwright/test').Page} page
*/
async function startAndAddNotebookObject(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=CUSTOME_NAME
await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
return;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextEntry(page) {
// Click .c-notebook__drag-area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.locator('div.c-ne__text').click();
await page.locator('div.c-ne__text').fill(TEST_TEXT);
await page.locator('div.c-ne__text').press('Enter');
return;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function dragAndDropEmbed(page) {
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Sine Wave Generator")
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click form[name="mctForm"] >> text=My Items
await page.locator('form[name="mctForm"] >> text=My Items').click();
// Click text=OK
await page.locator('text=OK').click();
// Click text=Open MCT My Items >> span >> nth=3
await page.locator('text=Open MCT My Items >> span').nth(3).click();
// Click text=Unnamed CUSTOM_NAME
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed CUSTOM_NAME').click()
]);
await page.dragAndDrop(SINE_WAVE_GENERATOR, NOTEBOOK_DROP_AREA);
return;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function lockPage(page) {
const commitButton = page.locator(COMMIT_BUTTON_TEXT);
await commitButton.click();
// confirmation dialog click
await page.locator('text=Lock Page').click();
// waiting for mutation of locked page
await new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
return;
}
/**
* @param {import('@playwright/test').Page} 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)
await page.locator('text=Open MCT My Items >> span').nth(3).click();
// Click a:has-text("Unnamed CUSTOM_NAME")
await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({
button: 'right'
});
return;
}
test.describe('Restricted Notebook', () => {
test.beforeEach(async ({ page }) => {
await startAndAddNotebookObject(page);
});
test('Can be renamed', async ({ page }) => {
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
});
test('Can be deleted if there are no locked pages', async ({ page }) => {
await openContextMenuRestrictedNotebook(page);
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('Remove');
const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
// notbook tree object exists
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
// Click text=Remove
await page.locator('text=Remove').click();
// Click text=OK
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine?tc.mode=fixed&tc.startBound=1653671067340&tc.endBound=1653672867340&tc.timeSystem=utc&view=grid' }*/),
page.locator('text=OK').click()
]);
// has been deleted
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0);
});
test('Can be locked if at least one page has one entry', async ({ page }) => {
await enterTextEntry(page);
const commitButton = page.locator(COMMIT_BUTTON_TEXT);
expect.soft(await commitButton.count()).toEqual(1);
});
});
test.describe('Restricted Notebook with at least one entry and with the page locked', () => {
test.beforeEach(async ({ page }) => {
await startAndAddNotebookObject(page);
await enterTextEntry(page);
await lockPage(page);
// open sidebar
await page.locator('button.c-notebook__toggle-nav-button').click();
});
test('Locked page should now be in a locked state', async ({ page }) => {
// main lock message on page
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
expect.soft(await lockMessage.count()).toEqual(1);
// lock icon on page in sidebar
const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock');
expect.soft(await pageLockIcon.count()).toEqual(1);
// no way to remove a restricted notebook with a locked page
await openContextMenuRestrictedNotebook(page);
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).not.toContainText('Remove');
});
test('Can still: add page, rename, add entry, delete unlocked pages', async ({ page }) => {
// Click text=Page Add >> button
await Promise.all([
page.waitForNavigation(),
page.locator('text=Page Add >> button').click()
]);
// Click text=Unnamed Page >> nth=1
await page.locator('text=Unnamed Page').nth(1).click();
// Press a with modifiers
await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME);
// expect to be able to rename unlocked pages
const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
const newPageCount = await newPageElement.count();
await newPageElement.press('Enter'); // exit contenteditable state
expect.soft(newPageCount).toEqual(1);
// enter test text
await enterTextEntry(page);
// expect new page to be lockable
const commitButton = page.locator(COMMIT_BUTTON_TEXT);
expect.soft(await commitButton.count()).toEqual(1);
// Click text=Unnamed PageTest Page >> button
await page.locator('text=Unnamed PageTest Page >> button').click();
// Click text=Delete Page
await page.locator('text=Delete Page').click();
// Click text=Ok
await Promise.all([
page.waitForNavigation(),
page.locator('text=Ok').click()
]);
// deleted page, should no longer exist
const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
expect.soft(await deletedPageElement.count()).toEqual(0);
});
});
test.describe('Restricted Notebook with a page locked and with an embed', () => {
test.beforeEach(async ({ page }) => {
await startAndAddNotebookObject(page);
await dragAndDropEmbed(page);
});
test('Allows embeds to be deleted if page unlocked', async ({ page }) => {
// Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu');
await expect.soft(embedMenu).toContainText('Remove This Embed');
});
test('Disallows embeds to be deleted if page locked', async ({ page }) => {
await lockPage(page);
// Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu');
await expect.soft(embedMenu).not.toContainText('Remove This Embed');
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -46,11 +46,8 @@ test.describe('Log plot tests', () => {
await testLogTicks(page);
//await testLogPlotPixels(page);
// FIXME: Get rid of the waitForTimeout() and lint warning exception.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1 * 1000);
// refresh page and wait for charts and ticks to load
await page.waitForTimeout(1 * 1000);
await page.reload({ waitUntil: 'networkidle'});
await page.waitForSelector('.gl-plot-chart-area');
await page.waitForSelector('.gl-plot-y-tick-label');
@ -60,9 +57,7 @@ test.describe('Log plot tests', () => {
//await testLogPlotPixels(page);
});
// Leaving test as 'TODO' for now.
// NOTE: Not eligible for community contributions.
test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
test.skip('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
await makeOverlayPlot(page);
await enableEditMode(page);
await enableLogMode(page);

View File

@ -1,101 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test } = require('../../../fixtures');
const { expect } = require('@playwright/test');
test.describe('Telemetry Table', () => {
test('unpauses when paused by button and user changes bounds', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5113'
});
const bannerMessage = '.c-message-banner__message';
const createButton = 'button:has-text("Create")';
await page.goto('/', { waitUntil: 'networkidle' });
// Click create button
await page.locator(createButton).click();
await page.locator('li:has-text("Telemetry Table")').click();
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
// Wait for Save Banner to appear
page.waitForSelector(bannerMessage)
]);
// Save (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click();
await page.locator('text=Save and Finish Editing').click();
// Click create button
await page.locator(createButton).click();
// add Sine Wave Generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
// Wait for Save Banner to appear
page.waitForSelector(bannerMessage)
]);
// focus the Telemetry Table
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Telemetry Table').first().click()
]);
// Click pause button
const pauseButton = await page.locator('button.c-button.icon-pause');
await pauseButton.click();
const tableWrapper = await page.locator('div.c-table-wrapper');
await expect(tableWrapper).toHaveClass(/is-paused/);
// Arbitrarily change end date to some time in the future
const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
await endTimeInput.click();
let endDate = await endTimeInput.inputValue();
endDate = new Date(endDate);
endDate.setUTCDate(endDate.getUTCDate() + 1);
endDate = endDate.toISOString().replace(/T.*/, '');
await endTimeInput.fill('');
await endTimeInput.fill(endDate);
await page.keyboard.press('Enter');
await expect(tableWrapper).not.toHaveClass(/is-paused/);
});
});

View File

@ -23,9 +23,9 @@
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Time conductor operations', () => {
test.describe('Time counductor operations', () => {
test('validate start time does not exceeds end time', async ({ page }) => {
// Go to baseURL
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
const year = new Date().getFullYear();
@ -73,163 +73,37 @@ test.describe('Time conductor operations', () => {
// Try to change the realtime offsets when in realtime (local clock) mode.
test.describe('Time conductor input fields real-time mode', () => {
test('validate input fields in real-time mode', async ({ page }) => {
const startOffset = {
secs: '23'
};
const endOffset = {
secs: '31'
};
// Go to baseURL
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Switch to real-time mode
await setRealTimeMode(page);
// Click fixed timespan button
await page.locator('.c-button__label >> text=Fixed Timespan').click();
// Set start time offset
await setStartOffset(page, startOffset);
// Click local clock
await page.locator('.icon-clock >> text=Local Clock').click();
// Click time offset button
await page.locator('.c-conductor__delta-button >> text=00:30:00').click();
// Input start time offset
await page.fill('.pr-time-controls__secs', '23');
// Click the check button
await page.locator('.icon-check').click();
// Verify time was updated on time offset button
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
await expect(page.locator('.c-conductor__delta-button').first()).toContainText('00:30:23');
// Set end time offset
await setEndOffset(page, endOffset);
// Click time offset set preceding now button
await page.locator('.c-conductor__delta-button >> text=00:00:30').click();
// Input preceding time offset
await page.fill('.pr-time-controls__secs', '31');
// Click the check buttons
await page.locator('.icon-check').click();
// Verify time was updated on preceding time offset button
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
});
/**
* Verify that offsets and url params are preserved when switching
* between fixed timespan and real-time mode.
*/
test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
const startOffset = {
mins: '30',
secs: '23'
};
const endOffset = {
secs: '01'
};
// Convert offsets to milliseconds
const startDelta = (30 * 60 * 1000) + (23 * 1000);
const endDelta = (1 * 1000);
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Switch to real-time mode
await setRealTimeMode(page);
// Set start time offset
await setStartOffset(page, startOffset);
// Set end time offset
await setEndOffset(page, endOffset);
// Switch to fixed timespan mode
await setFixedTimeMode(page);
// Switch back to real-time mode
await setRealTimeMode(page);
// Verify updated start time offset persists after mode switch
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
// Verify updated end time offset persists after mode switch
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
// Verify url parameters persist after mode switch
await page.waitForNavigation();
expect(page.url()).toContain(`startDelta=${startDelta}`);
expect(page.url()).toContain(`endDelta=${endDelta}`);
await expect(page.locator('.c-conductor__delta-button').nth(1)).toContainText('00:00:31');
});
});
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.icon-check').click();
}
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}

View File

@ -6,11 +6,11 @@
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1652301954635,\"end\":1652303754635}]}"
"value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1652303756008,\"modified\":1652303756007},\"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}}"
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}"
},
{
"name": "mct-tree-expanded",
@ -19,4 +19,4 @@
]
}
]
}
}

View File

@ -47,10 +47,7 @@ test.beforeEach(async ({ context }) => {
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
});
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers({
now: 0,
shouldAdvanceTime: true
}); //Set browser clock to UNIX Epoch
window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
});
});
@ -59,7 +56,8 @@ test('Visual - Root and About', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
// Verify that Create button is actionable
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
const createButtonLocator = page.locator('button:has-text("Create")');
await expect(createButtonLocator).toBeEnabled();
// Take a snapshot of the Dashboard
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
@ -173,36 +171,3 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => {
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'removed amplitude property value');
});
test('Visual - Save Successful Banner', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
//NOTE Something other than example imagery
await page.click('text=Timer');
// Click text=OK
await page.click('text=OK');
await page.locator('.c-message-banner__message').hover({ trial: true });
await percySnapshot(page, 'Banner message shown');
//Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await percySnapshot(page, 'Banner message gone');
});
test('Visual - Display Layout Icon is correct', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
//Hover on Display Layout option.
await page.locator('text=Display Layout').hover();
await percySnapshot(page, 'Display Layout Create Menu');
});

View File

@ -1,33 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import availableTags from './tags.json';
/**
* @returns {function} The plugin install function
*/
export default function exampleTagsPlugin() {
return function install(openmct) {
Object.keys(availableTags.tags).forEach(tagKey => {
const tagDefinition = availableTags.tags[tagKey];
openmct.annotation.defineTag(tagKey, tagDefinition);
});
};
}

View File

@ -1,19 +0,0 @@
{
"tags": {
"46a62ad1-bb86-4f88-9a17-2a029e12669d": {
"label": "Science",
"backgroundColor": "#cc0000",
"foregroundColor": "#ffffff"
},
"65f150ef-73b7-409a-b2e8-258cbd8b7323": {
"label": "Driving",
"backgroundColor": "#ffad32",
"foregroundColor": "#333333"
},
"f156b038-c605-46db-88a6-67cf2489a371": {
"label": "Drilling",
"backgroundColor": "#b0ac4e",
"foregroundColor": "#FFFFFF"
}
}
}

View File

@ -21,56 +21,19 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
import createExampleUser from './exampleUserCreator';
const STATUSES = [{
key: "NO_STATUS",
label: "Not set",
iconClass: "icon-question-mark",
iconClassPoll: "icon-status-poll-question-mark"
}, {
key: "GO",
label: "GO",
iconClass: "icon-check",
iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-ok",
statusBgColor: "#33cc33",
statusFgColor: "#000"
}, {
key: "MAYBE",
label: "MAYBE",
iconClass: "icon-alert-triangle",
iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-warning",
statusBgColor: "#ffb66c",
statusFgColor: "#000"
}, {
key: "NO_GO",
label: "NO GO",
iconClass: "icon-circle-slash",
iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-error",
statusBgColor: "#9900cc",
statusFgColor: "#fff"
}];
/**
* @implements {StatusUserProvider}
*/
export default class ExampleUserProvider extends EventEmitter {
constructor(openmct, {defaultStatusRole} = {defaultStatusRole: undefined}) {
constructor(openmct) {
super();
this.openmct = openmct;
this.user = undefined;
this.loggedIn = false;
this.autoLoginUser = undefined;
this.status = STATUSES[1];
this.pollQuestion = undefined;
this.defaultStatusRole = defaultStatusRole;
this.ExampleUser = createExampleUser(this.openmct.user.User);
this.loginPromise = undefined;
}
isLoggedIn() {
@ -82,19 +45,11 @@ export default class ExampleUserProvider extends EventEmitter {
}
getCurrentUser() {
if (!this.loginPromise) {
this.loginPromise = this._login().then(() => this.user);
if (this.loggedIn) {
return Promise.resolve(this.user);
}
return this.loginPromise;
}
canProvideStatusForRole() {
return Promise.resolve(true);
}
canSetPollQuestion() {
return Promise.resolve(true);
return this._login().then(() => this.user);
}
hasRole(roleId) {
@ -105,55 +60,6 @@ export default class ExampleUserProvider extends EventEmitter {
return Promise.resolve(this.user.getRoles().includes(roleId));
}
getStatusRoleForCurrentUser() {
return Promise.resolve(this.defaultStatusRole);
}
getAllStatusRoles() {
return Promise.resolve([this.defaultStatusRole]);
}
getStatusForRole(role) {
return Promise.resolve(this.status);
}
async getDefaultStatusForRole(role) {
const allRoles = await this.getPossibleStatuses();
return allRoles?.[0];
}
setStatusForRole(role, status) {
this.status = status;
this.emit('statusChange', {
role,
status
});
return true;
}
getPollQuestion() {
return Promise.resolve({
question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser',
timestamp: Date.now()
});
}
setPollQuestion(pollQuestion) {
this.pollQuestion = {
question: pollQuestion,
timestamp: Date.now()
};
this.emit("pollQuestionChange", this.pollQuestion);
return true;
}
getPossibleStatuses() {
return Promise.resolve(STATUSES);
}
_login() {
const id = uuid();
@ -202,6 +108,3 @@ export default class ExampleUserProvider extends EventEmitter {
);
}
}
/**
* @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider
*/

View File

@ -22,19 +22,8 @@
import ExampleUserProvider from './ExampleUserProvider';
export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = {
autoLoginUser: 'guest',
defaultStatusRole: 'test-role'
}) {
export default function ExampleUserPlugin() {
return function install(openmct) {
const userProvider = new ExampleUserProvider(openmct, {
defaultStatusRole
});
if (autoLoginUser !== undefined) {
userProvider.autoLogin(autoLoginUser);
}
openmct.user.setProvider(userProvider);
openmct.user.setProvider(new ExampleUserProvider(openmct));
};
}

View File

@ -26,7 +26,7 @@ import {
} from '../../src/utils/testing';
import ExampleUserProvider from './ExampleUserProvider';
describe("The Example User Plugin", () => {
xdescribe("The Example User Plugin", () => {
let openmct;
beforeEach(() => {
@ -47,4 +47,9 @@ describe("The Example User Plugin", () => {
});
openmct.install(openmct.plugins.example.ExampleUser());
});
// The rest of the functionality of the ExampleUser Plugin is
// tested in both the UserAPISpec.js and in the UserIndicatorPlugin spec.
// If that changes, those tests can be moved here.
});

View File

@ -1,83 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default function () {
return function install(openmct) {
openmct.install(openmct.plugins.FaultManagement());
openmct.faults.addProvider({
request(domainObject, options) {
const faults = JSON.parse(localStorage.getItem('faults'));
return Promise.resolve(faults.alarms);
},
subscribe(domainObject, callback) {
const faultsData = JSON.parse(localStorage.getItem('faults')).alarms;
function getRandomIndex(start, end) {
return Math.floor(start + (Math.random() * (end - start + 1)));
}
let id = setInterval(() => {
const index = getRandomIndex(0, faultsData.length - 1);
const randomFaultData = faultsData[index];
const randomFault = randomFaultData.fault;
randomFault.currentValueInfo.value = Math.random();
callback({
fault: randomFault,
type: 'alarms'
});
}, 300);
return () => {
clearInterval(id);
};
},
supportsRequest(domainObject) {
const faults = localStorage.getItem('faults');
return faults && domainObject.type === 'faultManagement';
},
supportsSubscribe(domainObject) {
const faults = localStorage.getItem('faults');
return faults && domainObject.type === 'faultManagement';
},
acknowledgeFault(fault, { comment = '' }) {
console.log('acknowledgeFault', fault);
console.log('comment', comment);
return Promise.resolve({
success: true
});
},
shelveFault(fault, shelveData) {
console.log('shelveFault', fault);
console.log('shelveData', shelveData);
return Promise.resolve({
success: true
});
}
});
};
}

View File

@ -1,47 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from '../../src/utils/testing';
describe("The Example Fault Source Plugin", () => {
let openmct;
beforeEach(() => {
openmct = createOpenMct();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('is not installed by default', () => {
expect(openmct.faults.provider).toBeUndefined();
});
it('can be installed', () => {
openmct.install(openmct.plugins.example.ExampleFaultSource());
expect(openmct.faults.provider).not.toBeUndefined();
});
});

View File

@ -29,12 +29,12 @@ define([
}
},
{
key: "wavelengths",
name: "Wavelength",
unit: "nm",
format: 'string[]',
key: "cos",
name: "Cosine",
unit: "deg",
formatString: '%0.2f',
hints: {
range: 4
domain: 3
}
},
// Need to enable "LocalTimeSystem" plugin to make use of this
@ -64,14 +64,6 @@ define([
hints: {
range: 2
}
},
{
key: "intensities",
name: "Intensities",
format: 'number[]',
hints: {
range: 3
}
}
]
},

View File

@ -23,7 +23,7 @@
define([
'uuid'
], function (
{ v4: uuid }
uuid
) {
function WorkerInterface(openmct) {
// eslint-disable-next-line no-undef

View File

@ -77,8 +77,7 @@
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
wavelengths: wavelengths(),
intensities: intensities(),
wavelength: wavelength(start, nextStep),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
}
});
@ -127,8 +126,7 @@
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
wavelengths: wavelengths(),
intensities: intensities(),
wavelength: wavelength(start, nextStep),
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
});
}
@ -156,28 +154,8 @@
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}
function wavelengths() {
let values = [];
while (values.length < 5) {
const randomValue = Math.random() * 100;
if (!values.includes(randomValue)) {
values.push(String(randomValue));
}
}
return values;
}
function intensities() {
let values = [];
while (values.length < 5) {
const randomValue = Math.random() * 10;
if (!values.includes(randomValue)) {
values.push(String(randomValue));
}
}
return values;
function wavelength(start, nextStep) {
return (nextStep - start) / 10;
}
function sendError(error, message) {

View File

@ -59,8 +59,7 @@ export default function () {
object.configuration = {
imageLocation: '',
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
imageSamples: [],
layers: []
imageSamples: []
};
object.telemetry = {
@ -91,21 +90,7 @@ export default function () {
format: 'image',
hints: {
image: 1
},
layers: [
{
source: 'dist/imagery/example-imagery-layer-16x9.png',
name: '16:9'
},
{
source: 'dist/imagery/example-imagery-layer-safe.png',
name: 'Safe'
},
{
source: 'dist/imagery/example-imagery-layer-scale.png',
name: 'Scale'
}
]
}
},
{
name: 'Image Download Name',

View File

@ -75,12 +75,12 @@
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
openmct.install(openmct.plugins.example.ExampleImagery());
openmct.install(openmct.plugins.example.ExampleTags());
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
@ -191,13 +191,11 @@
openmct.install(openmct.plugins.ObjectMigration());
openmct.install(openmct.plugins.ClearData(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],
{ indicator: true }
{indicator: true}
));
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
openmct.start();
</script>
</html>

View File

@ -1,18 +1,19 @@
{
"name": "openmct",
"version": "2.0.5",
"version": "2.0.4-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.2",
"@babel/eslint-parser": "7.16.3",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.2.1",
"@percy/playwright": "1.0.4",
"@percy/cli": "1.0.4",
"@percy/playwright": "1.0.3",
"@playwright/test": "1.21.1",
"@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1",
"@types/karma": "^6.3.2",
"@types/lodash": "^4.14.178",
"@types/mocha": "^9.1.0",
"allure-playwright": "2.0.0-beta.15",
"babel-loader": "8.2.3",
"babel-plugin-istanbul": "6.1.1",
"comma-separated-values": "3.6.4",
@ -25,9 +26,10 @@
"eslint": "8.13.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.9.0",
"eslint-plugin-vue": "9.1.0",
"eslint-plugin-vue": "8.5.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"exports-loader": "0.7.0",
"express": "4.13.1",
"file-saver": "2.0.5",
"git-rev-sync": "3.0.2",
@ -35,7 +37,7 @@
"imports-loader": "0.8.0",
"jasmine-core": "4.1.1",
"jsdoc": "3.5.5",
"karma": "6.3.20",
"karma": "6.3.18",
"karma-chrome-launcher": "3.1.1",
"karma-cli": "2.0.0",
"karma-coverage": "2.2.0",
@ -46,7 +48,7 @@
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.34",
"karma-webpack": "5.0.0",
"lighthouse": "9.6.1",
"lighthouse": "9.5.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.0",
@ -60,26 +62,27 @@
"printj": "1.3.1",
"request": "2.88.2",
"resolve-url-loader": "5.0.0",
"sass": "1.52.2",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"sinon": "14.0.0",
"sinon": "13.0.1",
"style-loader": "^1.0.1",
"uuid": "8.3.2",
"uuid": "3.3.3",
"vue": "2.6.14",
"vue-eslint-parser": "9.0.2",
"vue-eslint-parser": "8.3.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"webpack": "5.68.0",
"webpack-cli": "4.9.2",
"webpack-dev-middleware": "5.3.3",
"webpack-dev-middleware": "5.3.1",
"webpack-hot-middleware": "2.25.1",
"webpack-merge": "5.8.0"
"webpack-merge": "5.8.0",
"zepto": "1.2.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "node app.js",
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
"lint": "eslint example src e2e --ext .js,.vue openmct.js",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "cross-env webpack --config webpack.prod.js",
"build:dev": "webpack --config webpack.dev.js",
@ -89,12 +92,11 @@
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor branding clock exampleImagery",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",

View File

@ -42,7 +42,6 @@ define([
'./plugins/duplicate/plugin',
'./plugins/importFromJSONAction/plugin',
'./plugins/exportAsJSONAction/plugin',
'./ui/components/components',
'vue'
], function (
EventEmitter,
@ -66,7 +65,6 @@ define([
DuplicateActionPlugin,
ImportFromJSONAction,
ExportAsJSONAction,
components,
Vue
) {
/**
@ -238,22 +236,14 @@ define([
this.priority = api.PriorityAPI;
this.router = new ApplicationRouter(this);
this.faults = new api.FaultManagementAPI.default(this);
this.forms = new api.FormsAPI.default(this);
this.branding = BrandingAPI.default;
/**
* MCT's annotation API that enables
* human-created comments and categorization linked to data products
* @type {module:openmct.AnnotationAPI}
* @memberof module:openmct.MCT#
* @name annotation
*/
this.annotation = new api.AnnotationAPI(this);
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.ScatterPlot());
this.install(this.plugins.BarChart());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LicensesPlugin.default());
@ -389,7 +379,6 @@ define([
};
MCT.prototype.plugins = plugins;
MCT.prototype.components = components.default;
return MCT;
});

View File

@ -1,275 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { v4 as uuid } from 'uuid';
import EventEmitter from 'EventEmitter';
/**
* @readonly
* @enum {String} AnnotationType
* @property {String} NOTEBOOK The notebook annotation type
* @property {String} GEOSPATIAL The geospatial annotation type
* @property {String} PIXEL_SPATIAL The pixel-spatial annotation type
* @property {String} TEMPORAL The temporal annotation type
* @property {String} PLOT_SPATIAL The plot-spatial annotation type
*/
const ANNOTATION_TYPES = Object.freeze({
NOTEBOOK: 'NOTEBOOK',
GEOSPATIAL: 'GEOSPATIAL',
PIXEL_SPATIAL: 'PIXEL_SPATIAL',
TEMPORAL: 'TEMPORAL',
PLOT_SPATIAL: 'PLOT_SPATIAL'
});
/**
* @typedef {Object} Tag
* @property {String} key a unique identifier for the tag
* @property {String} backgroundColor eg. "#cc0000"
* @property {String} foregroundColor eg. "#ffffff"
*/
export default class AnnotationAPI extends EventEmitter {
constructor(openmct) {
super();
this.openmct = openmct;
this.availableTags = {};
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
this.openmct.types.addType('annotation', {
name: 'Annotation',
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
creatable: false,
cssClass: 'icon-notebook',
initialize: function (domainObject) {
domainObject.targets = domainObject.targets || {};
domainObject.originalContextPath = domainObject.originalContextPath || '';
domainObject.tags = domainObject.tags || [];
domainObject.contentText = domainObject.contentText || '';
domainObject.annotationType = domainObject.annotationType || 'plotspatial';
}
});
}
/**
* Create the a generic annotation
* @typedef {Object} CreateAnnotationOptions
* @property {String} name a name for the new parameter
* @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create
* @property {Tag[]} tags
* @property {String} contentText
* @property {import('../objects/ObjectAPI').Identifier[]} targets
*/
/**
* @method create
* @param {CreateAnnotationOptions} options
* @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object
* has been created, or be rejected if it cannot be saved
*/
async create({name, domainObject, annotationType, tags, contentText, targets}) {
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
throw new Error(`Unknown annotation type: ${annotationType}`);
}
if (!Object.keys(targets).length) {
throw new Error(`At least one target is required to create an annotation`);
}
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
const namespace = domainObject.identifier.namespace;
const type = 'annotation';
const typeDefinition = this.openmct.types.get(type);
const definition = typeDefinition.definition;
const createdObject = {
name,
type,
identifier: {
key: uuid(),
namespace
},
tags,
annotationType,
contentText,
originalContextPath
};
if (definition.initialize) {
definition.initialize(createdObject);
}
createdObject.targets = targets;
createdObject.originalContextPath = originalContextPath;
const success = await this.openmct.objects.save(createdObject);
if (success) {
this.emit('annotationCreated', createdObject);
return createdObject;
} else {
throw new Error('Failed to create object');
}
}
defineTag(tagKey, tagsDefinition) {
this.availableTags[tagKey] = tagsDefinition;
}
getAvailableTags() {
if (this.availableTags) {
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
return {
id: tagKey,
...this.availableTags[tagKey]
};
});
return rearrangedToArray;
} else {
return [];
}
}
async getAnnotation(query, searchType) {
let foundAnnotation = null;
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat();
if (searchResults) {
foundAnnotation = searchResults[0];
}
return foundAnnotation;
}
async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
if (!existingAnnotation) {
const targets = {};
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
targets[targetKeyString] = targetSpecificDetails;
const contentText = `${annotationType} tag`;
const annotationCreationArguments = {
name: contentText,
domainObject: targetDomainObject,
annotationType,
tags: [],
contentText,
targets
};
existingAnnotation = await this.create(annotationCreationArguments);
}
const tagArray = [tag, ...existingAnnotation.tags];
this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
return existingAnnotation;
}
removeAnnotationTag(existingAnnotation, tagToRemove) {
if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) {
const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove);
this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray);
} else {
throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation);
}
}
removeAnnotationTags(existingAnnotation) {
// just removes tags on the annotation as we can't really delete objects
if (existingAnnotation && existingAnnotation.tags) {
this.openmct.objects.mutate(existingAnnotation, 'tags', []);
}
}
#getMatchingTags(query) {
if (!query) {
return [];
}
const matchingTags = Object.keys(this.availableTags).filter(tagKey => {
if (this.availableTags[tagKey] && this.availableTags[tagKey].label) {
return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase());
}
return false;
});
return matchingTags;
}
#addTagMetaInformationToResults(results, matchingTagKeys) {
const tagsAddedToResults = results.map(result => {
const fullTagModels = result.tags.map(tagKey => {
const tagModel = this.availableTags[tagKey];
tagModel.tagID = tagKey;
return tagModel;
});
return {
fullTagModels,
matchingTagKeys,
...result
};
});
return tagsAddedToResults;
}
async #addTargetModelsToResults(results) {
const modelAddedToResults = await Promise.all(results.map(async result => {
const targetModels = await Promise.all(Object.keys(result.targets).map(async (targetID) => {
const targetModel = await this.openmct.objects.get(targetID);
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
return {
originalPath: originalPathObjects,
...targetModel
};
}));
return {
targetModels,
...result
};
}));
return modelAddedToResults;
}
/**
* @method searchForTags
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
* @param {Object} abortController An optional abort method to stop the query
* @returns {Promise} returns a model of matching tags with their target domain objects attached
*/
async searchForTags(query, abortController) {
const matchingTagKeys = this.#getMatchingTags(query);
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
return appliedTargetsModels;
}
}

View File

@ -1,176 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from '../../utils/testing';
import ExampleTagsPlugin from "../../../example/exampleTags/plugin";
describe("The Annotation API", () => {
let openmct;
let mockObjectProvider;
let mockDomainObject;
let mockAnnotationObject;
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(new ExampleTagsPlugin());
const availableTags = openmct.annotation.getAvailableTags();
mockDomainObject = {
type: 'notebook',
name: 'fooRabbitNotebook',
identifier: {
key: 'some-object',
namespace: 'fooNameSpace'
}
};
mockAnnotationObject = {
type: 'annotation',
name: 'Some Notebook Annotation',
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: [availableTags[0].id, availableTags[1].id],
identifier: {
key: 'anAnnotationKey',
namespace: 'fooNameSpace'
},
targets: {
'fooNameSpace:some-object': {
entryId: 'fooBarEntry'
}
}
};
mockObjectProvider = jasmine.createSpyObj("mock provider", [
"create",
"update",
"get"
]);
// eslint-disable-next-line require-await
mockObjectProvider.get = async (identifier) => {
if (identifier.key === mockDomainObject.identifier.key) {
return mockDomainObject;
} else if (identifier.key === mockAnnotationObject.identifier.key) {
return mockAnnotationObject;
} else {
return null;
}
};
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(async () => {
openmct.objects.providers = {};
await resetApplicationState(openmct);
});
it("is defined", () => {
expect(openmct.annotation).toBeDefined();
});
describe("Creation", () => {
it("can create annotations", async () => {
const annotationCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: "fooContext",
targets: {'fooTarget': {}}
};
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
});
it("fails if annotation is an unknown type", async () => {
try {
await openmct.annotation.create('Garbage Annotation', mockDomainObject, 'garbageAnnotation', ['sometag'], "fooContext", {'fooTarget': {}});
} catch (error) {
expect(error).toBeDefined();
}
});
});
describe("Tagging", () => {
it("can create a tag", async () => {
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
});
it("can delete a tag", async () => {
const originalAnnotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
const annotationObject = await openmct.annotation.addAnnotationTag(originalAnnotationObject, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'anotherTagToRemove');
expect(annotationObject).toBeDefined();
openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove');
expect(annotationObject.tags).toEqual(['aWonderfulTag']);
openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag');
expect(annotationObject.tags).toEqual([]);
});
it("throws an error if deleting non-existent tag", async () => {
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
}).toThrow();
});
it("can remove all tags", async () => {
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.removeAnnotationTags(annotationObject);
}).not.toThrow();
expect(annotationObject.tags).toEqual([]);
});
});
describe("Search", () => {
let sharedWorkerToRestore;
beforeEach(async () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
});
afterEach(() => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
});
it("can search for tags", async () => {
const results = await openmct.annotation.searchForTags('S');
expect(results).toBeDefined();
expect(results.length).toEqual(1);
});
it("can get notebook annotations", async () => {
const targetKeyString = openmct.objects.makeKeyString(mockDomainObject.identifier);
const query = {
targetKeyString,
entryId: 'fooBarEntry'
};
const results = await openmct.annotation.getAnnotation(query, openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
expect(results).toBeDefined();
expect(results.tags.length).toEqual(2);
});
});
});

View File

@ -24,7 +24,6 @@ define([
'./actions/ActionsAPI',
'./composition/CompositionAPI',
'./Editor',
'./faultmanagement/FaultManagementAPI',
'./forms/FormsAPI',
'./indicators/IndicatorAPI',
'./menu/MenuAPI',
@ -35,13 +34,11 @@ define([
'./telemetry/TelemetryAPI',
'./time/TimeAPI',
'./types/TypeRegistry',
'./user/UserAPI',
'./annotation/AnnotationAPI'
'./user/UserAPI'
], function (
ActionsAPI,
CompositionAPI,
EditorAPI,
FaultManagementAPI,
FormsAPI,
IndicatorAPI,
MenuAPI,
@ -52,16 +49,14 @@ define([
TelemetryAPI,
TimeAPI,
TypeRegistry,
UserAPI,
AnnotationAPI
UserAPI
) {
return {
ActionsAPI: ActionsAPI.default,
CompositionAPI: CompositionAPI,
EditorAPI: EditorAPI,
FaultManagementAPI: FaultManagementAPI,
FormsAPI: FormsAPI,
IndicatorAPI: IndicatorAPI.default,
IndicatorAPI: IndicatorAPI,
MenuAPI: MenuAPI.default,
NotificationAPI: NotificationAPI.default,
ObjectAPI: ObjectAPI,
@ -70,7 +65,6 @@ define([
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry,
UserAPI: UserAPI.default,
AnnotationAPI: AnnotationAPI.default
UserAPI: UserAPI.default
};
});

View File

@ -1,106 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class FaultManagementAPI {
constructor(openmct) {
this.openmct = openmct;
}
addProvider(provider) {
this.provider = provider;
}
supportsActions() {
return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined;
}
request(domainObject) {
if (!this.provider?.supportsRequest(domainObject)) {
return Promise.reject();
}
return this.provider.request(domainObject);
}
subscribe(domainObject, callback) {
if (!this.provider?.supportsSubscribe(domainObject)) {
return Promise.reject();
}
return this.provider.subscribe(domainObject, callback);
}
acknowledgeFault(fault, ackData) {
return this.provider.acknowledgeFault(fault, ackData);
}
shelveFault(fault, shelveData) {
return this.provider.shelveFault(fault, shelveData);
}
}
/** @typedef {object} Fault
* @property {string} type
* @property {object} fault
* @property {boolean} fault.acknowledged
* @property {object} fault.currentValueInfo
* @property {number} fault.currentValueInfo.value
* @property {string} fault.currentValueInfo.rangeCondition
* @property {string} fault.currentValueInfo.monitoringResult
* @property {string} fault.id
* @property {string} fault.name
* @property {string} fault.namespace
* @property {number} fault.seqNum
* @property {string} fault.severity
* @property {boolean} fault.shelved
* @property {string} fault.shortDescription
* @property {string} fault.triggerTime
* @property {object} fault.triggerValueInfo
* @property {number} fault.triggerValueInfo.value
* @property {string} fault.triggerValueInfo.rangeCondition
* @property {string} fault.triggerValueInfo.monitoringResult
* @example
* {
* "type": "",
* "fault": {
* "acknowledged": true,
* "currentValueInfo": {
* "value": 0,
* "rangeCondition": "",
* "monitoringResult": ""
* },
* "id": "",
* "name": "",
* "namespace": "",
* "seqNum": 0,
* "severity": "",
* "shelved": true,
* "shortDescription": "",
* "triggerTime": "",
* "triggerValueInfo": {
* "value": 0,
* "rangeCondition": "",
* "monitoringResult": ""
* }
* }
* }
*/

View File

@ -1,144 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* License); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an AS IS BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from '../../utils/testing';
const faultName = 'super duper fault';
const aFault = {
type: '',
fault: {
acknowledged: true,
currentValueInfo: {
value: 0,
rangeCondition: '',
monitoringResult: ''
},
id: '',
name: faultName,
namespace: '',
seqNum: 0,
severity: '',
shelved: true,
shortDescription: '',
triggerTime: '',
triggerValueInfo: {
value: 0,
rangeCondition: '',
monitoringResult: ''
}
}
};
const faultDomainObject = {
name: 'it is not your fault',
type: 'faultManagement',
identifier: {
key: 'nobodies',
namespace: 'fault'
}
};
const aComment = 'THIS is my fault.';
const faultManagementProvider = {
request() {
return Promise.resolve([aFault]);
},
subscribe(domainObject, callback) {
return () => {};
},
supportsRequest(domainObject) {
return domainObject.type === 'faultManagement';
},
supportsSubscribe(domainObject) {
return domainObject.type === 'faultManagement';
},
acknowledgeFault(fault, { comment = '' }) {
return Promise.resolve({
success: true
});
},
shelveFault(fault, shelveData) {
return Promise.resolve({
success: true
});
}
};
describe('The Fault Management API', () => {
let openmct;
beforeEach(() => {
openmct = createOpenMct();
openmct.install(openmct.plugins.FaultManagement());
// openmct.install(openmct.plugins.example.ExampleFaultSource());
openmct.faults.addProvider(faultManagementProvider);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('allows you to request a fault', async () => {
spyOn(faultManagementProvider, 'supportsRequest').and.callThrough();
let faultResponse = await openmct.faults.request(faultDomainObject);
expect(faultManagementProvider.supportsRequest).toHaveBeenCalledWith(faultDomainObject);
expect(faultResponse[0].fault.name).toEqual(faultName);
});
it('allows you to subscribe to a fault', () => {
spyOn(faultManagementProvider, 'subscribe').and.callThrough();
spyOn(faultManagementProvider, 'supportsSubscribe').and.callThrough();
let unsubscribe = openmct.faults.subscribe(faultDomainObject, () => {});
expect(unsubscribe).toEqual(jasmine.any(Function));
expect(faultManagementProvider.supportsSubscribe).toHaveBeenCalledWith(faultDomainObject);
expect(faultManagementProvider.subscribe).toHaveBeenCalledOnceWith(faultDomainObject, jasmine.any(Function));
});
it('will tell you if the fault management provider supports actions', () => {
expect(openmct.faults.supportsActions()).toBeTrue();
});
it('will allow you to acknowledge a fault', async () => {
spyOn(faultManagementProvider, 'acknowledgeFault').and.callThrough();
let ackResponse = await openmct.faults.acknowledgeFault(aFault, aComment);
expect(faultManagementProvider.acknowledgeFault).toHaveBeenCalledWith(aFault, aComment);
expect(ackResponse.success).toBeTrue();
});
it('will allow you to shelve a fault', async () => {
spyOn(faultManagementProvider, 'shelveFault').and.callThrough();
let shelveResponse = await openmct.faults.shelveFault(aFault, aComment);
expect(faultManagementProvider.shelveFault).toHaveBeenCalledWith(aFault, aComment);
expect(shelveResponse.success).toBeTrue();
});
});

View File

@ -44,14 +44,18 @@
>
{{ section.name }}
</h2>
<FormRow
<div
v-for="(row, index) in section.rows"
:key="row.id"
:css-class="row.cssClass"
:first="index < 1"
:row="row"
@onChange="onChange"
/>
class="u-contents"
>
<FormRow
:css-class="section.cssClass"
:first="index < 1"
:row="row"
@onChange="onChange"
/>
</div>
</div>
</form>
@ -77,7 +81,7 @@
<script>
import FormRow from "@/api/forms/components/FormRow.vue";
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
export default {
components: {

View File

@ -23,10 +23,7 @@
<template>
<div
class="form-row c-form__row"
:class="[
{ 'first': first },
cssClass
]"
:class="[{ 'first': first }]"
@onChange="onChange"
>
<div
@ -37,7 +34,7 @@
</div>
<div
class="c-form-row__state-indicator"
:class="reqClass"
:class="rowClass"
>
</div>
<div
@ -79,22 +76,24 @@ export default {
};
},
computed: {
reqClass() {
let reqClass = 'req';
rowClass() {
let cssClass = this.cssClass;
if (!this.row.required) {
return;
}
cssClass = `${cssClass} req`;
if (this.visited && this.valid !== undefined) {
if (this.valid === true) {
reqClass = 'valid';
cssClass = `${cssClass} valid`;
} else {
reqClass = 'invalid';
cssClass = `${cssClass} invalid`;
}
}
return reqClass;
return cssClass;
}
},
mounted() {

View File

@ -19,46 +19,35 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
ref="autoCompleteForm"
class="form-control c-input--autocomplete js-autocomplete"
>
<div
class="c-input--autocomplete__wrapper"
>
<div class="form-control autocomplete">
<span class="autocompleteInputAndArrow">
<input
ref="autoCompleteInput"
v-model="field"
class="c-input--autocomplete__input js-autocomplete__input"
class="autocompleteInput"
type="text"
:placeholder="placeHolderText"
@click="inputClicked()"
@keydown="keyDown($event)"
>
<div
class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow"
<span
class="icon-arrow-down"
@click="arrowClicked()"
></div>
</div>
></span>
</span>
<div
v-if="!hideOptions"
class="c-menu c-input--autocomplete__options"
class="autocompleteOptions"
@blur="hideOptions = true"
>
<ul>
<ul v-if="!hideOptions">
<li
v-for="opt in filteredOptions"
:key="opt.optionId"
:class="[
{'optionPreSelected': optionIndex === opt.optionId},
itemCssClass
]"
:style="itemStyle(opt)"
:class="{'optionPreSelected': optionIndex === opt.optionId}"
@click="fillInputWithString(opt.name)"
@mouseover="optionMouseover(opt.optionId)"
>
{{ opt.name }}
<span class="optionText">{{ opt.name }}</span>
</li>
</ul>
</div>
@ -76,23 +65,7 @@ export default {
props: {
model: {
type: Object,
required: true,
default() {
return {};
}
},
placeHolderText: {
type: String,
default() {
return "";
}
},
itemCssClass: {
type: String,
required: false,
default() {
return "";
}
required: true
}
},
data() {
@ -105,40 +78,31 @@ export default {
},
computed: {
filteredOptions() {
const fullOptions = this.options || [];
const options = this.optionNames || [];
if (this.showFilteredOptions) {
const optionsFiltered = fullOptions
return options
.filter(option => {
if (option.name && this.field) {
return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
}
return false;
return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
}).map((option, index) => {
return {
optionId: index,
name: option.name,
color: option.color
name: option
};
});
return optionsFiltered;
}
const optionsFiltered = fullOptions.map((option, index) => {
return options.map((option, index) => {
return {
optionId: index,
name: option.name,
color: option.color
name: option
};
});
return optionsFiltered;
}
},
watch: {
field(newValue, oldValue) {
if (newValue !== oldValue) {
const data = {
model: this.model,
value: newValue
@ -159,17 +123,17 @@ export default {
}
},
mounted() {
this.autocompleteInputAndArrow = this.$refs.autoCompleteForm;
this.autocompleteInputElement = this.$refs.autoCompleteInput;
if (this.model.options && this.model.options.length && !this.model.options[0].name) {
// If options is only an array of string.
this.options = this.model.options.map((option) => {
return {
name: option
};
this.options = this.model.options;
this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0];
this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
if (this.options[0].name) {
// If "options" include name, value pair
this.optionNames = this.options.map((opt) => {
return opt.name;
});
} else {
this.options = this.model.options;
// If options is only an array of string.
this.optionNames = this.options;
}
},
destroyed() {
@ -258,12 +222,6 @@ export default {
});
}
});
},
itemStyle(option) {
if (option.color) {
return { '--optionIconColor': option.color };
}
}
}
};

View File

@ -39,7 +39,7 @@
import toggleMixin from '../../toggle-check-box-mixin';
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
export default {
components: {

View File

@ -19,27 +19,27 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from "EventEmitter";
import SimpleIndicator from "./SimpleIndicator";
class IndicatorAPI extends EventEmitter {
constructor(openmct) {
super();
define([
'./SimpleIndicator',
'lodash'
], function (
SimpleIndicator,
_
) {
function IndicatorAPI(openmct) {
this.openmct = openmct;
this.indicatorObjects = [];
}
getIndicatorObjectsByPriority() {
IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
return sortedIndicators;
}
};
simpleIndicator() {
IndicatorAPI.prototype.simpleIndicator = function () {
return new SimpleIndicator(this.openmct);
}
};
/**
* Accepts an indicator object, which is a simple object
@ -62,16 +62,14 @@ class IndicatorAPI extends EventEmitter {
* myIndicator.iconClass("icon-info");
*
*/
add(indicator) {
IndicatorAPI.prototype.add = function (indicator) {
if (!indicator.priority) {
indicator.priority = this.openmct.priority.DEFAULT;
}
this.indicatorObjects.push(indicator);
};
this.emit('addIndicator', indicator);
}
return IndicatorAPI;
}
export default IndicatorAPI;
});

View File

@ -20,101 +20,82 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import indicatorTemplate from './res/indicator-template.html';
import { convertTemplateToHTML } from '@/utils/template/templateHelpers';
define(['zepto', './res/indicator-template.html'],
function ($, indicatorTemplate) {
const DEFAULT_ICON_CLASS = 'icon-info';
const DEFAULT_ICON_CLASS = 'icon-info';
function SimpleIndicator(openmct) {
this.openmct = openmct;
this.element = $(indicatorTemplate)[0];
this.priority = openmct.priority.DEFAULT;
class SimpleIndicator extends EventEmitter {
constructor(openmct) {
super();
this.textElement = this.element.querySelector('.js-indicator-text');
this.openmct = openmct;
this.element = convertTemplateToHTML(indicatorTemplate)[0];
this.priority = openmct.priority.DEFAULT;
this.textElement = this.element.querySelector('.js-indicator-text');
//Set defaults
this.text('New Indicator');
this.description('');
this.iconClass(DEFAULT_ICON_CLASS);
this.click = this.click.bind(this);
this.element.addEventListener('click', this.click);
openmct.once('destroy', () => {
this.removeAllListeners();
this.element.removeEventListener('click', this.click);
});
}
text(text) {
if (text !== undefined && text !== this.textValue) {
this.textValue = text;
this.textElement.innerText = text;
if (!text) {
this.element.classList.add('hidden');
} else {
this.element.classList.remove('hidden');
}
//Set defaults
this.text('New Indicator');
this.description('');
this.iconClass(DEFAULT_ICON_CLASS);
this.statusClass('');
}
return this.textValue;
}
SimpleIndicator.prototype.text = function (text) {
if (text !== undefined && text !== this.textValue) {
this.textValue = text;
this.textElement.innerText = text;
description(description) {
if (description !== undefined && description !== this.descriptionValue) {
this.descriptionValue = description;
this.element.title = description;
}
return this.descriptionValue;
}
iconClass(iconClass) {
if (iconClass !== undefined && iconClass !== this.iconClassValue) {
// element.classList is precious and throws errors if you try and add
// or remove empty strings
if (this.iconClassValue) {
this.element.classList.remove(this.iconClassValue);
if (!text) {
this.element.classList.add('hidden');
} else {
this.element.classList.remove('hidden');
}
}
if (iconClass) {
this.element.classList.add(iconClass);
return this.textValue;
};
SimpleIndicator.prototype.description = function (description) {
if (description !== undefined && description !== this.descriptionValue) {
this.descriptionValue = description;
this.element.title = description;
}
this.iconClassValue = iconClass;
}
return this.descriptionValue;
};
return this.iconClassValue;
}
SimpleIndicator.prototype.iconClass = function (iconClass) {
if (iconClass !== undefined && iconClass !== this.iconClassValue) {
// element.classList is precious and throws errors if you try and add
// or remove empty strings
if (this.iconClassValue) {
this.element.classList.remove(this.iconClassValue);
}
statusClass(statusClass) {
if (arguments.length === 1 && statusClass !== this.statusClassValue) {
if (this.statusClassValue) {
this.element.classList.remove(this.statusClassValue);
if (iconClass) {
this.element.classList.add(iconClass);
}
this.iconClassValue = iconClass;
}
if (statusClass !== undefined) {
this.element.classList.add(statusClass);
return this.iconClassValue;
};
SimpleIndicator.prototype.statusClass = function (statusClass) {
if (statusClass !== undefined && statusClass !== this.statusClassValue) {
if (this.statusClassValue) {
this.element.classList.remove(this.statusClassValue);
}
if (statusClass) {
this.element.classList.add(statusClass);
}
this.statusClassValue = statusClass;
}
this.statusClassValue = statusClass;
}
return this.statusClassValue;
};
return this.statusClassValue;
return SimpleIndicator;
}
click(event) {
this.emit('click', event);
}
getElement() {
return this.element;
}
}
export default SimpleIndicator;
);

View File

@ -12,7 +12,6 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
@ -36,9 +35,8 @@
<li
v-for="action in options.actions"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}

View File

@ -15,7 +15,6 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@ -46,7 +45,6 @@
:key="action.name"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
class InMemorySearchProvider {
/**
@ -39,10 +39,11 @@ class InMemorySearchProvider {
* If max results is not specified in query, use this as default.
*/
this.DEFAULT_MAX_RESULTS = 100;
this.openmct = openmct;
this.indexedIds = {};
this.indexedCompositions = {};
this.indexedTags = {};
this.idsToIndex = [];
this.pendingIndex = {};
this.pendingRequests = 0;
@ -51,18 +52,11 @@ class InMemorySearchProvider {
/**
* If we don't have SharedWorkers available (e.g., iOS)
*/
this.localIndexedDomainObjects = {};
this.localIndexedAnnotationsByDomainObject = {};
this.localIndexedAnnotationsByTag = {};
this.localIndexedItems = {};
this.pendingQueries = {};
this.onWorkerMessage = this.onWorkerMessage.bind(this);
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
this.localSearchForObjects = this.localSearchForObjects.bind(this);
this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);
this.localSearchForTags = this.localSearchForTags.bind(this);
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
this.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this);
@ -82,39 +76,13 @@ class InMemorySearchProvider {
startIndexing() {
const rootObject = this.openmct.objects.rootProvider.rootObject;
this.searchTypes = this.openmct.objects.SEARCH_TYPES;
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
this.scheduleForIndexing(rootObject.identifier);
this.indexAnnotations();
if (typeof SharedWorker !== 'undefined') {
this.worker = this.startSharedWorker();
} else {
// we must be on iOS
}
this.openmct.annotation.on('annotationCreated', this.onAnnotationCreation);
}
indexAnnotations() {
const theInMemorySearchProvider = this;
Object.values(this.openmct.objects.providers).forEach(objectProvider => {
if (objectProvider.getAllObjects) {
const allObjects = objectProvider.getAllObjects();
if (allObjects) {
Object.values(allObjects).forEach(domainObject => {
if (domainObject.type === 'annotation') {
theInMemorySearchProvider.scheduleForIndexing(domainObject.identifier);
}
});
}
}
});
}
/**
@ -130,60 +98,51 @@ class InMemorySearchProvider {
return intermediateResponse;
}
search(query, searchType) {
/**
* Query the search provider for results.
*
* @param {String} input the string to search by.
* @param {Number} maxResults max number of results to return.
* @returns {Promise} a promise for a modelResults object.
*/
query(input, maxResults) {
if (!maxResults) {
maxResults = this.DEFAULT_MAX_RESULTS;
}
const queryId = uuid();
const pendingQuery = this.getIntermediateResponse();
this.pendingQueries[queryId] = pendingQuery;
const searchOptions = {
queryId,
searchType,
query,
maxResults: this.DEFAULT_MAX_RESULTS
};
if (this.worker) {
this.#dispatchSearchToWorker(searchOptions);
this.dispatchSearch(queryId, input, maxResults);
} else {
this.#localQueryFallBack(searchOptions);
this.localSearch(queryId, input, maxResults);
}
return pendingQuery.promise;
}
#localQueryFallBack({queryId, searchType, query, maxResults}) {
if (searchType === this.searchTypes.OBJECTS) {
return this.localSearchForObjects(queryId, query, maxResults);
} else if (searchType === this.searchTypes.ANNOTATIONS) {
return this.localSearchForAnnotations(queryId, query, maxResults);
} else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
return this.localSearchForNotebookAnnotations(queryId, query, maxResults);
} else if (searchType === this.searchTypes.TAGS) {
return this.localSearchForTags(queryId, query, maxResults);
} else {
throw new Error(`🤷‍♂️ Unknown search type passed: ${searchType}`);
}
}
supportsSearchType(searchType) {
return this.supportedSearchTypes.includes(searchType);
}
/**
* Handle messages from the worker.
* Handle messages from the worker. Only really knows how to handle search
* results, which are parsed, transformed into a modelResult object, which
* is used to resolve the corresponding promise.
* @private
*/
async onWorkerMessage(event) {
if (event.data.request !== 'search') {
return;
}
const pendingQuery = this.pendingQueries[event.data.queryId];
const modelResults = {
total: event.data.total
};
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
if (hit && hit.keyString) {
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
const domainObject = await this.openmct.objects.get(identifier);
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
const domainObject = await this.openmct.objects.get(identifier);
return domainObject;
}
return domainObject;
}));
pendingQuery.resolve(modelResults);
@ -257,11 +216,6 @@ class InMemorySearchProvider {
}
}
onAnnotationCreation(annotationObject) {
const provider = this;
provider.index(annotationObject);
}
onNameMutation(domainObject, name) {
const provider = this;
@ -269,14 +223,6 @@ class InMemorySearchProvider {
provider.index(domainObject);
}
onTagMutation(domainObject, newTags) {
domainObject.oldTags = domainObject.tags;
domainObject.tags = newTags;
const provider = this;
provider.index(domainObject);
}
onCompositionMutation(domainObject, composition) {
const provider = this;
const indexedComposition = domainObject.composition;
@ -313,13 +259,6 @@ class InMemorySearchProvider {
'composition',
this.onCompositionMutation.bind(this, domainObject)
);
if (domainObject.type === 'annotation') {
this.indexedTags[keyString] = this.openmct.objects.observe(
domainObject,
'tags',
this.onTagMutation.bind(this, domainObject)
);
}
}
if ((keyString !== 'ROOT')) {
@ -378,87 +317,26 @@ class InMemorySearchProvider {
* @private
* @returns {String} a unique query Id for the query.
*/
#dispatchSearchToWorker({queryId, searchType, query, maxResults}) {
dispatchSearch(queryId, searchInput, maxResults) {
const message = {
request: searchType.toString(),
input: query,
request: 'search',
input: searchInput,
maxResults,
queryId
};
this.worker.port.postMessage(message);
}
localIndexTags(keyString, objectToIndex, model) {
// add new tags
model.tags.forEach(tagID => {
if (!this.localIndexedAnnotationsByTag[tagID]) {
this.localIndexedAnnotationsByTag[tagID] = [];
}
const existsInIndex = this.localIndexedAnnotationsByTag[tagID].some(indexedObject => {
return indexedObject.keyString === objectToIndex.keyString;
});
if (!existsInIndex) {
this.localIndexedAnnotationsByTag[tagID].push(objectToIndex);
}
});
// remove old tags
if (model.oldTags) {
model.oldTags.forEach(tagIDToRemove => {
const existsInNewModel = model.tags.includes(tagIDToRemove);
if (!existsInNewModel && this.localIndexedAnnotationsByTag[tagIDToRemove]) {
this.localIndexedAnnotationsByTag[tagIDToRemove] = this.localIndexedAnnotationsByTag[tagIDToRemove].
filter(annotationToRemove => {
const shouldKeep = annotationToRemove.keyString !== keyString;
return shouldKeep;
});
}
});
}
}
localIndexAnnotation(objectToIndex, model) {
Object.keys(model.targets).forEach(targetID => {
if (!this.localIndexedAnnotationsByDomainObject[targetID]) {
this.localIndexedAnnotationsByDomainObject[targetID] = [];
}
objectToIndex.targets = model.targets;
objectToIndex.tags = model.tags;
const existsInIndex = this.localIndexedAnnotationsByDomainObject[targetID].some(indexedObject => {
return indexedObject.keyString === objectToIndex.keyString;
});
if (!existsInIndex) {
this.localIndexedAnnotationsByDomainObject[targetID].push(objectToIndex);
}
});
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localIndexItem(keyString, model) {
const objectToIndex = {
this.localIndexedItems[keyString] = {
type: model.type,
name: model.name,
keyString
};
if (model && (model.type === 'annotation')) {
if (model.targets && model.targets) {
this.localIndexAnnotation(objectToIndex, model);
}
if (model.tags) {
this.localIndexTags(keyString, objectToIndex, model);
}
} else {
this.localIndexedDomainObjects[keyString] = objectToIndex;
}
}
/**
@ -468,122 +346,21 @@ class InMemorySearchProvider {
* Gets search results from the indexedItems based on provided search
* input. Returns matching results from indexedItems
*/
localSearchForObjects(queryId, searchInput, maxResults) {
localSearch(queryId, searchInput, maxResults) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
let results = [];
let results;
const input = searchInput.trim().toLowerCase();
const message = {
request: 'searchForObjects',
results: [],
request: 'search',
results: {},
total: 0,
queryId
};
results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => {
results = Object.values(this.localIndexedItems).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
}) || [];
message.total = results.length;
message.results = results
.slice(0, maxResults);
const eventToReturn = {
data: message
};
this.onWorkerMessage(eventToReturn);
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localSearchForAnnotations(queryId, searchInput, maxResults) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
let results = [];
const message = {
request: 'searchForAnnotations',
results: [],
total: 0,
queryId
};
results = this.localIndexedAnnotationsByDomainObject[searchInput] || [];
message.total = results.length;
message.results = results
.slice(0, maxResults);
const eventToReturn = {
data: message
};
this.onWorkerMessage(eventToReturn);
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localSearchForTags(queryId, matchingTagKeys, maxResults) {
let results = [];
const message = {
request: 'searchForTags',
results: [],
total: 0,
queryId
};
if (matchingTagKeys) {
matchingTagKeys.forEach(matchingTag => {
const matchingAnnotations = this.localIndexedAnnotationsByTag[matchingTag];
if (matchingAnnotations) {
matchingAnnotations.forEach(matchingAnnotation => {
const existsInResults = results.some(indexedObject => {
return matchingAnnotation.keyString === indexedObject.keyString;
});
if (!existsInResults) {
results.push(matchingAnnotation);
}
});
}
});
}
message.total = results.length;
message.results = results
.slice(0, maxResults);
const eventToReturn = {
data: message
};
this.onWorkerMessage(eventToReturn);
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localSearchForNotebookAnnotations(queryId, {entryId, targetKeyString}, maxResults) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
let results = [];
const message = {
request: 'searchForNotebookAnnotations',
results: [],
total: 0,
queryId
};
const matchingAnnotations = this.localIndexedAnnotationsByDomainObject[targetKeyString];
if (matchingAnnotations) {
results = matchingAnnotations.filter(matchingAnnotation => {
if (!matchingAnnotation.targets) {
return false;
}
const target = matchingAnnotation.targets[targetKeyString];
return (target && target.entryId && (target.entryId === entryId));
});
}
});
message.total = results.length;
message.results = results

View File

@ -26,27 +26,16 @@
(function () {
// An object composed of domain object IDs and models
// {id: domainObject's ID, name: domainObject's name}
const indexedDomainObjects = {};
const indexedAnnotationsByDomainObject = {};
const indexedAnnotationsByTag = {};
const indexedItems = {};
self.onconnect = function (e) {
const port = e.ports[0];
port.onmessage = function (event) {
const requestType = event.data.request;
if (requestType === 'index') {
if (event.data.request === 'index') {
indexItem(event.data.keyString, event.data.model);
} else if (requestType === 'OBJECTS') {
port.postMessage(searchForObjects(event.data));
} else if (requestType === 'ANNOTATIONS') {
port.postMessage(searchForAnnotations(event.data));
} else if (requestType === 'TAGS') {
port.postMessage(searchForTags(event.data));
} else if (requestType === 'NOTEBOOK_ANNOTATIONS') {
port.postMessage(searchForNotebookAnnotations(event.data));
} else {
throw new Error(`Unknown request ${event.data.request}`);
} else if (event.data.request === 'search') {
port.postMessage(search(event.data));
}
};
@ -59,73 +48,12 @@
console.error('Error on feed', error);
};
function indexAnnotation(objectToIndex, model) {
Object.keys(model.targets).forEach(targetID => {
if (!indexedAnnotationsByDomainObject[targetID]) {
indexedAnnotationsByDomainObject[targetID] = [];
}
objectToIndex.targets = model.targets;
objectToIndex.tags = model.tags;
const existsInIndex = indexedAnnotationsByDomainObject[targetID].some(indexedObject => {
return indexedObject.keyString === objectToIndex.keyString;
});
if (!existsInIndex) {
indexedAnnotationsByDomainObject[targetID].push(objectToIndex);
}
});
}
function indexTags(keyString, objectToIndex, model) {
// add new tags
model.tags.forEach(tagID => {
if (!indexedAnnotationsByTag[tagID]) {
indexedAnnotationsByTag[tagID] = [];
}
const existsInIndex = indexedAnnotationsByTag[tagID].some(indexedObject => {
return indexedObject.keyString === objectToIndex.keyString;
});
if (!existsInIndex) {
indexedAnnotationsByTag[tagID].push(objectToIndex);
}
});
// remove old tags
if (model.oldTags) {
model.oldTags.forEach(tagIDToRemove => {
const existsInNewModel = model.tags.includes(tagIDToRemove);
if (!existsInNewModel && indexedAnnotationsByTag[tagIDToRemove]) {
indexedAnnotationsByTag[tagIDToRemove] = indexedAnnotationsByTag[tagIDToRemove].
filter(annotationToRemove => {
const shouldKeep = annotationToRemove.keyString !== keyString;
return shouldKeep;
});
}
});
}
}
function indexItem(keyString, model) {
const objectToIndex = {
indexedItems[keyString] = {
type: model.type,
name: model.name,
keyString
};
if (model && (model.type === 'annotation')) {
if (model.targets && model.targets) {
indexAnnotation(objectToIndex, model);
}
if (model.tags) {
indexTags(keyString, objectToIndex, model);
}
} else {
indexedDomainObjects[keyString] = objectToIndex;
}
}
/**
@ -137,98 +65,21 @@
* * maxResults: The maximum number of search results desired
* * queryId: an id identifying this query, will be returned.
*/
function searchForObjects(data) {
let results = [];
function search(data) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
let results;
const input = data.input.trim().toLowerCase();
const message = {
request: 'searchForObjects',
results: [],
total: 0,
queryId: data.queryId
};
results = Object.values(indexedDomainObjects).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
}) || [];
message.total = results.length;
message.results = results
.slice(0, data.maxResults);
return message;
}
function searchForAnnotations(data) {
let results = [];
const message = {
request: 'searchForAnnotations',
results: [],
total: 0,
queryId: data.queryId
};
results = indexedAnnotationsByDomainObject[data.input] || [];
message.total = results.length;
message.results = results
.slice(0, data.maxResults);
return message;
}
function searchForTags(data) {
let results = [];
const message = {
request: 'searchForTags',
results: [],
total: 0,
queryId: data.queryId
};
if (data.input) {
data.input.forEach(matchingTag => {
const matchingAnnotations = indexedAnnotationsByTag[matchingTag];
if (matchingAnnotations) {
matchingAnnotations.forEach(matchingAnnotation => {
const existsInResults = results.some(indexedObject => {
return matchingAnnotation.keyString === indexedObject.keyString;
});
if (!existsInResults) {
results.push(matchingAnnotation);
}
});
}
});
}
message.total = results.length;
message.results = results
.slice(0, data.maxResults);
return message;
}
function searchForNotebookAnnotations(data) {
let results = [];
const message = {
request: 'searchForNotebookAnnotations',
request: 'search',
results: {},
total: 0,
queryId: data.queryId
};
const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString];
if (matchingAnnotations) {
results = matchingAnnotations.filter(matchingAnnotation => {
if (!matchingAnnotation.targets) {
return false;
}
const target = matchingAnnotation.targets[data.input.targetKeyString];
return (target && target.entryId && (target.entryId === data.input.entryId));
});
}
results = Object.values(indexedItems).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
});
message.total = results.length;
message.results = results

File diff suppressed because it is too large Load Diff

View File

@ -17,16 +17,13 @@ describe("The Object API Search Function", () => {
openmct = createOpenMct();
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"search", "supportsSearchType"
"search"
]);
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
"search", "supportsSearchType"
"search"
]);
openmct.objects.addProvider('objects', mockObjectProvider);
openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
mockObjectProvider.supportsSearchType.and.callFake(() => {
return true;
});
mockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const mockProviderSearch = {
@ -41,9 +38,6 @@ describe("The Object API Search Function", () => {
}, MOCK_PROVIDER_SEARCH_DELAY);
});
});
anotherMockObjectProvider.supportsSearchType.and.callFake(() => {
return true;
});
anotherMockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const anotherMockProviderSearch = {
@ -116,8 +110,8 @@ describe("The Object API Search Function", () => {
namespace: ''
});
openmct.objects.addProvider('foo', defaultObjectProvider);
spyOn(openmct.objects.inMemorySearchProvider, "search").and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, "localSearchForObjects").and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
openmct.on('start', async () => {
mockIdentifier1 = {
@ -161,7 +155,7 @@ describe("The Object API Search Function", () => {
it("can provide indexing without a provider", () => {
openmct.objects.search('foo');
expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled();
expect(openmct.objects.inMemorySearchProvider.query).toHaveBeenCalled();
});
it("can do partial search", async () => {
@ -183,22 +177,16 @@ describe("The Object API Search Function", () => {
});
describe("Without Shared Workers", () => {
let sharedWorkerToRestore;
beforeEach(async () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
// reindex locally
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
});
afterEach(() => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
});
it("calls local search", () => {
openmct.objects.search('foo');
expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled();
expect(openmct.objects.inMemorySearchProvider.localSearch).toHaveBeenCalled();
});
it("can do partial search", async () => {

View File

@ -7,7 +7,6 @@
<div class="c-overlay__outer">
<button
v-if="dismissable"
aria-label="Close"
class="c-click-icon c-overlay__close-button icon-x"
@click="destroy"
></button>

View File

@ -121,18 +121,6 @@ define([
return _.sortBy(matchingMetadata, ...iteratees);
};
/**
* check out of a given metadata has array values
*/
TelemetryMetadataManager.prototype.isArrayValue = function (metadata) {
const regex = /\[\]$/g;
if (!metadata.format && !metadata.formatString) {
return false;
}
return (metadata.format || metadata.formatString).match(regex) !== null;
};
TelemetryMetadataManager.prototype.getFilterableValues = function () {
return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0);
};
@ -150,7 +138,7 @@ define([
valueMetadata = this.values()[0];
}
return valueMetadata;
return valueMetadata.key;
};
return TelemetryMetadataManager;

View File

@ -43,23 +43,9 @@ define([
};
this.valueMetadata = valueMetadata;
this.formatter = formatMap.get(valueMetadata.format) || numberFormatter;
function getNonArrayValue(value) {
//metadata format could have array formats ex. string[]/number[]
const arrayRegex = /\[\]$/g;
if (value && value.match(arrayRegex)) {
return value.replace(arrayRegex, '');
}
return value;
}
let valueMetadataFormat = getNonArrayValue(valueMetadata.format);
//Is there an existing formatter for the format specified? If not, default to number format
this.formatter = formatMap.get(valueMetadataFormat) || numberFormatter;
if (valueMetadataFormat === 'enum') {
if (valueMetadata.format === 'enum') {
this.formatter = {};
this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) {
vm.byValue[e.value] = e.string;
@ -91,13 +77,13 @@ define([
// Check for formatString support once instead of per format call.
if (valueMetadata.formatString) {
const baseFormat = this.formatter.format;
const formatString = getNonArrayValue(valueMetadata.formatString);
const formatString = valueMetadata.formatString;
this.formatter.format = function (value) {
return printj.sprintf(formatString, baseFormat.call(this, value));
};
}
if (valueMetadataFormat === 'string') {
if (valueMetadata.format === 'string') {
this.formatter.parse = function (value) {
if (value === undefined) {
return '';
@ -122,14 +108,7 @@ define([
TelemetryValueFormatter.prototype.parse = function (datum) {
if (_.isObject(datum)) {
const objectDatum = datum[this.valueMetadata.source];
if (Array.isArray(objectDatum)) {
return objectDatum.map((item) => {
return this.formatter.parse(item);
});
} else {
return this.formatter.parse(objectDatum);
}
return this.formatter.parse(datum[this.valueMetadata.source]);
}
return this.formatter.parse(datum);
@ -137,14 +116,7 @@ define([
TelemetryValueFormatter.prototype.format = function (datum) {
if (_.isObject(datum)) {
const objectDatum = datum[this.valueMetadata.source];
if (Array.isArray(objectDatum)) {
return objectDatum.map((item) => {
return this.formatter.format(item);
});
} else {
return this.formatter.format(objectDatum);
}
return this.formatter.format(datum[this.valueMetadata.source]);
}
return this.formatter.format(datum);

View File

@ -1,295 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from "EventEmitter";
export default class StatusAPI extends EventEmitter {
#userAPI;
#openmct;
constructor(userAPI, openmct) {
super();
this.#userAPI = userAPI;
this.#openmct = openmct;
this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
this.#openmct.once('destroy', () => {
const provider = this.#userAPI.getProvider();
if (typeof provider?.off === 'function') {
provider.off('statusChange', this.onProviderStatusChange);
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
}
});
this.#userAPI.on('providerAdded', this.listenToStatusEvents);
}
/**
* Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status.
* @returns {Promise<PollQuestion>}
*/
getPollQuestion() {
const provider = this.#userAPI.getProvider();
if (provider.getPollQuestion) {
return provider.getPollQuestion();
} else {
this.#userAPI.error("User provider does not support polling questions");
}
}
/**
* Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status.
* @param {String} questionText - The text of the question
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setPollQuestion(questionText) {
const canSetPollQuestion = await this.canSetPollQuestion();
if (canSetPollQuestion) {
const provider = this.#userAPI.getProvider();
const result = await provider.setPollQuestion(questionText);
try {
await this.resetAllStatuses();
} catch (error) {
console.warn("Poll question set but unable to clear operator statuses.");
console.error(error);
}
return result;
} else {
this.#userAPI.error("User provider does not support setting polling question");
}
}
/**
* Can the currently logged in user set the operator status poll question.
* @returns {Promise<Boolean>}
*/
canSetPollQuestion() {
const provider = this.#userAPI.getProvider();
if (provider.canSetPollQuestion) {
return provider.canSetPollQuestion();
} else {
return Promise.resolve(false);
}
}
/**
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
*/
async getPossibleStatuses() {
const provider = this.#userAPI.getProvider();
if (provider.getPossibleStatuses) {
const possibleStatuses = await provider.getPossibleStatuses() || [];
return possibleStatuses.map(status => status);
} else {
this.#userAPI.error("User provider cannot provide statuses");
}
}
/**
* @param {import("./UserAPI").Role} role The role to fetch the current status for.
* @returns {Promise<Status>} the current status of the provided role
*/
async getStatusForRole(role) {
const provider = this.#userAPI.getProvider();
if (provider.getStatusForRole) {
const status = await provider.getStatusForRole(role);
return status;
} else {
this.#userAPI.error("User provider does not support role status");
}
}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role
* @see StatusUserProvider
*/
canProvideStatusForRole(role) {
const provider = this.#userAPI.getProvider();
if (provider.canProvideStatusForRole) {
return provider.canProvideStatusForRole(role);
} else {
return false;
}
}
/**
* @param {import("./UserAPI").Role} role The role to set the status for.
* @param {Status} status The status to set for the provided role
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForRole(role, status) {
const provider = this.#userAPI.getProvider();
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, status);
} else {
this.#userAPI.error("User provider does not support setting role status");
}
}
/**
* Resets the status of the provided role back to its default status.
* @param {import("./UserAPI").Role} role The role to set the status for.
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await this.getDefaultStatus();
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, defaultStatus);
} else {
this.#userAPI.error("User provider does not support resetting role status");
}
}
/**
* Resets the status of all operators to their default status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetAllStatuses() {
const allStatusRoles = await this.getAllStatusRoles();
return Promise.all(allStatusRoles.map(role => this.resetStatusForRole(role)));
}
/**
* The default status. This is the status that will be used before the user has selected any status.
* @param {import("./UserAPI").Role} role
* @returns {Promise<Status>} the default operator status if no other has been set.
*/
async getDefaultStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await provider.getDefaultStatusForRole(role);
return defaultStatus;
}
/**
* All possible status roles. A status role is a user role that can provide status. In some systems
* this may be all user roles, but there may be cases where some users are not are not polled
* for status if they do not have a real-time operational role.
*
* @returns {Promise<Array<import("./UserAPI").Role>>} the default operator status if no other has been set.
*/
getAllStatusRoles() {
const provider = this.#userAPI.getProvider();
if (provider.getAllStatusRoles) {
return provider.getAllStatusRoles();
} else {
this.#userAPI.error("User provider cannot provide all status roles");
}
}
/**
* The status role of the current user. A user may have multiple roles, but will only have one role
* that provides status at any time.
* @returns {Promise<import("./UserAPI").Role>} the role for which the current user can provide status.
*/
getStatusRoleForCurrentUser() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusRoleForCurrentUser) {
return provider.getStatusRoleForCurrentUser();
} else {
this.#userAPI.error("User provider cannot provide role status for this user");
}
}
/**
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
* @see StatusUserProvider
*/
async canProvideStatusForCurrentUser() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusRoleForCurrentUser) {
const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser();
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
return canProvideStatus;
} else {
return false;
}
}
/**
* Private internal function that cannot be made #private because it needs to be registered as a callback to the user provider
* @private
*/
listenToStatusEvents(provider) {
if (typeof provider.on === 'function') {
provider.on('statusChange', this.onProviderStatusChange);
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
}
}
/**
* @private
*/
onProviderStatusChange(newStatus) {
this.emit('statusChange', newStatus);
}
/**
* @private
*/
onProviderPollQuestionChange(pollQuestion) {
this.emit('pollQuestionChange', pollQuestion);
}
}
/**
* @typedef {import('./UserProvider')} UserProvider
*/
/**
* @typedef {import('./StatusUserProvider')} StatusUserProvider
*/
/**
* The PollQuestion type
* @typedef {Object} PollQuestion
* @property {String} question - The question to be presented to users
* @property {Number} timestamp - The time that the poll question was set.
*/
/**
* The Status type
* @typedef {Object} Status
* @property {String} key - A unique identifier for this status
* @property {Number} label - A human readable label for this status
*/

View File

@ -1,81 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import UserProvider from "./UserProvider";
export default class StatusUserProvider extends UserProvider {
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
* @param {Function} callback a function to invoke when this event occurs
*/
on(event, callback) {}
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
* @param {Function} callback the callback function used to register the listener
*/
off(event, callback) {}
/**
* @returns {import("./StatusAPI").PollQuestion} the current status poll question
*/
async getPollQuestion() {}
/**
* @param {import("./StatusAPI").PollQuestion} pollQuestion a new poll question to set
* @returns {Promise<Boolean>} true if operation was successful, otherwise false
*/
async setPollQuestion(pollQuestion) {}
/**
* @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false
*/
async canSetPollQuestion() {}
/**
* @returns {Promise<Array<import("./StatusAPI").Status>>} a list of the possible statuses that an operator can be in
*/
async getPossibleStatuses() {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getDefaultStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @param {*} status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setStatusForRole(role, status) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean} true if the user provider can provide status for the given role
*/
async canProvideStatusForRole(role) {}
/**
* @returns {Promise<Array<import("./UserAPI").Role>>} a list of all available status roles, if user permissions allow it.
*/
async getAllStatusRoles() {}
/**
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
*/
async getStatusRoleForCurrentUser() {}
}

View File

@ -25,22 +25,16 @@ import {
MULTIPLE_PROVIDER_ERROR,
NO_PROVIDER_ERROR
} from './constants';
import StatusAPI from './StatusAPI';
import User from './User';
class UserAPI extends EventEmitter {
/**
* @param {OpenMCT} openmct
* @param {UserAPIConfiguration} config
*/
constructor(openmct, config) {
constructor(openmct) {
super();
this._openmct = openmct;
this._provider = undefined;
this.User = User;
this.status = new StatusAPI(this, openmct, config);
}
/**
@ -53,15 +47,12 @@ class UserAPI extends EventEmitter {
*/
setProvider(provider) {
if (this.hasProvider()) {
this.error(MULTIPLE_PROVIDER_ERROR);
this._error(MULTIPLE_PROVIDER_ERROR);
}
this._provider = provider;
this.emit('providerAdded', this._provider);
}
getProvider() {
return this._provider;
this.emit('providerAdded', this._provider);
}
/**
@ -83,7 +74,7 @@ class UserAPI extends EventEmitter {
* @throws Will throw an error if no user provider is set
*/
getCurrentUser() {
this.noProviderCheck();
this._noProviderCheck();
return this._provider.getCurrentUser();
}
@ -114,7 +105,7 @@ class UserAPI extends EventEmitter {
* @throws Will throw an error if no user provider is set
*/
hasRole(roleId) {
this.noProviderCheck();
this._noProviderCheck();
return this._provider.hasRole(roleId);
}
@ -125,9 +116,9 @@ class UserAPI extends EventEmitter {
* @private
* @throws Will throw an error if no user provider is set
*/
noProviderCheck() {
_noProviderCheck() {
if (!this.hasProvider()) {
this.error(NO_PROVIDER_ERROR);
this._error(NO_PROVIDER_ERROR);
}
}
@ -138,26 +129,9 @@ class UserAPI extends EventEmitter {
* @param {string} error description of error
* @throws Will throw error passed in
*/
error(error) {
_error(error) {
throw new Error(error);
}
}
export default UserAPI;
/**
* @typedef {String} Role
*/
/**
* @typedef {Object} OpenMCT
*/
/**
* @typedef {{statusStyles: Object.<string, StatusStyleDefinition>}} UserAPIConfiguration
*/
/**
* @typedef {Object} StatusStyleDefinition
* @property {String} iconClass The icon class to apply to the status indicator when this status is active "icon-circle-slash",
* @property {String} iconClassPoll The icon class to apply to the poll question indicator when this style is active eg. "icon-status-poll-question-mark"
* @property {String} statusClass The class to apply to the indicator when this status is active eg. "s-status-error"
* @property {String} statusBgColor The background color to apply in the status summary section of the poll question popup for this status eg."#9900cc"
* @property {String} statusFgColor The foreground color to apply in the status summary section of the poll question popup for this status eg. "#fff"
*/

View File

@ -1,36 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class UserProvider {
/**
* @returns {Promise<User>} A promise that resolves with the currently logged in user
*/
getCurrentUser() {}
/**
* @returns {Boolean} true if a user is currently logged in, otherwise false
*/
isLoggedIn() {}
/**
* @param {String} role
* @returns {Promise<Boolean>} true if the current user has the given role
*/
hasRole(role) {}
}

View File

@ -1,103 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from '../../utils/testing';
describe("The User Status API", () => {
let openmct;
let userProvider;
let mockUser;
beforeEach(() => {
userProvider = jasmine.createSpyObj("userProvider", [
"setPollQuestion",
"getPollQuestion",
"getCurrentUser",
"getPossibleStatuses",
"getAllStatusRoles",
"canSetPollQuestion",
"isLoggedIn",
"on"
]);
openmct = createOpenMct();
mockUser = new openmct.user.User("test-user", "A test user");
userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser));
userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([]));
userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([]));
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));
userProvider.isLoggedIn.and.returnValue(true);
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("the poll question", () => {
it('can be set via a user status provider if supported', () => {
openmct.user.setProvider(userProvider);
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true));
return openmct.user.status.setPollQuestion('This is a poll question').then(() => {
expect(userProvider.setPollQuestion).toHaveBeenCalledWith('This is a poll question');
});
});
// fit('emits an event when the poll question changes', () => {
// const pollQuestionChangeCallback = jasmine.createSpy('pollQuestionChangeCallback');
// let pollQuestionListener;
// userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true));
// userProvider.on.and.callFake((eventName, listener) => {
// if (eventName === 'pollQuestionChange') {
// pollQuestionListener = listener;
// }
// });
// openmct.user.on('pollQuestionChange', pollQuestionChangeCallback);
// openmct.user.setProvider(userProvider);
// return openmct.user.status.setPollQuestion('This is a poll question').then(() => {
// expect(pollQuestionListener).toBeDefined();
// pollQuestionListener();
// expect(pollQuestionChangeCallback).toHaveBeenCalled();
// const pollQuestion = pollQuestionChangeCallback.calls.mostRecent().args[0];
// expect(pollQuestion.question).toBe('This is a poll question');
// openmct.user.off('pollQuestionChange', pollQuestionChangeCallback);
// });
// });
it('cannot be set if the user is not permitted', () => {
openmct.user.setProvider(userProvider);
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));
return openmct.user.status.setPollQuestion('This is a poll question').catch((error) => {
expect(error).toBeInstanceOf(Error);
}).finally(() => {
expect(userProvider.setPollQuestion).not.toHaveBeenCalled();
});
});
});
});

View File

@ -33,7 +33,7 @@ function replaceDotsWithUnderscores(filename) {
import {saveAs} from 'saveAs';
import html2canvas from 'html2canvas';
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
class ImageExporter {
constructor(openmct) {
@ -51,7 +51,7 @@ class ImageExporter {
const overlays = this.openmct.overlays;
const dialog = overlays.dialog({
iconClass: 'info',
message: 'Capturing image, please wait...',
message: 'Caputuring an image',
buttons: [
{
label: 'Cancel',

View File

@ -52,6 +52,7 @@ export default (agent, document) => {
if (agent.isMobile()) {
const mediaQuery = window.matchMedia("(orientation: landscape)");
function eventHandler(event) {
console.log("changed");
if (event.matches) {
body.classList.remove("portrait");
body.classList.add("landscape");

View File

@ -20,8 +20,10 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([],
function () {
define(
['zepto'],
function ($) {
// Set of connection states; changing among these states will be
// reflected in the indicator's appearance.
// CONNECTED: Everything nominal, expect to be able to read/write.
@ -73,17 +75,12 @@ define([],
};
URLIndicator.prototype.fetchUrl = function () {
fetch(this.URLpath)
.then(response => {
if (response.ok) {
this.handleSuccess();
} else {
this.handleError();
}
})
.catch(error => {
this.handleError();
});
$.ajax({
type: 'GET',
url: this.URLpath,
success: this.handleSuccess,
error: this.handleError
});
};
URLIndicator.prototype.handleError = function (e) {

View File

@ -25,35 +25,37 @@ define(
"utils/testing",
"./URLIndicator",
"./URLIndicatorPlugin",
"../../MCT"
"../../MCT",
"zepto"
],
function (
testingUtils,
URLIndicator,
URLIndicatorPlugin,
MCT
MCT,
$
) {
const defaultAjaxFunction = $.ajax;
describe("The URLIndicator", function () {
let openmct;
let indicatorElement;
let pluginOptions;
let ajaxOptions;
let urlIndicator; // eslint-disable-line
let fetchSpy;
beforeEach(function () {
jasmine.clock().install();
openmct = new testingUtils.createOpenMct();
spyOn(openmct.indicators, 'add');
fetchSpy = spyOn(window, 'fetch').and.callFake(() => Promise.resolve({
ok: true
}));
spyOn($, 'ajax');
$.ajax.and.callFake(function (options) {
ajaxOptions = options;
});
});
afterEach(function () {
if (window.fetch.restore) {
window.fetch.restore();
}
$.ajax = defaultAjaxFunction;
jasmine.clock().uninstall();
return testingUtils.resetApplicationState(openmct);
@ -94,11 +96,11 @@ define(
expect(indicatorElement.classList.contains('iconClass-checked')).toBe(true);
});
it("uses custom interval", function () {
expect(window.fetch).toHaveBeenCalledTimes(1);
expect($.ajax.calls.count()).toEqual(1);
jasmine.clock().tick(1);
expect(window.fetch).toHaveBeenCalledTimes(1);
expect($.ajax.calls.count()).toEqual(1);
jasmine.clock().tick(pluginOptions.interval + 1);
expect(window.fetch).toHaveBeenCalledTimes(2);
expect($.ajax.calls.count()).toEqual(2);
});
it("uses custom label if supplied in initialization", function () {
expect(indicatorElement.textContent.indexOf(pluginOptions.label) >= 0).toBe(true);
@ -118,21 +120,18 @@ define(
it("requests the provided URL", function () {
jasmine.clock().tick(pluginOptions.interval + 1);
expect(window.fetch).toHaveBeenCalledWith(pluginOptions.url);
expect(ajaxOptions.url).toEqual(pluginOptions.url);
});
it("indicates success if connection is nominal", async function () {
it("indicates success if connection is nominal", function () {
jasmine.clock().tick(pluginOptions.interval + 1);
await urlIndicator.fetchUrl();
ajaxOptions.success();
expect(indicatorElement.classList.contains('s-status-on')).toBe(true);
});
it("indicates an error when the server cannot be reached", async function () {
fetchSpy.and.callFake(() => Promise.resolve({
ok: false
}));
it("indicates an error when the server cannot be reached", function () {
jasmine.clock().tick(pluginOptions.interval + 1);
await urlIndicator.fetchUrl();
ajaxOptions.error();
expect(indicatorElement.classList.contains('s-status-warning-hi')).toBe(true);
});
});

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
import AutoflowTabularConstants from './AutoflowTabularConstants';
import $ from 'zepto';
import DOMObserver from './dom-observer';
import {
createOpenMct,
@ -121,7 +122,7 @@ xdescribe("AutoflowTabularPlugin", () => {
name: "Object " + key
};
});
testContainer = document.createElement('div');
testContainer = $('<div>')[0];
domObserver = new DOMObserver(testContainer);
testHistories = testKeys.reduce((histories, key, index) => {
@ -194,7 +195,7 @@ xdescribe("AutoflowTabularPlugin", () => {
describe("when rows have been populated", () => {
function rowsMatch() {
const rows = testContainer.querySelectorAll(".l-autoflow-row").length;
const rows = $(testContainer).find(".l-autoflow-row").length;
return rows === testChildren.length;
}
@ -240,20 +241,20 @@ xdescribe("AutoflowTabularPlugin", () => {
const nextWidth =
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
expect(testContainer.querySelector('.l-autoflow-col').css('width'))
expect($(testContainer).find('.l-autoflow-col').css('width'))
.toEqual(initialWidth + 'px');
testContainer.querySelector('.change-column-width').click();
$(testContainer).find('.change-column-width').click();
function widthHasChanged() {
const width = testContainer.querySelector('.l-autoflow-col').css('width');
const width = $(testContainer).find('.l-autoflow-col').css('width');
return width !== initialWidth + 'px';
}
return domObserver.when(widthHasChanged)
.then(() => {
expect(testContainer.querySelector('.l-autoflow-col').css('width'))
expect($(testContainer).find('.l-autoflow-col').css('width'))
.toEqual(nextWidth + 'px');
});
});
@ -266,13 +267,13 @@ xdescribe("AutoflowTabularPlugin", () => {
it("displays historical telemetry", () => {
function rowTextDefined() {
return testContainer.querySelector(".l-autoflow-item").filter(".r").text() !== "";
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
}
return domObserver.when(rowTextDefined).then(() => {
testKeys.forEach((key, index) => {
const datum = testHistories[key];
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
@ -293,7 +294,7 @@ xdescribe("AutoflowTabularPlugin", () => {
return waitsForChange().then(() => {
testData.forEach((datum, index) => {
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
@ -311,7 +312,7 @@ xdescribe("AutoflowTabularPlugin", () => {
return waitsForChange().then(() => {
testKeys.forEach((datum, index) => {
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.hasClass(testClass)).toBe(true);
});
});
@ -321,16 +322,16 @@ xdescribe("AutoflowTabularPlugin", () => {
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
const count = testKeys.length;
const $container = testContainer;
const $container = $(testContainer);
let promiseChain = Promise.resolve();
function columnsHaveAutoflowed() {
const itemsHeight = $container.querySelector('.l-autoflow-items').height();
const itemsHeight = $container.find('.l-autoflow-items').height();
const availableHeight = itemsHeight - sliderHeight;
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
const columns = Math.ceil(count / availableRows);
return $container.querySelector('.l-autoflow-col').length === columns;
return $container.find('.l-autoflow-col').length === columns;
}
$container.find('.abs').css({

View File

@ -40,6 +40,14 @@ export default {
BarGraph
},
inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
default() {
return {};
}
}
},
data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
@ -67,9 +75,7 @@ export default {
this.setTimeContext();
this.loadComposition();
this.unobserveAxes = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.refreshData);
this.unobserveInterpolation = this.openmct.objects.observe(this.domainObject, 'configuration.useInterpolation', this.refreshData);
this.unobserveBar = this.openmct.objects.observe(this.domainObject, 'configuration.useBar', this.refreshData);
},
beforeDestroy() {
this.stopFollowingTimeContext();
@ -80,19 +86,8 @@ export default {
return;
}
this.composition.off('add', this.addToComposition);
this.composition.off('add', this.addTelemetryObject);
this.composition.off('remove', this.removeTelemetryObject);
if (this.unobserveAxes) {
this.unobserveAxes();
}
if (this.unobserveInterpolation) {
this.unobserveInterpolation();
}
if (this.unobserveBar) {
this.unobserveBar();
}
},
methods: {
setTimeContext() {
@ -110,42 +105,6 @@ export default {
this.timeContext.off('bounds', this.refreshData);
}
},
addToComposition(telemetryObject) {
if (Object.values(this.telemetryObjects).length > 0) {
this.confirmRemoval(telemetryObject);
} else {
this.addTelemetryObject(telemetryObject);
}
},
confirmRemoval(telemetryObject) {
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will replace the current telemetry source. Do you want to continue?',
buttons: [
{
label: 'Ok',
emphasis: true,
callback: () => {
const oldTelemetryObject = Object.values(this.telemetryObjects)[0];
this.removeFromComposition(oldTelemetryObject);
this.removeTelemetryObject(oldTelemetryObject.identifier);
this.addTelemetryObject(telemetryObject);
dialog.dismiss();
}
},
{
label: 'Cancel',
callback: () => {
this.removeFromComposition(telemetryObject);
dialog.dismiss();
}
}
]
});
},
removeFromComposition(telemetryObject) {
this.composition.remove(telemetryObject);
},
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
@ -206,12 +165,7 @@ export default {
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
const xAxisMetadata = metadata.valuesForHints(['range'])
.map((metaDatum) => {
metaDatum.isArrayValue = metadata.isArrayValue(metaDatum);
return metaDatum;
});
const xAxisMetadata = metadata.valuesForHints(['range']);
return {
xAxisMetadata,
@ -229,7 +183,13 @@ export default {
loadComposition() {
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addToComposition);
if (!this.composition) {
this.addTelemetryObject(this.domainObject);
return;
}
this.composition.on('add', this.addTelemetryObject);
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
},
@ -252,10 +212,7 @@ export default {
},
removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier);
if (this.telemetryObjects[key]) {
delete this.telemetryObjects[key];
}
delete this.telemetryObjects[key];
if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
delete this.telemetryObjectFormats[key];
}
@ -280,72 +237,49 @@ export default {
this.openmct.notifications.alert(data.message);
}
if (!this.isDataInTimeRange(data, key, telemetryObject)) {
return;
}
if (this.domainObject.configuration.axes.xKey === undefined || this.domainObject.configuration.axes.yKey === undefined) {
if (!this.isDataInTimeRange(data, key)) {
return;
}
let xValues = [];
let yValues = [];
let xAxisMetadata = axisMetadata.xAxisMetadata.find(metadata => metadata.key === this.domainObject.configuration.axes.xKey);
if (xAxisMetadata && xAxisMetadata.isArrayValue) {
//populate x and y values
let metadataKey = this.domainObject.configuration.axes.xKey;
if (data[metadataKey] !== undefined) {
xValues = this.parse(key, metadataKey, data);
}
metadataKey = this.domainObject.configuration.axes.yKey;
if (data[metadataKey] !== undefined) {
yValues = this.parse(key, metadataKey, data);
//populate X and Y values for plotly
axisMetadata.xAxisMetadata.forEach((metadata) => {
xValues.push(metadata.name);
if (data[metadata.key]) {
const formattedValue = this.format(key, metadata.key, data);
yValues.push(formattedValue);
} else {
yValues.push(null);
}
} else {
//populate X and Y values for plotly
axisMetadata.xAxisMetadata.filter(metadataObj => !metadataObj.isArrayValue).forEach((metadata) => {
if (!xAxisMetadata) {
//Assign the first metadata to use for any formatting
xAxisMetadata = metadata;
}
xValues.push(metadata.name);
if (data[metadata.key]) {
const parsedValue = this.parse(key, metadata.key, data);
yValues.push(parsedValue);
} else {
yValues.push(null);
}
});
}
});
let trace = {
key,
name: telemetryObject.name,
x: xValues,
y: yValues,
xAxisMetadata: xAxisMetadata,
text: yValues.map(String),
xAxisMetadata: axisMetadata.xAxisMetadata,
yAxisMetadata: axisMetadata.yAxisMetadata,
type: this.domainObject.configuration.useBar ? 'bar' : 'scatter',
mode: 'lines',
line: {
shape: this.domainObject.configuration.useInterpolation
},
type: this.options.type ? this.options.type : 'bar',
marker: {
color: this.domainObject.configuration.barStyles.series[key].color
},
hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y'
hoverinfo: 'skip'
};
if (this.options.type) {
trace.mode = 'markers';
trace.hoverinfo = 'x+y';
}
this.addTrace(trace, key);
},
isDataInTimeRange(datum, key, telemetryObject) {
isDataInTimeRange(datum, key) {
const timeSystemKey = this.timeContext.timeSystem().key;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValue = metadata.value(timeSystemKey) || { key: timeSystemKey };
let currentTimestamp = this.parse(key, metadataValue.key, datum);
let currentTimestamp = this.parse(key, timeSystemKey, datum);
return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp;
},
@ -365,8 +299,7 @@ export default {
},
requestDataFor(telemetryObject) {
const axisMetadata = this.getAxisMetadata(telemetryObject);
const options = this.getOptions();
this.openmct.telemetry.request(telemetryObject, options)
this.openmct.telemetry.request(telemetryObject)
.then(data => {
data.forEach((datum) => {
this.addDataToGraph(telemetryObject, datum, axisMetadata);

View File

@ -20,155 +20,18 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-bar-graph-options js-bar-plot-option">
<ul class="c-tree">
<h2 title="Display properties for this object">Bar Graph Series</h2>
<li>
<series-options
v-for="series in plotSeries"
:key="series.key"
:item="series"
:color-palette="colorPalette"
/>
</li>
</ul>
<div class="grid-properties">
<ul class="l-inspector-part">
<h2 title="Y axis settings for this object">Axes</h2>
<li class="grid-row">
<div
class="grid-cell label"
title="X axis selection."
>X Axis</div>
<div
v-if="isEditing"
class="grid-cell value"
>
<select
v-model="xKey"
@change="updateForm('xKey')"
>
<option
v-for="option in xKeyOptions"
:key="`xKey-${option.value}`"
:value="option.value"
:selected="option.value === xKey"
>
{{ option.name }}
</option>
</select>
</div>
<div
v-else
class="grid-cell value"
>{{ xKeyLabel }}</div>
</li>
<li
v-if="yKey !== ''"
class="grid-row"
>
<div
class="grid-cell label"
title="Y axis selection."
>Y Axis</div>
<div
v-if="isEditing"
class="grid-cell value"
>
<select
v-model="yKey"
@change="updateForm('yKey')"
>
<option
v-for="option in yKeyOptions"
:key="`yKey-${option.value}`"
:value="option.value"
:selected="option.value === yKey"
>
{{ option.name }}
</option>
</select>
</div>
<div
v-else
class="grid-cell value"
>{{ yKeyLabel }}</div>
</li>
</ul>
</div>
<div class="grid-properties">
<ul class="l-inspector-part">
<h2 title="Settings for plot">Settings</h2>
<li class="grid-row">
<div
v-if="isEditing"
class="grid-cell label"
title="Display style for the plot"
>Display Style</div>
<div
v-if="isEditing"
class="grid-cell value"
>
<select
v-model="useBar"
@change="updateBar"
>
<option :value="true">Bar</option>
<option :value="false">Line</option>
</select>
</div>
<div
v-if="!isEditing"
class="grid-cell label"
title="Display style for plot"
>Display Style</div>
<div
v-if="!isEditing"
class="grid-cell value"
>{{ {
'true': 'Bar',
'false': 'Line'
}[useBar] }}
</div>
</li>
<li
v-if="!useBar"
class="grid-row"
>
<div
v-if="isEditing"
class="grid-cell label"
title="The rendering method to join lines for this series."
>Line Method</div>
<div
v-if="isEditing"
class="grid-cell value"
>
<select
v-model="useInterpolation"
@change="updateInterpolation"
>
<option value="linear">Linear interpolate</option>
<option value="hv">Step after</option>
</select>
</div>
<div
v-if="!isEditing"
class="grid-cell label"
title="The rendering method to join lines for this series."
>Line Method</div>
<div
v-if="!isEditing"
class="grid-cell value"
>{{ {
'linear': 'Linear interpolation',
'hv': 'Step After'
}[useInterpolation] }}
</div>
</li>
</ul>
</div>
</div>
<ul class="c-tree c-bar-graph-options">
<h2 title="Display properties for this object">Bar Graph Series</h2>
<li
v-for="series in domainObject.composition"
:key="series.key"
>
<series-options
:item="series"
:color-palette="colorPalette"
/>
</li>
</ul>
</template>
<script>
@ -182,17 +45,8 @@ export default {
inject: ['openmct', 'domainObject'],
data() {
return {
xKey: this.domainObject.configuration.axes.xKey,
yKey: this.domainObject.configuration.axes.yKey,
xKeyLabel: '',
yKeyLabel: '',
plotSeries: [],
yKeyOptions: [],
xKeyOptions: [],
isEditing: this.openmct.editor.isEditing(),
colorPalette: this.colorPalette,
useInterpolation: this.domainObject.configuration.useInterpolation,
useBar: this.domainObject.configuration.useBar
colorPalette: this.colorPalette
};
},
computed: {
@ -205,187 +59,13 @@ export default {
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
this.composition = this.openmct.composition.get(this.domainObject);
this.registerListeners();
this.composition.load();
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
this.stopListening();
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
},
registerListeners() {
this.composition.on('add', this.addSeries);
this.composition.on('remove', this.removeSeries);
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setKeysAndSetupOptions);
},
stopListening() {
this.composition.off('add', this.addSeries);
this.composition.off('remove', this.removeSeries);
if (this.unobserve) {
this.unobserve();
}
},
addSeries(series, index) {
this.$set(this.plotSeries, this.plotSeries.length, series);
this.setupOptions();
},
removeSeries(seriesIdentifier) {
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier));
if (index >= 0) {
this.$delete(this.plotSeries, index);
this.setupOptions();
}
},
setKeysAndSetupOptions() {
this.xKey = this.domainObject.configuration.axes.xKey;
this.yKey = this.domainObject.configuration.axes.yKey;
this.setupOptions();
},
setupOptions() {
this.xKeyOptions = [];
this.yKeyOptions = [];
if (this.plotSeries.length <= 0) {
return;
}
let update = false;
const series = this.plotSeries[0];
const metadata = this.openmct.telemetry.getMetadata(series);
const metadataRangeValues = metadata.valuesForHints(['range']).map((metaDatum) => {
metaDatum.isArrayValue = metadata.isArrayValue(metaDatum);
return metaDatum;
});
const metadataArrayValues = metadataRangeValues.filter(metadataObj => metadataObj.isArrayValue);
const metadataValues = metadataRangeValues.filter(metadataObj => !metadataObj.isArrayValue);
metadataArrayValues.forEach((metadataValue) => {
this.xKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.key,
isArrayValue: metadataValue.isArrayValue
});
this.yKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.key,
isArrayValue: metadataValue.isArrayValue
});
});
//Metadata values that are not array values will be grouped together as x-axis only option.
// Here, the y-axis is not relevant.
if (metadataValues.length) {
this.xKeyOptions.push(
metadataValues.reduce((previousValue, currentValue) => {
return {
name: `${previousValue.name}, ${currentValue.name}`,
value: currentValue.key,
isArrayValue: currentValue.isArrayValue
};
})
);
}
let xKeyOptionIndex;
let yKeyOptionIndex;
if (this.domainObject.configuration.axes.xKey) {
xKeyOptionIndex = this.xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
if (xKeyOptionIndex > -1) {
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
this.xKeyLabel = this.xKeyOptions[xKeyOptionIndex].name;
}
} else {
if (this.xKey === undefined) {
update = true;
xKeyOptionIndex = 0;
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
this.xKeyLabel = this.xKeyOptions[xKeyOptionIndex].name;
}
}
if (metadataRangeValues.length > 1) {
if (this.domainObject.configuration.axes.yKey && this.domainObject.configuration.axes.yKey !== 'none') {
yKeyOptionIndex = this.yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) {
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;
}
} else {
if (this.yKey === undefined) {
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;
}
}
}
this.yKeyOptions = this.yKeyOptions.map((option, index) => {
if (index === xKeyOptionIndex) {
option.name = `${option.name} (swap)`;
option.swap = yKeyOptionIndex;
} else {
option.name = option.name.replace(' (swap)', '');
option.swap = undefined;
}
return option;
});
}
this.xKeyOptions = this.xKeyOptions.map((option, index) => {
if (index === yKeyOptionIndex) {
option.name = `${option.name} (swap)`;
option.swap = xKeyOptionIndex;
} else {
option.name = option.name.replace(' (swap)', '');
option.swap = undefined;
}
return option;
});
if (update === true) {
this.saveConfiguration();
}
},
updateForm(property) {
if (property === 'xKey') {
const xKeyOption = this.xKeyOptions.find(option => option.value === this.xKey);
if (xKeyOption.swap !== undefined) {
//swap
this.yKey = this.xKeyOptions[xKeyOption.swap].value;
} else if (!xKeyOption.isArrayValue) {
this.yKey = 'none';
} else {
this.yKey = undefined;
}
} else if (property === 'yKey') {
const yKeyOption = this.yKeyOptions.find(option => option.value === this.yKey);
if (yKeyOption.swap !== undefined) {
//swap
this.xKey = this.yKeyOptions[yKeyOption.swap].value;
}
}
this.saveConfiguration();
},
saveConfiguration() {
this.openmct.objects.mutate(this.domainObject, `configuration.axes`, {
xKey: this.xKey,
yKey: this.yKey
});
},
updateInterpolation(event) {
this.openmct.objects.mutate(this.domainObject, `configuration.useInterpolation`, this.useInterpolation);
},
updateBar(event) {
this.openmct.objects.mutate(this.domainObject, `configuration.useBar`, this.useBar);
}
}
};

View File

@ -38,19 +38,16 @@
<div class="c-object-label__name">{{ name }}</div>
</div>
</li>
<ul class="grid-properties">
<li class="grid-row">
<ColorSwatch
v-if="expanded"
:current-color="currentColor"
title="Manually set the color for this bar graph series."
edit-title="Manually set the color for this bar graph series."
view-title="The color for this bar graph series."
short-label="Color"
@colorSet="setColor"
/>
</li>
</ul>
<ColorSwatch
v-if="expanded"
:current-color="currentColor"
title="Manually set the color for this bar graph series."
edit-title="Manually set the color for this bar graph series"
view-title="The color for this bar graph series."
short-label="Color"
class="grid-properties"
@colorSet="setColor"
/>
</ul>
</template>
@ -112,6 +109,7 @@ export default {
}
},
mounted() {
this.key = this.openmct.objects.makeKeyString(this.item);
this.initColorAndName();
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
},
@ -122,7 +120,6 @@ export default {
},
methods: {
initColorAndName() {
this.key = this.openmct.objects.makeKeyString(this.item.identifier);
// this is called before the plot is initialized
if (!this.domainObject.configuration.barStyles.series[this.key]) {
const color = this.colorPalette.getNextColor().asHexString();

View File

@ -28,17 +28,14 @@ export default function () {
return function install(openmct) {
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
name: "Graph (Bar or Line)",
name: "Bar Graph",
cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
barStyles: { series: {} },
axes: {},
useInterpolation: 'linear',
useBar: true
barStyles: { series: {} }
};
},
priority: 891

View File

@ -57,18 +57,18 @@ describe("the plugin", function () {
const testTelemetry = [
{
'utc': 1,
'some-key': ['1.3222'],
'some-other-key': [1]
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1'
},
{
'utc': 2,
'some-key': ['2.555'],
'some-other-key': [2]
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2'
},
{
'utc': 3,
'some-key': ['3.888'],
'some-other-key': [3]
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3'
}
];
@ -123,6 +123,7 @@ describe("the plugin", function () {
});
describe("The bar graph view", () => {
let testDomainObject;
let barGraphObject;
// eslint-disable-next-line no-unused-vars
let component;
@ -134,21 +135,51 @@ describe("the plugin", function () {
namespace: "",
key: "test-plot"
},
configuration: {
barStyles: {
series: {}
},
axes: {},
useInterpolation: 'linear',
useBar: true
},
type: "telemetry.plot.bar-graph",
name: "Test Bar Graph"
};
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
configuration: {
barStyles: {
series: {}
}
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
return [];
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
@ -216,116 +247,15 @@ describe("the plugin", function () {
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
const barGraphView = plotViewProvider.view(barGraphObject, [barGraphObject]);
const barGraphView = plotViewProvider.view(testDomainObject, [testDomainObject]);
barGraphView.show(child, true);
expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object");
mockComposition.emit('add', dotFullTelemetryObject);
expect(barGraphObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
expect(testDomainObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
barGraphView.destroy();
});
});
describe("The spectral plot view for telemetry objects with array values", () => {
let barGraphObject;
// eslint-disable-next-line no-unused-vars
let component;
let mockComposition;
beforeEach(async () => {
barGraphObject = {
identifier: {
namespace: "",
key: "test-plot"
},
configuration: {
barStyles: {
series: {}
},
axes: {
xKey: 'some-key',
yKey: 'some-other-key'
},
useInterpolation: 'linear',
useBar: false
},
type: "telemetry.plot.bar-graph",
name: "Test Bar Graph"
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
return [];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
BarGraph
},
provide: {
openmct: openmct,
domainObject: barGraphObject,
composition: openmct.composition.get(barGraphObject)
},
template: "<BarGraph></BarGraph>"
});
await Vue.nextTick();
});
it("Renders spectral plots", () => {
const dotFullTelemetryObject = {
identifier: {
namespace: "someNamespace",
key: "~OpenMCT~outer.test-object.foo.bar"
},
type: "test-dotful-object",
name: "A Dotful Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
formatString: '%0.2f[]',
hints: {
range: 1
},
source: 'some-key'
}, {
key: "some-other-key",
name: "Another attribute",
format: "number[]",
hints: {
range: 2
},
source: 'some-other-key'
}]
}
};
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
const barGraphView = plotViewProvider.view(barGraphObject, [barGraphObject]);
barGraphView.show(child, true);
mockComposition.emit('add', dotFullTelemetryObject);
return Vue.nextTick().then(() => {
const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');
expect(plotElement).not.toBeNull();
barGraphView.destroy();
});
});
});
describe("the bar graph objects", () => {
const mockObject = {
name: 'A very nice bar graph',
@ -482,7 +412,7 @@ describe("the plugin", function () {
testDomainObject = {
identifier: {
namespace: "",
key: "~Some~foo.bar"
key: "test-object"
},
type: "test-object",
name: "Test Object",
@ -530,16 +460,11 @@ describe("the plugin", function () {
isAlias: true
}
}
},
axes: {},
useInterpolation: 'linear',
useBar: true
}
},
composition: [
{
identifier: {
key: '~Some~foo.bar'
}
key: '~Some~foo.bar'
}
]
}

View File

@ -89,7 +89,6 @@ export default function ClockPlugin(options) {
"key": "timezone",
"name": "Timezone",
"control": "autocomplete",
"cssClass": "c-clock__timezone-selection c-menu--no-icon",
"options": momentTimezone.tz.names(),
property: [
'configuration',

View File

@ -21,7 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { evaluateResults } from './utils/evaluator';
import { getLatestTimestamp } from './utils/time';

View File

@ -22,7 +22,7 @@
import Condition from "./Condition";
import { getLatestTimestamp } from './utils/time';
import { v4 as uuid } from 'uuid';
import uuid from "uuid";
import EventEmitter from 'EventEmitter';
export default class ConditionManager extends EventEmitter {

View File

@ -78,13 +78,11 @@ export default class StyleRuleManager extends EventEmitter {
this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {
this.openmct.telemetry.request(conditionSetDomainObject)
.then(output => {
if (output && output.length && (this.conditionSetIdentifier && this.openmct.objects.areIdsEqual(conditionSetDomainObject.identifier, this.conditionSetIdentifier))) {
if (output && output.length) {
this.handleConditionSetResultUpdated(output[0]);
}
});
if (this.conditionSetIdentifier && this.openmct.objects.areIdsEqual(conditionSetDomainObject.identifier, this.conditionSetIdentifier)) {
this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this));
}
this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this));
});
}

View File

@ -214,7 +214,7 @@
import Criterion from './Criterion.vue';
import ConditionDescription from "./ConditionDescription.vue";
import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
export default {
components: {

View File

@ -23,7 +23,7 @@ import ConditionSetViewProvider from './ConditionSetViewProvider.js';
import ConditionSetCompositionPolicy from "./ConditionSetCompositionPolicy";
import ConditionSetMetadataProvider from './ConditionSetMetadataProvider';
import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider';
import { v4 as uuid } from 'uuid';
import uuid from "uuid";
export default function ConditionPlugin() {

View File

@ -211,15 +211,13 @@ define(['lodash'], function (_) {
options: [
{
value: false,
icon: 'icon-frame-hide',
title: "Frame visible",
label: 'Hide frame'
icon: 'icon-frame-show',
title: "Frame visible"
},
{
value: true,
icon: 'icon-frame-show',
title: "Frame hidden",
label: 'Show frame'
icon: 'icon-frame-hide',
title: "Frame hidden"
}
]
};
@ -403,7 +401,6 @@ define(['lodash'], function (_) {
},
icon: "icon-pencil",
title: "Edit text properties",
label: "Edit text",
dialog: DIALOG_FORM.text
};
}
@ -517,14 +514,12 @@ define(['lodash'], function (_) {
{
value: true,
icon: 'icon-eye-open',
title: "Show units",
label: "Show units"
title: "Show units"
},
{
value: false,
icon: 'icon-eye-disabled',
title: "Hide units",
label: "Hide units"
title: "Hide units"
}
]
};
@ -567,7 +562,6 @@ define(['lodash'], function (_) {
domainObject: selectedParent,
icon: "icon-object",
title: "Switch the way this telemetry is displayed",
label: "View type",
options: viewOptions,
method: function (option) {
displayLayoutContext.switchViewType(selectedItemContext, option.value, selection);
@ -668,9 +662,9 @@ define(['lodash'], function (_) {
'display-mode': [],
'telemetry-value': [],
'style': [],
'unit-toggle': [],
'position': [],
'duplicate': [],
'unit-toggle': [],
'remove': [],
'toggle-grid': []
};
@ -695,7 +689,6 @@ define(['lodash'], function (_) {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects),
@ -719,17 +712,9 @@ define(['lodash'], function (_) {
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
}
if (toolbar['unit-toggle'].length === 0) {
let toggleUnitsButton = getToggleUnitsButton(selectedParent, selectedObjects);
if (toggleUnitsButton) {
toolbar['unit-toggle'] = [toggleUnitsButton];
}
}
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects),
@ -744,11 +729,17 @@ define(['lodash'], function (_) {
if (toolbar.viewSwitcher.length === 0) {
toolbar.viewSwitcher = [getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects)];
}
if (toolbar['unit-toggle'].length === 0) {
let toggleUnitsButton = getToggleUnitsButton(selectedParent, selectedObjects);
if (toggleUnitsButton) {
toolbar['unit-toggle'] = [toggleUnitsButton];
}
}
} else if (layoutItem.type === 'text-view') {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects),
@ -767,7 +758,6 @@ define(['lodash'], function (_) {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects),
@ -782,7 +772,6 @@ define(['lodash'], function (_) {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects),
@ -797,7 +786,6 @@ define(['lodash'], function (_) {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getX2Input(selectedParent, selectedObjects),

View File

@ -73,7 +73,7 @@
</template>
<script>
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
import SubobjectView from './SubobjectView.vue';
import TelemetryView from './TelemetryView.vue';
import BoxView from './BoxView.vue';

View File

@ -25,7 +25,8 @@
class="l-layout__frame c-frame"
:class="{
'no-frame': !item.hasFrame,
'u-inspectable': inspectable
'u-inspectable': inspectable,
'is-in-small-container': size.width < 600 || size.height < 600
}"
:style="style"
>

View File

@ -91,7 +91,7 @@ export default {
width: DEFAULT_TELEMETRY_DIMENSIONS[0],
height: DEFAULT_TELEMETRY_DIMENSIONS[1],
displayMode: 'all',
value: metadata.getDefaultDisplayValue()?.key,
value: metadata.getDefaultDisplayValue(),
stroke: "",
fill: "",
color: "",

View File

@ -9,6 +9,10 @@
> *:first-child {
flex: 1 1 auto;
}
&.is-in-small-container {
//background: rgba(blue, 0.1);
}
}
.c-frame__move-bar {
@ -28,6 +32,7 @@
&[s-selected] {
// All frames selected while editing
border: $editFrameSelectedBorder;
box-shadow: $editFrameSelectedShdw;
.c-frame__move-bar {

View File

@ -17,12 +17,12 @@
}
}
&__value {
@include isLimit();
> * + * {
margin-left: $interiorMargin;
}
&__label {
margin-right: $interiorMargin;
&__value {
@include isLimit();
}
.c-frame & {

View File

@ -41,7 +41,7 @@ describe('the plugin', function () {
element.appendChild(child);
openmct.on('start', done);
openmct.start(child);
openmct.startHeadless();
});
afterEach(() => {
@ -88,35 +88,6 @@ describe('the plugin', function () {
expect(displayLayoutViewProvider).toBeDefined();
});
it('renders a display layout view without errors', () => {
const testViewObject = {
identifier: {
namespace: 'test-namespace',
key: 'test-key'
},
type: 'layout',
configuration: {
items: [],
layoutGrid: [10, 10]
},
composition: []
};
const applicableViews = openmct.objectViews.get(testViewObject, []);
let displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
let view = displayLayoutViewProvider.view(testViewObject);
let error;
try {
view.show(child, false);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
});
describe('the alpha numeric format view', () => {
let displayLayoutItem;
let telemetryItem;
@ -380,7 +351,7 @@ describe('the plugin', function () {
it('provides controls including separators', () => {
const displayLayoutToolbar = openmct.toolbars.get(selection);
expect(displayLayoutToolbar.length).toBe(8);
expect(displayLayoutToolbar.length).toBe(7);
});
});
});

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
/**
* This class encapsulates the process of duplicating/copying a domain object

View File

@ -22,7 +22,7 @@
import JSONExporter from '/src/exporters/JSONExporter.js';
import _ from 'lodash';
import { v4 as uuid } from 'uuid';
import uuid from "uuid";
export default class ExportAsJSONAction {
constructor(openmct) {

View File

@ -1,129 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
v-if="isShowDetails"
class="c-inspector__properties c-inspect-properties"
>
<div class="c-inspect-properties__header">Fault Details</div>
<ul
class="c-inspect-properties__section"
>
<DetailText :detail="sourceDetails" />
<DetailText :detail="occuredDetails" />
<DetailText :detail="criticalityDetails" />
<DetailText :detail="descriptionDetails" />
</ul>
<div class="c-inspect-properties__header">Telemetry</div>
<ul
class="c-inspect-properties__section"
>
<DetailText :detail="systemDetails" />
<DetailText :detail="tripValueDetails" />
<DetailText :detail="currentValueDetails" />
</ul>
</div>
</template>
<script>
import DetailText from '@/ui/inspector/details/DetailText.vue';
export default {
name: 'FaultManagementInspector',
components: {
DetailText
},
inject: ['openmct'],
data() {
return {
isShowDetails: false
};
},
computed: {
criticalityDetails() {
return {
name: 'Criticality',
value: this.selectedFault?.severity
};
},
currentValueDetails() {
return {
name: 'Live value',
value: this.selectedFault?.currentValueInfo?.value
};
},
descriptionDetails() {
return {
name: 'Description',
value: this.selectedFault?.shortDescription
};
},
occuredDetails() {
return {
name: 'Occured',
value: this.selectedFault?.triggerTime
};
},
sourceDetails() {
return {
name: 'Source',
value: this.selectedFault?.name
};
},
systemDetails() {
return {
name: 'System',
value: this.selectedFault?.namespace
};
},
tripValueDetails() {
return {
name: 'Trip Value',
value: this.selectedFault?.triggerValueInfo?.value
};
}
},
mounted() {
this.updateSelectedFaults();
},
methods: {
updateSelectedFaults() {
const selection = this.openmct.selection.get();
this.isShowDetails = false;
if (selection.length === 0 || selection[0].length < 2) {
return;
}
const selectedFaults = selection[0][1].context.selectedFaults;
if (selectedFaults.length !== 1) {
return;
}
this.isShowDetails = true;
this.selectedFault = selectedFaults[0];
}
}
};
</script>

View File

@ -1,71 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import FaultManagementInspector from './FaultManagementInspector.vue';
import Vue from 'vue';
import { FAULT_MANAGEMENT_INSPECTOR, FAULT_MANAGEMENT_TYPE } from './constants';
export default function FaultManagementInspectorViewProvider(openmct) {
return {
openmct: openmct,
key: FAULT_MANAGEMENT_INSPECTOR,
name: 'FAULT_MANAGEMENT_TYPE',
canView: (selection) => {
if (selection.length !== 1 || selection[0].length === 0) {
return false;
}
let object = selection[0][0].context.item;
return object && object.type === FAULT_MANAGEMENT_TYPE;
},
view: (selection) => {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
FaultManagementInspector
},
provide: {
openmct
},
template: '<FaultManagementInspector></FaultManagementInspector>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: () => {
return 1;
}
};
}

View File

@ -1,103 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-fault-mgmt__list-header c-fault-mgmt__list">
<div class="c-fault-mgmt__checkbox">
<input
type="checkbox"
:checked="isSelectAll"
@input="selectAll"
>
</div>
<div class="c-fault-mgmt__list-content">
<div class="c-fault-mgmt__list-header-results"> {{ totalFaultsCount }} Results </div>
<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__list-header-liveVal c-fault-mgmt__list-curVal">Live Value</div>
<div class="c-fault-mgmt__list-header-trigTime c-fault-mgmt__list-trigTime">Trigger Time</div>
</div>
</div>
<div class="c-fault-mgmt__list-action-wrapper">
<div class="c-fault-mgmt__list-header-sortButton c-fault-mgmt__list-action-button">
<SelectField
class="c-fault-mgmt-viewButton"
title="Sort By"
:model="model"
@onChange="onChange"
/>
</div>
</div>
</div>
</template>
<script>
import SelectField from '@/api/forms/components/controls/SelectField.vue';
import { SORT_ITEMS } from './constants';
export default {
components: {
SelectField
},
inject: ['openmct', 'domainObject'],
props: {
selectedFaults: {
type: Array,
default() {
return [];
}
},
totalFaultsCount: {
type: Number,
default() {
return 0;
}
}
},
data() {
return {
model: {}
};
},
computed: {
isSelectAll() {
return this.totalFaultsCount > 0 && this.selectedFaults.length === this.totalFaultsCount;
}
},
beforeMount() {
const options = Object.values(SORT_ITEMS);
this.model = {
options,
value: options[0].value
};
},
methods: {
onChange(data) {
this.$emit('sortChanged', data);
},
selectAll(e) {
this.$emit('selectAll', e.target.checked);
}
}
};
</script>

View File

@ -1,191 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
class="c-fault-mgmt__list data-selectable"
:class="[
{'is-selected': isSelected},
{'is-unacknowledged': !fault.acknowledged},
{'is-shelved': fault.shelved}
]"
>
<div class="c-fault-mgmt__checkbox">
<input
type="checkbox"
:checked="isSelected"
@input="toggleSelected"
>
</div>
<div
class="c-fault-mgmt__list-severity"
:title="fault.severity"
:class="[
'is-severity-' + severity
]"
>
</div>
<div class="c-fault-mgmt__list-content">
<div class="c-fault-mgmt__list-pathname">
<div class="c-fault-mgmt__list-path">{{ fault.namespace }}</div>
<div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div>
</div>
<div class="c-fault-mgmt__list-content-right">
<div
class="c-fault-mgmt__list-trigVal"
:class="tripValueClassname"
title="Trip Value"
>{{ fault.triggerValueInfo.value }}</div>
<div
class="c-fault-mgmt__list-curVal"
:class="liveValueClassname"
title="Live Value"
>
{{ fault.currentValueInfo.value }}
</div>
<div
class="c-fault-mgmt__list-trigTime"
>{{ fault.triggerTime }}
</div>
</div>
</div>
<div class="c-fault-mgmt__list-action-wrapper">
<button
class="c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots"
title="Disposition Actions"
@click="showActionMenu"
></button>
</div>
</div>
</template>
<script>
const RANGE_CONDITION_CLASS = {
'LOW': 'is-limit--lwr',
'HIGH': 'is-limit--upr'
};
const SEVERITY_CLASS = {
'CRITICAL': 'is-limit--red',
'WARNING': 'is-limit--yellow',
'WATCH': 'is-limit--cyan'
};
export default {
inject: ['openmct', 'domainObject'],
props: {
fault: {
type: Object,
required: true
},
isSelected: {
type: Boolean,
default: () => {
return false;
}
}
},
computed: {
liveValueClassname() {
const currentValueInfo = this.fault?.currentValueInfo;
if (!currentValueInfo || currentValueInfo.monitoringResult === 'IN_LIMITS') {
return '';
}
let classname = RANGE_CONDITION_CLASS[currentValueInfo.rangeCondition] || '';
classname += ' ';
classname += SEVERITY_CLASS[currentValueInfo.monitoringResult] || '';
return classname.trim();
},
name() {
return `${this.fault?.name}/${this.fault?.namespace}`;
},
severity() {
return this.fault?.severity?.toLowerCase();
},
triggerTime() {
return this.fault?.triggerTime;
},
triggerValue() {
return this.fault?.triggerValueInfo?.value;
},
tripValueClassname() {
const triggerValueInfo = this.fault?.triggerValueInfo;
if (!triggerValueInfo || triggerValueInfo.monitoringResult === 'IN_LIMITS') {
return '';
}
let classname = RANGE_CONDITION_CLASS[triggerValueInfo.rangeCondition] || '';
classname += ' ';
classname += SEVERITY_CLASS[triggerValueInfo.monitoringResult] || '';
return classname.trim();
}
},
methods: {
showActionMenu(event) {
event.stopPropagation();
const menuItems = [
{
cssClass: 'icon-bell',
isDisabled: this.fault.acknowledged,
name: 'Acknowledge',
description: '',
onItemClicked: (e) => {
this.$emit('acknowledgeSelected', [this.fault]);
}
},
{
cssClass: 'icon-timer',
name: 'Shelve',
description: '',
onItemClicked: () => {
this.$emit('shelveSelected', [this.fault], { shelved: true });
}
},
{
cssClass: 'icon-timer',
isDisabled: Boolean(!this.fault.shelved),
name: 'Unshelve',
description: '',
onItemClicked: () => {
this.$emit('shelveSelected', [this.fault], { shelved: false });
}
}
];
this.openmct.menus.showMenu(event.x, event.y, menuItems);
},
toggleSelected(event) {
const faultData = {
fault: this.fault,
selected: event.target.checked
};
this.$emit('toggleSelected', faultData);
}
}
};
</script>

View File

@ -1,299 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-faults-list-view">
<FaultManagementSearch
:search-term="searchTerm"
@filterChanged="updateFilter"
@updateSearchTerm="updateSearchTerm"
/>
<FaultManagementToolbar
v-if="showToolbar"
:selected-faults="selectedFaults"
@acknowledgeSelected="toggleAcknowledgeSelected"
@shelveSelected="toggleShelveSelected"
/>
<FaultManagementListHeader
class="header"
:selected-faults="Object.values(selectedFaults)"
:total-faults-count="filteredFaultsList.length"
@selectAll="selectAll"
@sortChanged="sortChanged"
/>
<template v-if="filteredFaultsList.length > 0">
<FaultManagementListItem
v-for="fault of filteredFaultsList"
:key="fault.id"
:fault="fault"
:is-selected="isSelected(fault)"
@toggleSelected="toggleSelected"
@acknowledgeSelected="toggleAcknowledgeSelected"
@shelveSelected="toggleShelveSelected"
/>
</template>
</div>
</template>
<script>
import FaultManagementListHeader from './FaultManagementListHeader.vue';
import FaultManagementListItem from './FaultManagementListItem.vue';
import FaultManagementSearch from './FaultManagementSearch.vue';
import FaultManagementToolbar from './FaultManagementToolbar.vue';
import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants';
export default {
components: {
FaultManagementListHeader,
FaultManagementListItem,
FaultManagementSearch,
FaultManagementToolbar
},
inject: ['openmct', 'domainObject'],
props: {
faultsList: {
type: Array,
default: () => []
}
},
data() {
return {
filterIndex: 0,
searchTerm: '',
selectedFaults: {},
sortBy: Object.values(SORT_ITEMS)[0].value
};
},
computed: {
filteredFaultsList() {
const filterName = FILTER_ITEMS[this.filterIndex];
let list = this.faultsList.filter(fault => !fault.shelved);
if (filterName === 'Acknowledged') {
list = this.faultsList.filter(fault => fault.acknowledged);
}
if (filterName === 'Unacknowledged') {
list = this.faultsList.filter(fault => !fault.acknowledged);
}
if (filterName === 'Shelved') {
list = this.faultsList.filter(fault => fault.shelved);
}
if (this.searchTerm.length > 0) {
list = list.filter(this.filterUsingSearchTerm);
}
list.sort(SORT_ITEMS[this.sortBy].sortFunction);
return list;
},
showToolbar() {
return this.openmct.faults.supportsActions();
}
},
methods: {
filterUsingSearchTerm(fault) {
if (fault?.id?.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
if (fault?.triggerValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
if (fault?.currentValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
if (fault?.triggerTime.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
if (fault?.severity.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
return false;
},
isSelected(fault) {
return Boolean(this.selectedFaults[fault.id]);
},
selectAll(toggle = false) {
this.faultsList.forEach(fault => {
const faultData = {
fault,
selected: toggle
};
this.toggleSelected(faultData);
});
},
sortChanged(sort) {
this.sortBy = sort.value;
},
toggleSelected({ fault, selected = false}) {
if (selected) {
this.$set(this.selectedFaults, fault.id, fault);
} else {
this.$delete(this.selectedFaults, fault.id);
}
const selectedFaults = Object.values(this.selectedFaults);
this.openmct.selection.select(
[
{
element: this.$el,
context: {
item: this.openmct.router.path[0]
}
},
{
element: this.$el,
context: {
selectedFaults
}
}
],
false);
},
toggleAcknowledgeSelected(faults = Object.values(this.selectedFaults)) {
let title = '';
if (faults.length > 1) {
title = `Acknowledge ${faults.length} selected faults`;
} else {
title = `Acknowledge fault: ${faults[0].name}`;
}
const formStructure = {
title,
sections: [
{
rows: [
{
key: 'comment',
control: 'textarea',
name: 'Comment',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
value: ''
}
]
}
],
buttons: {
submit: {
label: 'Acknowledge'
}
}
};
this.openmct.forms.showForm(formStructure)
.then(data => {
Object.values(faults)
.forEach(selectedFault => {
this.openmct.faults.acknowledgeFault(selectedFault, data);
});
});
this.selectedFaults = {};
},
async toggleShelveSelected(faults = Object.values(this.selectedFaults), shelveData = {}) {
const { shelved = true } = shelveData;
if (shelved) {
let title = faults.length > 1
? `Shelve ${faults.length} selected faults`
: `Shelve fault: ${faults[0].name}`
;
const formStructure = {
title,
sections: [
{
rows: [
{
key: 'comment',
control: 'textarea',
name: 'Comment',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
value: ''
},
{
key: 'shelveDuration',
control: 'select',
name: 'Shelve Duration',
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
required: false,
cssClass: 'l-input-lg',
value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
}
]
}
],
buttons: {
submit: {
label: 'Shelve'
}
}
};
let data;
try {
data = await this.openmct.forms.showForm(formStructure);
} catch (e) {
return;
}
shelveData.comment = data.comment || '';
shelveData.shelveDuration = data.shelveDuration !== undefined
? data.shelveDuration
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
} else {
shelveData = {
shelved: false
};
}
Object.values(faults)
.forEach(selectedFault => {
this.openmct.faults.shelveFault(selectedFault, shelveData);
});
this.selectedFaults = {};
},
updateFilter(filter) {
this.selectAll();
this.filterIndex = filter.model.options.findIndex(option => option.value === filter.value);
},
updateSearchTerm(term = '') {
this.searchTerm = term.toLowerCase();
}
}
};
</script>

View File

@ -1,56 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { FAULT_MANAGEMENT_TYPE, FAULT_MANAGEMENT_VIEW, FAULT_MANAGEMENT_NAMESPACE } from './constants';
export default class FaultManagementObjectProvider {
constructor(openmct) {
this.openmct = openmct;
this.namespace = FAULT_MANAGEMENT_NAMESPACE;
this.key = FAULT_MANAGEMENT_VIEW;
this.objects = {};
this.createFaultManagementRootObject();
}
createFaultManagementRootObject() {
this.rootObject = {
identifier: {
key: this.key,
namespace: this.namespace
},
name: 'Fault Management',
type: FAULT_MANAGEMENT_TYPE,
location: 'ROOT'
};
this.openmct.objects.addRoot(this.rootObject.identifier);
}
get(identifier) {
if (identifier.key === FAULT_MANAGEMENT_VIEW) {
return Promise.resolve(this.rootObject);
}
return Promise.reject();
}
}

View File

@ -1,42 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import FaultManagementViewProvider from './FaultManagementViewProvider';
import FaultManagementObjectProvider from './FaultManagementObjectProvider';
import FaultManagementInspectorViewProvider from './FaultManagementInspectorViewProvider';
import { FAULT_MANAGEMENT_TYPE, FAULT_MANAGEMENT_NAMESPACE } from './constants';
export default function FaultManagementPlugin() {
return function (openmct) {
openmct.types.addType(FAULT_MANAGEMENT_TYPE, {
name: 'Fault Management',
creatable: false,
description: 'Fault Management View',
cssClass: 'icon-telemetry'
});
openmct.objectViews.addProvider(new FaultManagementViewProvider(openmct));
openmct.inspectorViews.addProvider(new FaultManagementInspectorViewProvider(openmct));
openmct.objects.addProvider(FAULT_MANAGEMENT_NAMESPACE, new FaultManagementObjectProvider(openmct));
};
}

Some files were not shown because too many files have changed in this diff Show More