Compare commits

..

55 Commits

Author SHA1 Message Date
a181faff4e Address merge conflict 2022-05-19 17:54:30 -07:00
10c26ac39a Move styling from the API into the view 2022-05-19 16:48:35 -07:00
7d3d68277b Move status style configuration into Open 2022-05-19 15:41:15 -07:00
a34190f44c Fixed issues found in test environment 2022-05-19 08:53:36 -07:00
f6f8db3142 Merge branch 'master' into operator-status 2022-05-18 09:25:25 -07:00
4619ab23f5 Merge branch 'master' into operator-status 2022-05-18 08:33:11 -07:00
8bcbb776c5 Additional tests for raf utility function 2022-05-18 08:32:52 -07:00
ab93b61530 Added test spec for raf utility function 2022-05-18 08:06:55 -07:00
35e2d2c643 Revert changes to index.html 2022-05-18 07:12:30 -07:00
3663f22807 Fix failing tests 2022-05-18 07:11:11 -07:00
d58111c0ce Added jsdocs 2022-05-17 17:09:56 -07:00
be8e9ba605 Fix test issues 2022-05-17 09:31:16 -07:00
0d6359bc58 Add copyright notices 2022-05-17 06:48:24 -07:00
60d759f76c Error handling 2022-05-16 17:49:02 -07:00
28e3d2e066 Create RAF utility class 2022-05-16 15:18:42 -07:00
934285b3fb Cleanup and refactoring 2022-05-16 15:18:30 -07:00
fe7b368cbd Refactor user status to its own sub-API 2022-05-16 13:56:09 -07:00
f8838e5a6e Move status into separate API 2022-05-16 08:45:16 -07:00
aabde3df80 Removed redundant comment 2022-05-16 08:12:40 -07:00
c51cf72fa7 Implemented status reset 2022-05-13 14:20:06 -07:00
72cbd3dd3b Re-fetch status summary on status change 2022-05-13 12:44:52 -07:00
d93ed2b69c Refactoring and code cleanup 2022-05-13 11:32:16 -07:00
22e45848af Refactor how status summary is determined to simplify API 2022-05-13 11:13:49 -07:00
c94bab7964 Get roles by status instead of users 2022-05-13 06:55:07 -07:00
63363ccbb9 Some code cleanup 2022-05-11 09:10:58 -07:00
bdfc99ed03 Also fix positioning of status indicator 2022-05-11 06:55:51 -07:00
458e9211e9 Merge branch 'operator-status' of https://github.com/nasa/openmct into operator-status 2022-05-11 06:43:36 -07:00
5399e06370 Fix positioning of popups 2022-05-11 06:43:30 -07:00
e76fed4005 Cherrypick symbols font updates from restricted-notebook branch. This is the most full and complete version of the symbols font - OVERRIDE ANY MERGE CONFLICTS WITH THIS COMMIT! 2022-05-10 17:02:46 -07:00
f4af7aa2f4 Styling for operator status
- Changed user indicator to display response when set to other than "NO_STATUS".
- Standardized icon display.
2022-05-10 11:28:01 -07:00
ad081c0db7 Merge branch 'operator-status' of https://github.com/nasa/openmct into operator-status 2022-05-10 07:48:19 -07:00
32b1ccff5b Make User API a little less brittle 2022-05-10 07:48:16 -07:00
343ea86f48 Styling for operator status
- Added default color for indicator icon;
2022-05-09 16:00:00 -07:00
98e0fc5bfb Styling for operator status
- Fixed erroneous font glyph mapping;
2022-05-09 15:51:47 -07:00
fb2cbc72ba Styling for operator status
- WIP on OperatorStatus styling.
2022-05-07 18:17:38 -07:00
c85e6904a3 Styling for operator status
- New icon glyph - IMPORTANT: OVERRIDE ANY MERGE CONFLICTS USING THIS COMMIT!
2022-05-07 18:14:53 -07:00
6918d7d3d9 Get statuses from providers. Reset statuses when poll question set 2022-05-06 11:17:38 -07:00
814d8a8380 Merge branch 'master' into operator-status 2022-05-05 16:41:18 -07:00
5035403507 Implementing status summary 2022-05-05 16:41:01 -07:00
1fc082afe5 Show poll question in op stat indicator 2022-05-05 11:30:17 -07:00
441e24a78a Fix positioning and onclick bug 2022-05-04 17:03:17 -07:00
7ec02258e8 Set status class on indicator. Clear all statuses 2022-05-04 16:19:44 -07:00
45c66b758b Fixed event handling 2022-05-04 13:38:33 -07:00
e197d39ce0 Apply config options 2022-05-04 12:58:33 -07:00
c8a4ff09f8 Adding admin indicator 2022-04-29 08:55:09 -07:00
2550d93b1e Update icon with status 2022-04-29 07:00:17 -07:00
7bcf136f43 Updated example user provider 2022-04-28 09:19:31 -07:00
245d61adda Adding user status API 2022-04-27 15:14:32 -07:00
18b05b6faf Support adding indicators asynchronously 2022-04-27 15:14:07 -07:00
b950031eae Merged from master 2022-04-27 15:13:06 -07:00
a72aa10d7b Implementing user role status API 2022-04-27 09:00:33 -07:00
c5ff94ad7a Fixed issue with poll question subscription 2022-04-26 17:24:35 -07:00
e75cc35cb7 Moved operator status plugin to Open 2022-04-26 14:58:52 -07:00
f216bd8769 Emit event on click 2022-04-04 14:51:27 -07:00
79a430278e Added click event to simple indicator 2022-03-30 19:10:12 -07:00
262 changed files with 3095 additions and 12695 deletions

View File

@ -23,7 +23,7 @@ commands:
- node/install: - node/install:
install-npm: true install-npm: true
node-version: << parameters.node-version >> node-version: << parameters.node-version >>
- run: npm install --prefer-offline --no-audit --progress=false - run: npm install
restore_cache_cmd: 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" 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: parameters:
@ -128,25 +128,11 @@ jobs:
suite: suite:
type: string type: string
executor: pw-focal-development executor: pw-focal-development
parallelism: 4
steps: steps:
- build_and_install: - build_and_install:
node-version: <<parameters.node-version>> 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
- store_test_results: - run: npm run test:e2e:<<parameters.suite>>
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: - store_test_results:
path: test-results/results.xml path: test-results/results.xml
- store_artifacts: - store_artifacts:
@ -164,6 +150,10 @@ workflows:
browser: ChromeHeadless browser: ChromeHeadless
post-steps: post-steps:
- upload_code_covio - upload_code_covio
- unit-test:
name: node16-chrome
node-version: lts/gallium
browser: ChromeHeadless
- unit-test: - unit-test:
name: node18-chrome name: node18-chrome
node-version: "18" node-version: "18"
@ -172,8 +162,6 @@ workflows:
name: e2e-ci name: e2e-ci
node-version: lts/gallium node-version: lts/gallium
suite: ci suite: ci
- perf-test:
node-version: lts/gallium
the-nightly: #These jobs do not run on PRs, but against master at night the-nightly: #These jobs do not run on PRs, but against master at night
jobs: jobs:
- unit-test: - unit-test:

View File

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

View File

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

View File

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

View File

@ -4,26 +4,12 @@
const base = require('@playwright/test'); const base = require('@playwright/test');
const { expect } = 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({ exports.test = base.test.extend({
page: async ({ baseURL, page }, use) => { page: async ({ baseURL, page }, use) => {
const messages = []; const messages = [];
page.on('console', (msg) => messages.push(msg)); page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`));
await use(page); await use(page);
messages.forEach( await expect.soft(messages.toString()).not.toContain('[error]');
msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error')
);
}, },
browser: async ({ playwright, browser }, use, workerInfo) => { browser: async ({ playwright, browser }, use, workerInfo) => {
// Use browserless if configured // Use browserless if configured

View File

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

View File

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

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} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
retries: 0, // visual tests should never retry due to snapshot comparison errors retries: 0,
testDir: 'tests/visual', testDir: 'tests',
timeout: 90 * 1000, timeout: 90 * 1000,
workers: 1, // visual tests should never run in parallel due to test pollution workers: 1,
webServer: { webServer: {
command: 'npm run start', command: 'npm run start',
port: 8080, port: 8080,
@ -17,7 +17,7 @@ const config = {
use: { use: {
browserName: "chromium", browserName: "chromium",
baseURL: 'http://localhost:8080/', baseURL: 'http://localhost:8080/',
headless: true, // this needs to remain headless to avoid visual changes due to GPU headless: true,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'on', screenshot: 'on',
trace: 'off', trace: 'off',
@ -25,7 +25,8 @@ const config = {
}, },
reporter: [ reporter: [
['list'], ['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"]', ''); await page.fill('text=Properties Title Notes >> input[type="text"]', '');
// Press Tab // Press Tab
await page.press('text=Properties Title Notes >> input[type="text"]', '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'); 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"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1); 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([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('text=OK').click(),
@ -58,10 +54,6 @@ test.describe('Move item tests', () => {
await page.locator('li.icon-folder').click(); await page.locator('li.icon-folder').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click(); await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2); 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([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('text=OK').click(),
@ -80,8 +72,10 @@ test.describe('Move item tests', () => {
}); });
await page.locator('li.icon-move').click(); await page.locator('li.icon-move').click();
await page.locator('form[name="mctForm"] >> text=My Items').click(); await page.locator('form[name="mctForm"] >> text=My Items').click();
await Promise.all([
await page.locator('text=OK').click(); page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Expect that Folder 2 is in My Items, the root folder // Expect that Folder 2 is in My Items, the root folder
expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy(); 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('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"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable); await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
await Promise.all([
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 page.waitForNavigation(),
await page.click('form[name="mctForm"] a:has-text("My Items")'); page.locator('text=OK').click()
]);
await page.locator('text=OK').click();
// Finish editing and save Telemetry Table // Finish editing and save Telemetry Table
await page.locator('.c-button--menu.c-button--major.icon-save').click(); 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 // Continue test regardless of assertion and create it in My Items
await page.locator('form[name="mctForm"] >> text=My Items').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()
]);
// Open My Items // Open My Items
await page.locator('text=Open MCT My Items >> span').nth(3).click(); await page.locator('text=Open MCT My Items >> span').nth(3).click();

View File

@ -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 // Click .icon-arrow-down
await page.locator('.icon-arrow-down').click(); await page.locator('.icon-arrow-down').click();
//verify if the autocomplete dropdown is visible //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 // Click .icon-arrow-down
await page.locator('.icon-arrow-down').click(); await page.locator('.icon-arrow-down').click();
// Verify clicking on the autocomplete arrow collapses the dropdown // 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 // 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 //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 // Verify clicking outside the autocomplete dropdown collapses it
await page.locator('text=Timezone').click(); await page.locator('text=Timezone').click();
// Verify clicking on the autocomplete arrow collapses the dropdown // 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,10 +32,7 @@ const { expect } = require('@playwright/test');
let conditionSetUrl; let conditionSetUrl;
let getConditionSetIdentifierFromUrl; let getConditionSetIdentifierFromUrl;
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
//Go to baseURL //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@ -43,7 +40,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
// Click text=Condition Set // Click text=Condition Set
await page.locator('li:has-text("Condition Set")').click(); await page.click('text=Condition Set');
// Click text=OK // Click text=OK
await Promise.all([ await Promise.all([
@ -51,6 +48,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
page.click('text=OK') page.click('text=OK')
]); ]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Save localStorage for future test execution //Save localStorage for future test execution
await context.storageState({ path: './e2e/tests/recycled_storage.json' }); await context.storageState({ path: './e2e/tests/recycled_storage.json' });
@ -59,13 +57,14 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
console.log('conditionSetUrl ' + conditionSetUrl); console.log('conditionSetUrl ' + conditionSetUrl);
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0]; getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl); console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
});
test.afterAll(async ({ browser }) => {
await browser.close();
}); });
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//Load localStorage for subsequent tests //Load localStorage for subsequent tests
test.use({ storageState: './e2e/tests/recycled_storage.json' }); test.use({ storageState: './e2e/tests/recycled_storage.json' });
//Begin suite of tests again localStorage //Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector', async ({ page }) => { test('Condition set object properties persist in main view and inspector', async ({ page }) => {
//Navigate to baseURL with injected localStorage //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 // Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property // Verify Search Tree reflects renamed Name property
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page //Reload Page
@ -146,31 +145,35 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Condition Set Object is renamed in Tree // Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property // Verify Search Tree reflects renamed Name property
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); 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 }) => { test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
//Navigate to baseURL //Navigate to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); 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 //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 // Search for Unnamed Condition Set
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set'); await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Click Search Result // Right Click to Open Actions Menu
await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click(); await page.locator('a:has-text("Unnamed Condition Set")').click({
// Click hamburger button button: 'right'
await page.locator('[title="More options"]').click(); });
// Click text=Remove // Click Remove Action
await page.locator('text=Remove').click(); await page.locator('text=Remove').click();
await page.locator('text=OK').click(); await page.locator('text=OK').click();
//Expect Unnamed Condition Set to be removed in Main View //Expect Unnamed Condition Set to be removed in Main View
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1);
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? //Feature?
//Domain Object is still available by direct URL after delete //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.describe('Example Imagery', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
page.on('console', msg => console.log(msg.text()));
//Go to baseURL //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@ -58,21 +59,21 @@ test.describe('Example Imagery', () => {
const backgroundImageSelector = '.c-imagery__main-image__background-image'; const backgroundImageSelector = '.c-imagery__main-image__background-image';
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => { 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 const deltaYStep = 100; //equivalent to 1x zoom
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
// zoom in // zoom in
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
await page.mouse.wheel(0, deltaYStep * 2); await page.mouse.wheel(0, deltaYStep * 2);
// wait for zoom animation to finish // wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
// zoom out // zoom out
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
await page.mouse.wheel(0, -deltaYStep); await page.mouse.wheel(0, -deltaYStep);
// wait for zoom animation to finish // wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox(); const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
@ -86,12 +87,12 @@ test.describe('Example Imagery', () => {
const deltaYStep = 100; //equivalent to 1x zoom const deltaYStep = 100; //equivalent to 1x zoom
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const bgImageLocator = page.locator(backgroundImageSelector); const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
// zoom in // zoom in
await page.mouse.wheel(0, deltaYStep * 2); await page.mouse.wheel(0, deltaYStep * 2);
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const zoomedBoundingBox = await bgImageLocator.boundingBox(); const zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 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 }) => { test('Can use + - buttons to zoom on the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector); const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); const zoomInBtn = await page.locator('.t-btn-zoom-in');
const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0); const zoomOutBtn = await page.locator('.t-btn-zoom-out');
const initialBoundingBox = await bgImageLocator.boundingBox(); const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click(); await zoomInBtn.click();
await zoomInBtn.click(); await zoomInBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const zoomedInBoundingBox = await bgImageLocator.boundingBox(); const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomOutBtn.click(); await zoomOutBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const zoomedOutBoundingBox = await bgImageLocator.boundingBox(); const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); 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 }) => { 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 // 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 zoomInBtn = await page.locator('.t-btn-zoom-in');
const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0); const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
const initialBoundingBox = await bgImageLocator.boundingBox(); const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click(); await zoomInBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
await zoomInBtn.click(); await zoomInBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const zoomedInBoundingBox = await bgImageLocator.boundingBox(); const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
@ -194,7 +195,7 @@ test.describe('Example Imagery', () => {
await zoomResetBtn.click(); await zoomResetBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const resetBoundingBox = await bgImageLocator.boundingBox(); const resetBoundingBox = await bgImageLocator.boundingBox();
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
@ -208,18 +209,18 @@ test.describe('Example Imagery', () => {
const bgImageLocator = page.locator(backgroundImageSelector); const bgImageLocator = page.locator(backgroundImageSelector);
const pausePlayButton = page.locator('.c-button.pause-play'); const pausePlayButton = page.locator('.c-button.pause-play');
// wait for zoom animation to finish // wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
// open the time conductor drop down // 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 // 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/); 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(); await zoomInBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
return expect(pausePlayButton).not.toHaveClass(/is-paused/); 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'); // ('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'; const backgroundImageSelector = '.c-imagery__main-image__background-image';
test('Example Imagery in Display layout', async ({ page }) => { 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 // Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@ -248,11 +244,9 @@ test('Example Imagery in Display layout', async ({ page }) => {
// Click text=Example Imagery // Click text=Example Imagery
await page.click('text=Example Imagery'); await page.click('text=Example Imagery');
// Clear and set Image load delay to minimum value // Clear and set Image load delay (milliseconds)
// FIXME: Update the value to 5000 ms when this bug is fixed. await page.click('input[type="number"]', {clickCount: 3});
// See: https://github.com/nasa/openmct/issues/5265 await page.type('input[type="number"]', "20");
await page.locator('input[type="number"]').fill('');
await page.locator('input[type="number"]').fill('0');
// Click text=OK // Click text=OK
await Promise.all([ await Promise.all([
@ -261,15 +255,14 @@ test('Example Imagery in Display layout', async ({ page }) => {
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
// Wait until Save Banner is gone // Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
const bgImageLocator = page.locator(backgroundImageSelector); const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
// Click previous image button // Click previous image button
const previousImageButton = page.locator('.c-nav--prev'); const previousImageButton = await page.locator('.c-nav--prev');
await previousImageButton.click(); await previousImageButton.click();
// Verify previous image // Verify previous image
@ -278,7 +271,7 @@ test('Example Imagery in Display layout', async ({ page }) => {
// Zoom in // Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const deltaYStep = 100; // equivalent to 1x zoom const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * 2); await page.mouse.wheel(0, deltaYStep * 2);
const zoomedBoundingBox = await bgImageLocator.boundingBox(); const zoomedBoundingBox = await bgImageLocator.boundingBox();
@ -286,7 +279,7 @@ test('Example Imagery in Display layout', async ({ page }) => {
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// Wait for zoom animation to finish // Wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
@ -295,26 +288,27 @@ test('Example Imagery in Display layout', async ({ page }) => {
await page.mouse.move(imageCenterX, imageCenterY); await page.mouse.move(imageCenterX, imageCenterY);
// Pan Imagery Hints // 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 expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
const imageryHintsText = await page.locator('.c-imagery__hints').innerText(); const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
expect(expectedAltText).toEqual(imageryHintsText); expect(expectedAltText).toEqual(imageryHintsText);
// Click next image button // Click next image button
const nextImageButton = page.locator('.c-nav--next'); const nextImageButton = await page.locator('.c-nav--next');
await nextImageButton.click(); await nextImageButton.click();
// Click time conductor mode button // Click fixed timespan button
await page.locator('.c-mode-button').click(); await page.locator('.c-button__label >> text=Fixed Timespan').click();
// Select local clock mode // Click local clock
await page.locator('[data-testid=conductor-modeOption-realtime]').click(); await page.locator('.icon-clock >> text=Local Clock').click();
// Zoom in on next image // Zoom in on next image
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
await page.mouse.wheel(0, deltaYStep * 2); await page.mouse.wheel(0, deltaYStep * 2);
// Wait for zoom animation to finish // Wait for zoom animation to finish
await bgImageLocator.hover({trial: true}); await bgImageLocator.hover();
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
@ -325,133 +319,29 @@ test('Example Imagery in Display layout', async ({ page }) => {
// Verify previous image // Verify previous image
await expect(selectedImage).toBeVisible(); await expect(selectedImage).toBeVisible();
const imageCount = await page.locator('.c-imagery__thumb').count(); // Wait 20ms to verify no new image has come in
await expect.poll(async () => { await page.waitForTimeout(21);
const newImageCount = await page.locator('.c-imagery__thumb').count();
return newImageCount;
}, {
message: "verify that new images still stream in",
timeout: 6 * 1000
}).toBeGreaterThan(imageCount);
// Verify selected image is still displayed
await expect(selectedImage).toBeVisible();
// Unpause imagery
await page.locator('.pause-play').click();
//Get background-image url from background-image css prop //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) => { let backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
}); });
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
console.log('backgroundImageUrl1 ' + backgroundImageUrl1); console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
let backgroundImageUrl2; // sleep 21ms
await expect.poll(async () => { await page.waitForTimeout(21);
// Verify next image has updated // Verify next image has updated
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => { let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
}); });
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre let backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
return backgroundImageUrl2;
}, {
message: "verify next image has updated",
timeout: 6 * 1000
}).not.toBe(backgroundImageUrl1);
console.log('backgroundImageUrl2 ' + backgroundImageUrl2); console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
});
test.describe('Example imagery thumbnails resize in display layouts', () => { // Expect backgroundImageUrl2 to be greater then backgroundImageUrl1
expect(backgroundImageUrl2 >= backgroundImageUrl1);
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Display Layout")
await page.locator('li:has-text("Display Layout")').click();
const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]');
await displayLayoutTitleField.click();
await displayLayoutTitleField.fill('Thumbnail Display Layout');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click text=Save and Finish Editing
await page.locator('text=Save and Finish Editing').click();
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Example Imagery")
await page.locator('li:has-text("Example Imagery")').click();
const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]');
// Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
await imageryTitleField.click();
// Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
await imageryTitleField.fill('Thumbnail Example Imagery');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0
await Promise.all([
page.waitForNavigation(),
page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click()
]);
// Edit mode
await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click();
// Click on example imagery to expose toolbar
await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click();
// expect thumbnails not be visible when first added
expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
// Resize the example imagery vertically to change the thumbnail visibility
/*
The following arbitrary values are added to observe the separate visual
conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
Specifically, height is set to 50px for small thumbs and 100px for regular
*/
// Click #mct-input-id-103
await page.locator('#mct-input-id-103').click();
// Fill #mct-input-id-103
await page.locator('#mct-input-id-103').fill('50');
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
// Resize the example imagery vertically to change the thumbnail visibility
// Click #mct-input-id-103
await page.locator('#mct-input-id-103').click();
// Fill #mct-input-id-103
await page.locator('#mct-input-id-103').fill('100');
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
});
}); });
test.describe('Example Imagery in Flexible layout', () => { test.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 testLogTicks(page);
//await testLogPlotPixels(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 // refresh page and wait for charts and ticks to load
await page.waitForTimeout(1 * 1000);
await page.reload({ waitUntil: 'networkidle'}); await page.reload({ waitUntil: 'networkidle'});
await page.waitForSelector('.gl-plot-chart-area'); await page.waitForSelector('.gl-plot-chart-area');
await page.waitForSelector('.gl-plot-y-tick-label'); await page.waitForSelector('.gl-plot-y-tick-label');
@ -60,9 +57,7 @@ test.describe('Log plot tests', () => {
//await testLogPlotPixels(page); //await testLogPlotPixels(page);
}); });
// Leaving test as 'TODO' for now. test.skip('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
// NOTE: Not eligible for community contributions.
test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
await makeOverlayPlot(page); await makeOverlayPlot(page);
await enableEditMode(page); await enableEditMode(page);
await enableLogMode(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,7 +23,7 @@
const { test } = require('../../../fixtures.js'); const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test'); 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 }) => { test('validate start time does not exceeds end time', async ({ page }) => {
//Go to baseURL //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@ -73,163 +73,37 @@ test.describe('Time conductor operations', () => {
// Try to change the realtime offsets when in realtime (local clock) mode. // Try to change the realtime offsets when in realtime (local clock) mode.
test.describe('Time conductor input fields real-time mode', () => { test.describe('Time conductor input fields real-time mode', () => {
test('validate input fields in real-time mode', async ({ page }) => { 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' }); await page.goto('/', { waitUntil: 'networkidle' });
// Switch to real-time mode // Click fixed timespan button
await setRealTimeMode(page); await page.locator('.c-button__label >> text=Fixed Timespan').click();
// Set start time offset // Click local clock
await setStartOffset(page, startOffset); await page.locator('.icon-clock >> text=Local Clock').click();
// Verify time was updated on time offset button // Click time offset button
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23'); await page.locator('.c-conductor__delta-button >> text=00:30:00').click();
// Set end time offset // Input start time offset
await setEndOffset(page, endOffset); await page.fill('.pr-time-controls__secs', '23');
// Verify time was updated on preceding time offset button
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
});
/**
* Verify that offsets and url params are preserved when switching
* between fixed timespan and real-time mode.
*/
test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
const startOffset = {
mins: '30',
secs: '23'
};
const endOffset = {
secs: '01'
};
// Convert offsets to milliseconds
const startDelta = (30 * 60 * 1000) + (23 * 1000);
const endDelta = (1 * 1000);
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Switch to real-time mode
await setRealTimeMode(page);
// Set start time offset
await setStartOffset(page, startOffset);
// Set end time offset
await setEndOffset(page, endOffset);
// Switch to fixed timespan mode
await setFixedTimeMode(page);
// Switch back to real-time mode
await setRealTimeMode(page);
// Verify updated start time offset persists after mode switch
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
// Verify updated end time offset persists after mode switch
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
// Verify url parameters persist after mode switch
await page.waitForNavigation();
expect(page.url()).toContain(`startDelta=${startDelta}`);
expect(page.url()).toContain(`endDelta=${endDelta}`);
});
});
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button // Click the check button
await page.locator('.icon-check').click(); await page.locator('.icon-check').click();
}
/** // Verify time was updated on time offset button
* Set the time conductor mode to either fixed timespan or realtime mode. await expect(page.locator('.c-conductor__delta-button').first()).toContainText('00:30:23');
* @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 // Click time offset set preceding now button
if (isFixedTimespan) { await page.locator('.c-conductor__delta-button >> text=00:00:30').click();
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else { // Input preceding time offset
await page.locator('data-testid=conductor-modeOption-realtime').click(); await page.fill('.pr-time-controls__secs', '31');
}
} // Click the check buttons
await page.locator('.icon-check').click();
// Verify time was updated on preceding time offset button
await expect(page.locator('.c-conductor__delta-button').nth(1)).toContainText('00:00:31');
});
});

View File

@ -6,11 +6,11 @@
"localStorage": [ "localStorage": [
{ {
"name": "tcHistory", "name": "tcHistory",
"value": "{\"utc\":[{\"start\":1652301954635,\"end\":1652303754635}]}" "value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
}, },
{ {
"name": "mct", "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", "name": "mct-tree-expanded",

View File

@ -47,10 +47,7 @@ test.beforeEach(async ({ context }) => {
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
}); });
await context.addInitScript(() => { await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers({ window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
now: 0,
shouldAdvanceTime: true
}); //Set browser clock to UNIX Epoch
}); });
}); });
@ -59,7 +56,8 @@ test('Visual - Root and About', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
// Verify that Create button is actionable // 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 // Take a snapshot of the Dashboard
await page.waitForTimeout(VISUAL_GRACE_PERIOD); 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 page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'removed amplitude property value'); 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,7 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import { v4 as uuid } from 'uuid'; import uuid from 'uuid';
import createExampleUser from './exampleUserCreator'; import createExampleUser from './exampleUserCreator';
const STATUSES = [{ const STATUSES = [{

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

View File

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

View File

@ -77,8 +77,7 @@
utc: nextStep, utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000, yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
wavelengths: wavelengths(), wavelength: wavelength(start, nextStep),
intensities: intensities(),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness) cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
} }
}); });
@ -127,8 +126,7 @@
utc: nextStep, utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000, yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness), sin: sin(nextStep, period, amplitude, offset, phase, randomness),
wavelengths: wavelengths(), wavelength: wavelength(start, nextStep),
intensities: intensities(),
cos: cos(nextStep, period, amplitude, offset, phase, randomness) 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; * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
} }
function wavelengths() { function wavelength(start, nextStep) {
let values = []; return (nextStep - start) / 10;
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 sendError(error, message) { function sendError(error, message) {

View File

@ -59,8 +59,7 @@ export default function () {
object.configuration = { object.configuration = {
imageLocation: '', imageLocation: '',
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS, imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
imageSamples: [], imageSamples: []
layers: []
}; };
object.telemetry = { object.telemetry = {
@ -91,21 +90,7 @@ export default function () {
format: 'image', format: 'image',
hints: { hints: {
image: 1 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', name: 'Image Download Name',

View File

@ -75,12 +75,12 @@
const TWO_HOURS = ONE_HOUR * 2; const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24; const ONE_DAY = ONE_HOUR * 24;
openmct.install(openmct.plugins.LocalStorage()); openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.example.Generator()); openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin()); openmct.install(openmct.plugins.example.EventGeneratorPlugin());
openmct.install(openmct.plugins.example.ExampleImagery()); openmct.install(openmct.plugins.example.ExampleImagery());
openmct.install(openmct.plugins.example.ExampleTags());
openmct.install(openmct.plugins.Espresso()); openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems()); openmct.install(openmct.plugins.MyItems());
@ -196,8 +196,6 @@
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true })); openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer()); openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist()); openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
openmct.start(); openmct.start();
</script> </script>
</html> </html>

View File

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

View File

@ -42,7 +42,6 @@ define([
'./plugins/duplicate/plugin', './plugins/duplicate/plugin',
'./plugins/importFromJSONAction/plugin', './plugins/importFromJSONAction/plugin',
'./plugins/exportAsJSONAction/plugin', './plugins/exportAsJSONAction/plugin',
'./ui/components/components',
'vue' 'vue'
], function ( ], function (
EventEmitter, EventEmitter,
@ -66,7 +65,6 @@ define([
DuplicateActionPlugin, DuplicateActionPlugin,
ImportFromJSONAction, ImportFromJSONAction,
ExportAsJSONAction, ExportAsJSONAction,
components,
Vue Vue
) { ) {
/** /**
@ -238,22 +236,14 @@ define([
this.priority = api.PriorityAPI; this.priority = api.PriorityAPI;
this.router = new ApplicationRouter(this); this.router = new ApplicationRouter(this);
this.faults = new api.FaultManagementAPI.default(this);
this.forms = new api.FormsAPI.default(this); this.forms = new api.FormsAPI.default(this);
this.branding = BrandingAPI.default; 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 // Plugins that are installed by default
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
this.install(this.plugins.ScatterPlot());
this.install(this.plugins.BarChart());
this.install(this.plugins.TelemetryTable.default()); this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default()); this.install(PreviewPlugin.default());
this.install(LicensesPlugin.default()); this.install(LicensesPlugin.default());
@ -389,7 +379,6 @@ define([
}; };
MCT.prototype.plugins = plugins; MCT.prototype.plugins = plugins;
MCT.prototype.components = components.default;
return MCT; 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', './actions/ActionsAPI',
'./composition/CompositionAPI', './composition/CompositionAPI',
'./Editor', './Editor',
'./faultmanagement/FaultManagementAPI',
'./forms/FormsAPI', './forms/FormsAPI',
'./indicators/IndicatorAPI', './indicators/IndicatorAPI',
'./menu/MenuAPI', './menu/MenuAPI',
@ -35,13 +34,11 @@ define([
'./telemetry/TelemetryAPI', './telemetry/TelemetryAPI',
'./time/TimeAPI', './time/TimeAPI',
'./types/TypeRegistry', './types/TypeRegistry',
'./user/UserAPI', './user/UserAPI'
'./annotation/AnnotationAPI'
], function ( ], function (
ActionsAPI, ActionsAPI,
CompositionAPI, CompositionAPI,
EditorAPI, EditorAPI,
FaultManagementAPI,
FormsAPI, FormsAPI,
IndicatorAPI, IndicatorAPI,
MenuAPI, MenuAPI,
@ -52,14 +49,12 @@ define([
TelemetryAPI, TelemetryAPI,
TimeAPI, TimeAPI,
TypeRegistry, TypeRegistry,
UserAPI, UserAPI
AnnotationAPI
) { ) {
return { return {
ActionsAPI: ActionsAPI.default, ActionsAPI: ActionsAPI.default,
CompositionAPI: CompositionAPI, CompositionAPI: CompositionAPI,
EditorAPI: EditorAPI, EditorAPI: EditorAPI,
FaultManagementAPI: FaultManagementAPI,
FormsAPI: FormsAPI, FormsAPI: FormsAPI,
IndicatorAPI: IndicatorAPI.default, IndicatorAPI: IndicatorAPI.default,
MenuAPI: MenuAPI.default, MenuAPI: MenuAPI.default,
@ -70,7 +65,6 @@ define([
TelemetryAPI: TelemetryAPI, TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default, TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry, TypeRegistry: TypeRegistry,
UserAPI: UserAPI.default, UserAPI: UserAPI.default
AnnotationAPI: AnnotationAPI.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,15 +44,19 @@
> >
{{ section.name }} {{ section.name }}
</h2> </h2>
<FormRow <div
v-for="(row, index) in section.rows" v-for="(row, index) in section.rows"
:key="row.id" :key="row.id"
:css-class="row.cssClass" class="u-contents"
>
<FormRow
:css-class="section.cssClass"
:first="index < 1" :first="index < 1"
:row="row" :row="row"
@onChange="onChange" @onChange="onChange"
/> />
</div> </div>
</div>
</form> </form>
<div class="mct-form__controls c-overlay__button-bar c-form__bottom-bar"> <div class="mct-form__controls c-overlay__button-bar c-form__bottom-bar">
@ -77,7 +81,7 @@
<script> <script>
import FormRow from "@/api/forms/components/FormRow.vue"; import FormRow from "@/api/forms/components/FormRow.vue";
import { v4 as uuid } from 'uuid'; import uuid from 'uuid';
export default { export default {
components: { components: {

View File

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

View File

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

View File

@ -22,7 +22,6 @@
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import indicatorTemplate from './res/indicator-template.html'; import indicatorTemplate from './res/indicator-template.html';
import { convertTemplateToHTML } from '@/utils/template/templateHelpers';
const DEFAULT_ICON_CLASS = 'icon-info'; const DEFAULT_ICON_CLASS = 'icon-info';
@ -31,7 +30,7 @@ class SimpleIndicator extends EventEmitter {
super(); super();
this.openmct = openmct; this.openmct = openmct;
this.element = convertTemplateToHTML(indicatorTemplate)[0]; this.element = compileTemplate(indicatorTemplate)[0];
this.priority = openmct.priority.DEFAULT; this.priority = openmct.priority.DEFAULT;
this.textElement = this.element.querySelector('.js-indicator-text'); this.textElement = this.element.querySelector('.js-indicator-text');
@ -117,4 +116,11 @@ class SimpleIndicator extends EventEmitter {
} }
} }
function compileTemplate(htmlTemplate) {
const templateNode = document.createElement('template');
templateNode.innerHTML = htmlTemplate;
return templateNode.content.cloneNode(true).children;
}
export default SimpleIndicator; export default SimpleIndicator;

View File

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

View File

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

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { v4 as uuid } from 'uuid'; import uuid from 'uuid';
class InMemorySearchProvider { class InMemorySearchProvider {
/** /**
@ -39,10 +39,11 @@ class InMemorySearchProvider {
* If max results is not specified in query, use this as default. * If max results is not specified in query, use this as default.
*/ */
this.DEFAULT_MAX_RESULTS = 100; this.DEFAULT_MAX_RESULTS = 100;
this.openmct = openmct; this.openmct = openmct;
this.indexedIds = {}; this.indexedIds = {};
this.indexedCompositions = {}; this.indexedCompositions = {};
this.indexedTags = {};
this.idsToIndex = []; this.idsToIndex = [];
this.pendingIndex = {}; this.pendingIndex = {};
this.pendingRequests = 0; this.pendingRequests = 0;
@ -51,18 +52,11 @@ class InMemorySearchProvider {
/** /**
* If we don't have SharedWorkers available (e.g., iOS) * If we don't have SharedWorkers available (e.g., iOS)
*/ */
this.localIndexedDomainObjects = {}; this.localIndexedItems = {};
this.localIndexedAnnotationsByDomainObject = {};
this.localIndexedAnnotationsByTag = {};
this.pendingQueries = {}; this.pendingQueries = {};
this.onWorkerMessage = this.onWorkerMessage.bind(this); this.onWorkerMessage = this.onWorkerMessage.bind(this);
this.onWorkerMessageError = this.onWorkerMessageError.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.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this); this.startIndexing = this.startIndexing.bind(this);
@ -82,39 +76,13 @@ class InMemorySearchProvider {
startIndexing() { startIndexing() {
const rootObject = this.openmct.objects.rootProvider.rootObject; 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.scheduleForIndexing(rootObject.identifier);
this.indexAnnotations();
if (typeof SharedWorker !== 'undefined') { if (typeof SharedWorker !== 'undefined') {
this.worker = this.startSharedWorker(); this.worker = this.startSharedWorker();
} else { } else {
// we must be on iOS // 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; 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 queryId = uuid();
const pendingQuery = this.getIntermediateResponse(); const pendingQuery = this.getIntermediateResponse();
this.pendingQueries[queryId] = pendingQuery; this.pendingQueries[queryId] = pendingQuery;
const searchOptions = {
queryId,
searchType,
query,
maxResults: this.DEFAULT_MAX_RESULTS
};
if (this.worker) { if (this.worker) {
this.#dispatchSearchToWorker(searchOptions); this.dispatchSearch(queryId, input, maxResults);
} else { } else {
this.#localQueryFallBack(searchOptions); this.localSearch(queryId, input, maxResults);
} }
return pendingQuery.promise; 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 * @private
*/ */
async onWorkerMessage(event) { async onWorkerMessage(event) {
if (event.data.request !== 'search') {
return;
}
const pendingQuery = this.pendingQueries[event.data.queryId]; const pendingQuery = this.pendingQueries[event.data.queryId];
const modelResults = { const modelResults = {
total: event.data.total total: event.data.total
}; };
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => { modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
if (hit && hit.keyString) {
const identifier = this.openmct.objects.parseKeyString(hit.keyString); const identifier = this.openmct.objects.parseKeyString(hit.keyString);
const domainObject = await this.openmct.objects.get(identifier); const domainObject = await this.openmct.objects.get(identifier);
return domainObject; return domainObject;
}
})); }));
pendingQuery.resolve(modelResults); pendingQuery.resolve(modelResults);
@ -257,11 +216,6 @@ class InMemorySearchProvider {
} }
} }
onAnnotationCreation(annotationObject) {
const provider = this;
provider.index(annotationObject);
}
onNameMutation(domainObject, name) { onNameMutation(domainObject, name) {
const provider = this; const provider = this;
@ -269,14 +223,6 @@ class InMemorySearchProvider {
provider.index(domainObject); provider.index(domainObject);
} }
onTagMutation(domainObject, newTags) {
domainObject.oldTags = domainObject.tags;
domainObject.tags = newTags;
const provider = this;
provider.index(domainObject);
}
onCompositionMutation(domainObject, composition) { onCompositionMutation(domainObject, composition) {
const provider = this; const provider = this;
const indexedComposition = domainObject.composition; const indexedComposition = domainObject.composition;
@ -313,13 +259,6 @@ class InMemorySearchProvider {
'composition', 'composition',
this.onCompositionMutation.bind(this, domainObject) 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')) { if ((keyString !== 'ROOT')) {
@ -378,87 +317,26 @@ class InMemorySearchProvider {
* @private * @private
* @returns {String} a unique query Id for the query. * @returns {String} a unique query Id for the query.
*/ */
#dispatchSearchToWorker({queryId, searchType, query, maxResults}) { dispatchSearch(queryId, searchInput, maxResults) {
const message = { const message = {
request: searchType.toString(), request: 'search',
input: query, input: searchInput,
maxResults, maxResults,
queryId queryId
}; };
this.worker.port.postMessage(message); 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 * A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS) * if we don't have SharedWorkers available (e.g., iOS)
*/ */
localIndexItem(keyString, model) { localIndexItem(keyString, model) {
const objectToIndex = { this.localIndexedItems[keyString] = {
type: model.type, type: model.type,
name: model.name, name: model.name,
keyString 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 * Gets search results from the indexedItems based on provided search
* input. Returns matching results from indexedItems * input. Returns matching results from indexedItems
*/ */
localSearchForObjects(queryId, searchInput, maxResults) { localSearch(queryId, searchInput, maxResults) {
// This results dictionary will have domain object ID keys which // This results dictionary will have domain object ID keys which
// point to the value the domain object's score. // point to the value the domain object's score.
let results = []; let results;
const input = searchInput.trim().toLowerCase(); const input = searchInput.trim().toLowerCase();
const message = { const message = {
request: 'searchForObjects', request: 'search',
results: [], results: {},
total: 0, total: 0,
queryId queryId
}; };
results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => { results = Object.values(this.localIndexedItems).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input); 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.total = results.length;
message.results = results message.results = results

View File

@ -26,27 +26,16 @@
(function () { (function () {
// An object composed of domain object IDs and models // An object composed of domain object IDs and models
// {id: domainObject's ID, name: domainObject's name} // {id: domainObject's ID, name: domainObject's name}
const indexedDomainObjects = {}; const indexedItems = {};
const indexedAnnotationsByDomainObject = {};
const indexedAnnotationsByTag = {};
self.onconnect = function (e) { self.onconnect = function (e) {
const port = e.ports[0]; const port = e.ports[0];
port.onmessage = function (event) { port.onmessage = function (event) {
const requestType = event.data.request; if (event.data.request === 'index') {
if (requestType === 'index') {
indexItem(event.data.keyString, event.data.model); indexItem(event.data.keyString, event.data.model);
} else if (requestType === 'OBJECTS') { } else if (event.data.request === 'search') {
port.postMessage(searchForObjects(event.data)); port.postMessage(search(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}`);
} }
}; };
@ -59,73 +48,12 @@
console.error('Error on feed', error); 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) { function indexItem(keyString, model) {
const objectToIndex = { indexedItems[keyString] = {
type: model.type, type: model.type,
name: model.name, name: model.name,
keyString 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 * * maxResults: The maximum number of search results desired
* * queryId: an id identifying this query, will be returned. * * queryId: an id identifying this query, will be returned.
*/ */
function searchForObjects(data) { function search(data) {
let results = []; // 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 input = data.input.trim().toLowerCase();
const message = { const message = {
request: 'searchForObjects', request: 'search',
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',
results: {}, results: {},
total: 0, total: 0,
queryId: data.queryId queryId: data.queryId
}; };
const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString]; results = Object.values(indexedItems).filter((indexedItem) => {
if (matchingAnnotations) { return indexedItem.name.toLowerCase().includes(input);
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));
}); });
}
message.total = results.length; message.total = results.length;
message.results = results message.results = results

View File

@ -30,55 +30,15 @@ import Transaction from './Transaction';
import ConflictError from './ConflictError'; import ConflictError from './ConflictError';
import InMemorySearchProvider from './InMemorySearchProvider'; import InMemorySearchProvider from './InMemorySearchProvider';
/**
* Uniquely identifies a domain object.
*
* @typedef Identifier
* @memberof module:openmct.ObjectAPI~
* @property {string} namespace the namespace to/from which this domain
* object should be loaded/stored.
* @property {string} key a unique identifier for the domain object
* within that namespace
*/
/**
* A domain object is an entity of relevance to a user's workflow, that
* should appear as a distinct and meaningful object within the user
* interface. Examples of domain objects are folders, telemetry sensors,
* and so forth.
*
* A few common properties are defined for domain objects. Beyond these,
* individual types of domain objects may add more as they see fit.
*
* @typedef DomainObject
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
* uniquely identifies this domain object
* @property {string} type the type of domain object
* @property {string} name the human-readable name for this domain object
* @property {string} [creator] the user name of the creator of this domain
* object
* @property {number} [modified] the time, in milliseconds since the UNIX
* epoch, at which this domain object was last modified
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
* present, this will be used by the default composition provider
* to load domain objects
* @memberof module:openmct
*/
/** /**
* Utilities for loading, saving, and manipulating domain objects. * Utilities for loading, saving, and manipulating domain objects.
* @interface ObjectAPI * @interface ObjectAPI
* @memberof module:openmct * @memberof module:openmct
*/ */
export default class ObjectAPI {
constructor(typeRegistry, openmct) { function ObjectAPI(typeRegistry, openmct) {
this.openmct = openmct; this.openmct = openmct;
this.typeRegistry = typeRegistry; this.typeRegistry = typeRegistry;
this.SEARCH_TYPES = Object.freeze({
OBJECTS: 'OBJECTS',
ANNOTATIONS: 'ANNOTATIONS',
NOTEBOOK_ANNOTATIONS: 'NOTEBOOK_ANNOTATIONS',
TAGS: 'TAGS'
});
this.eventEmitter = new EventEmitter(); this.eventEmitter = new EventEmitter();
this.providers = {}; this.providers = {};
this.rootRegistry = new RootRegistry(openmct); this.rootRegistry = new RootRegistry(openmct);
@ -96,31 +56,41 @@ export default class ObjectAPI {
} }
/** /**
* Retrieve the provider for a given identifier. * Set fallback provider, this is an internal API for legacy reasons.
* @private
*/ */
getProvider(identifier) { ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
this.fallbackProvider = p;
};
/**
* Retrieve the provider for a given identifier.
* @private
*/
ObjectAPI.prototype.getProvider = function (identifier) {
if (identifier.key === 'ROOT') { if (identifier.key === 'ROOT') {
return this.rootProvider; return this.rootProvider;
} }
return this.providers[identifier.namespace] || this.fallbackProvider; return this.providers[identifier.namespace] || this.fallbackProvider;
} };
/** /**
* Get an active transaction instance * Get an active transaction instance
* @returns {Transaction} a transaction object * @returns {Transaction} a transaction object
*/ */
getActiveTransaction() { ObjectAPI.prototype.getActiveTransaction = function () {
return this.transaction; return this.transaction;
} };
/** /**
* Get the root-level object. * Get the root-level object.
* @returns {Promise.<DomainObject>} a promise for the root object * @returns {Promise.<DomainObject>} a promise for the root object
*/ */
getRoot() { ObjectAPI.prototype.getRoot = function () {
return this.rootProvider.get(); return this.rootProvider.get();
} };
/** /**
* Register a new object provider for a particular namespace. * Register a new object provider for a particular namespace.
@ -131,9 +101,9 @@ export default class ObjectAPI {
* @memberof {module:openmct.ObjectAPI#} * @memberof {module:openmct.ObjectAPI#}
* @name addProvider * @name addProvider
*/ */
addProvider(namespace, provider) { ObjectAPI.prototype.addProvider = function (namespace, provider) {
this.providers[namespace] = provider; this.providers[namespace] = provider;
} };
/** /**
* Provides the ability to read, write, and delete domain objects. * Provides the ability to read, write, and delete domain objects.
@ -189,7 +159,7 @@ export default class ObjectAPI {
* has been saved, or be rejected if it cannot be saved * has been saved, or be rejected if it cannot be saved
*/ */
get(identifier, abortSignal) { ObjectAPI.prototype.get = function (identifier, abortSignal) {
let keystring = this.makeKeyString(identifier); let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) { if (this.cache[keystring] !== undefined) {
@ -241,7 +211,7 @@ export default class ObjectAPI {
this.cache[keystring] = objectPromise; this.cache[keystring] = objectPromise;
return objectPromise; return objectPromise;
} };
/** /**
* Search for domain objects. * Search for domain objects.
@ -255,33 +225,23 @@ export default class ObjectAPI {
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
* @param {string} query the term to search for * @param {string} query the term to search for
* @param {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests * @param {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests
* @param {string} searchType the type of search as defined by SEARCH_TYPES
* @returns {Array.<Promise.<module:openmct.DomainObject>>} * @returns {Array.<Promise.<module:openmct.DomainObject>>}
* an array of promises returned from each object provider's search function * an array of promises returned from each object provider's search function
* each resolving to domain objects matching provided search query and options. * each resolving to domain objects matching provided search query and options.
*/ */
search(query, abortSignal, searchType = this.SEARCH_TYPES.OBJECTS) { ObjectAPI.prototype.search = function (query, abortSignal) {
if (!Object.keys(this.SEARCH_TYPES).includes(searchType.toUpperCase())) {
throw new Error(`Unknown search type: ${searchType}`);
}
const searchPromises = Object.values(this.providers) const searchPromises = Object.values(this.providers)
.filter(provider => { .filter(provider => provider.search !== undefined)
return ((provider.supportsSearchType !== undefined) && provider.supportsSearchType(searchType)); .map(provider => provider.search(query, abortSignal));
}) // abortSignal doesn't seem to be used in generic search?
.map(provider => provider.search(query, abortSignal, searchType)); searchPromises.push(this.inMemorySearchProvider.query(query, null)
if (!this.inMemorySearchProvider.supportsSearchType(searchType)) {
throw new Error(`${searchType} not implemented in inMemorySearchProvider`);
}
searchPromises.push(this.inMemorySearchProvider.search(query, searchType)
.then(results => results.hits .then(results => results.hits
.map(hit => { .map(hit => {
return hit; return hit;
}))); })));
return searchPromises; return searchPromises;
} };
/** /**
* Will fetch object for the given identifier, returning a version of the object that will automatically keep * Will fetch object for the given identifier, returning a version of the object that will automatically keep
@ -294,7 +254,7 @@ export default class ObjectAPI {
* @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if * @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if
* the object can be mutated. * the object can be mutated.
*/ */
getMutable(identifier) { ObjectAPI.prototype.getMutable = function (identifier) {
if (!this.supportsMutation(identifier)) { if (!this.supportsMutation(identifier)) {
throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`); throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
} }
@ -302,7 +262,7 @@ export default class ObjectAPI {
return this.get(identifier).then((object) => { return this.get(identifier).then((object) => {
return this._toMutable(object); return this._toMutable(object);
}); });
} };
/** /**
* This function is for cleaning up a mutable domain object when you're done with it. * This function is for cleaning up a mutable domain object when you're done with it.
@ -310,44 +270,45 @@ export default class ObjectAPI {
* platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle. * platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle.
* @param {MutableDomainObject} domainObject * @param {MutableDomainObject} domainObject
*/ */
destroyMutable(domainObject) { ObjectAPI.prototype.destroyMutable = function (domainObject) {
if (domainObject.isMutable) { if (domainObject.isMutable) {
return domainObject.$destroy(); return domainObject.$destroy();
} else { } else {
throw new Error("Attempted to destroy non-mutable domain object"); throw new Error("Attempted to destroy non-mutable domain object");
} }
} };
delete() { ObjectAPI.prototype.delete = function () {
throw new Error('Delete not implemented'); throw new Error('Delete not implemented');
} };
isPersistable(idOrKeyString) { ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
let identifier = utils.parseKeyString(idOrKeyString); let identifier = utils.parseKeyString(idOrKeyString);
let provider = this.getProvider(identifier); let provider = this.getProvider(identifier);
return provider !== undefined return provider !== undefined
&& provider.create !== undefined && provider.create !== undefined
&& provider.update !== undefined; && provider.update !== undefined;
} };
isMissing(domainObject) { ObjectAPI.prototype.isMissing = function (domainObject) {
let identifier = utils.makeKeyString(domainObject.identifier); let identifier = utils.makeKeyString(domainObject.identifier);
let missingName = 'Missing: ' + identifier; let missingName = 'Missing: ' + identifier;
return domainObject.name === missingName; return domainObject.name === missingName;
} };
/** /**
* Save this domain object in its current state. * Save this domain object in its current state. EXPERIMENTAL
* *
* @private
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
* @param {module:openmct.DomainObject} domainObject the domain object to * @param {module:openmct.DomainObject} domainObject the domain object to
* save * save
* @returns {Promise} a promise which will resolve when the domain object * @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved * has been saved, or be rejected if it cannot be saved
*/ */
save(domainObject) { ObjectAPI.prototype.save = function (domainObject) {
let provider = this.getProvider(domainObject.identifier); let provider = this.getProvider(domainObject.identifier);
let savedResolve; let savedResolve;
let savedReject; let savedReject;
@ -355,7 +316,7 @@ export default class ObjectAPI {
if (!this.isPersistable(domainObject.identifier)) { if (!this.isPersistable(domainObject.identifier)) {
result = Promise.reject('Object provider does not support saving'); result = Promise.reject('Object provider does not support saving');
} else if (this.#hasAlreadyBeenPersisted(domainObject)) { } else if (hasAlreadyBeenPersisted(domainObject)) {
result = Promise.resolve(true); result = Promise.resolve(true);
} else { } else {
const persistedTime = Date.now(); const persistedTime = Date.now();
@ -384,25 +345,25 @@ export default class ObjectAPI {
} }
return result; return result;
} };
/** /**
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects * After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
*/ */
startTransaction() { ObjectAPI.prototype.startTransaction = function () {
if (this.isTransactionActive()) { if (this.isTransactionActive()) {
throw new Error("Unable to start new Transaction: Previous Transaction is active"); throw new Error("Unable to start new Transaction: Previous Transaction is active");
} }
this.transaction = new Transaction(this); this.transaction = new Transaction(this);
} };
/** /**
* Clear instance of Transaction * Clear instance of Transaction
*/ */
endTransaction() { ObjectAPI.prototype.endTransaction = function () {
this.transaction = null; this.transaction = null;
} };
/** /**
* Add a root-level object. * Add a root-level object.
@ -415,9 +376,9 @@ export default class ObjectAPI {
* @method addRoot * @method addRoot
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
*/ */
addRoot(identifier, priority) { ObjectAPI.prototype.addRoot = function (identifier, priority) {
this.rootRegistry.addRoot(identifier, priority); this.rootRegistry.addRoot(identifier, priority);
} };
/** /**
* Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get * Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
@ -428,30 +389,30 @@ export default class ObjectAPI {
* @method addGetInterceptor * @method addGetInterceptor
* @memberof module:openmct.InterceptorRegistry# * @memberof module:openmct.InterceptorRegistry#
*/ */
addGetInterceptor(interceptorDef) { ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
this.interceptorRegistry.addInterceptor(interceptorDef); this.interceptorRegistry.addInterceptor(interceptorDef);
} };
/** /**
* Retrieve the interceptors for a given domain object. * Retrieve the interceptors for a given domain object.
* @private * @private
*/ */
#listGetInterceptors(identifier, object) { ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
return this.interceptorRegistry.getInterceptors(identifier, object); return this.interceptorRegistry.getInterceptors(identifier, object);
} };
/** /**
* Inovke interceptors if applicable for a given domain object. * Inovke interceptors if applicable for a given domain object.
* @private * @private
*/ */
applyGetInterceptors(identifier, domainObject) { ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
const interceptors = this.#listGetInterceptors(identifier, domainObject); const interceptors = this.listGetInterceptors(identifier, domainObject);
interceptors.forEach(interceptor => { interceptors.forEach(interceptor => {
domainObject = interceptor.invoke(identifier, domainObject); domainObject = interceptor.invoke(identifier, domainObject);
}); });
return domainObject; return domainObject;
} };
/** /**
* Return relative url path from a given object path * Return relative url path from a given object path
@ -459,12 +420,13 @@ export default class ObjectAPI {
* @param {Array} objectPath * @param {Array} objectPath
* @returns {string} relative url for object * @returns {string} relative url for object
*/ */
getRelativePath(objectPath) { ObjectAPI.prototype.getRelativePath = function (objectPath) {
return objectPath return objectPath
.map(p => this.makeKeyString(p.identifier)) .map(p => this.makeKeyString(p.identifier))
.reverse() .reverse()
.join('/'); .join('/')
} ;
};
/** /**
* Modify a domain object. * Modify a domain object.
@ -474,7 +436,7 @@ export default class ObjectAPI {
* @method mutate * @method mutate
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
*/ */
mutate(domainObject, path, value) { ObjectAPI.prototype.mutate = function (domainObject, path, value) {
if (!this.supportsMutation(domainObject.identifier)) { if (!this.supportsMutation(domainObject.identifier)) {
throw `Error: Attempted to mutate immutable object ${domainObject.name}`; throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
} }
@ -501,12 +463,12 @@ export default class ObjectAPI {
} else { } else {
this.save(domainObject); this.save(domainObject);
} }
} };
/** /**
* @private * @private
*/ */
_toMutable(object) { ObjectAPI.prototype._toMutable = function (object) {
let mutableObject; let mutableObject;
if (object.isMutable) { if (object.isMutable) {
@ -536,14 +498,14 @@ export default class ObjectAPI {
} }
return mutableObject; return mutableObject;
} };
/** /**
* Updates a domain object based on its latest persisted state. Note that this will mutate the provided object. * Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
* @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store * @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
* @returns {Promise} the provided object, updated to reflect the latest persisted state of the object. * @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
*/ */
async refresh(domainObject) { ObjectAPI.prototype.refresh = async function (domainObject) {
const refreshedObject = await this.get(domainObject.identifier); const refreshedObject = await this.get(domainObject.identifier);
if (domainObject.isMutable) { if (domainObject.isMutable) {
@ -553,15 +515,15 @@ export default class ObjectAPI {
} }
return domainObject; return domainObject;
} };
/** /**
* @param module:openmct.ObjectAPI~Identifier identifier An object identifier * @param module:openmct.ObjectAPI~Identifier identifier An object identifier
* @returns {boolean} true if the object can be mutated, otherwise returns false * @returns {boolean} true if the object can be mutated, otherwise returns false
*/ */
supportsMutation(identifier) { ObjectAPI.prototype.supportsMutation = function (identifier) {
return this.isPersistable(identifier); return this.isPersistable(identifier);
} };
/** /**
* Observe changes to a domain object. * Observe changes to a domain object.
@ -572,7 +534,7 @@ export default class ObjectAPI {
* @method observe * @method observe
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
*/ */
observe(domainObject, path, callback) { ObjectAPI.prototype.observe = function (domainObject, path, callback) {
if (domainObject.isMutable) { if (domainObject.isMutable) {
return domainObject.$observe(path, callback); return domainObject.$observe(path, callback);
} else { } else {
@ -581,38 +543,38 @@ export default class ObjectAPI {
return () => mutable.$destroy(); return () => mutable.$destroy();
} }
} };
/** /**
* @param {module:openmct.ObjectAPI~Identifier} identifier * @param {module:openmct.ObjectAPI~Identifier} identifier
* @returns {string} A string representation of the given identifier, including namespace and key * @returns {string} A string representation of the given identifier, including namespace and key
*/ */
makeKeyString(identifier) { ObjectAPI.prototype.makeKeyString = function (identifier) {
return utils.makeKeyString(identifier); return utils.makeKeyString(identifier);
} };
/** /**
* @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon. * @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon.
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object * @returns {module:openmct.ObjectAPI~Identifier} An identifier object
*/ */
parseKeyString(keyString) { ObjectAPI.prototype.parseKeyString = function (keyString) {
return utils.parseKeyString(keyString); return utils.parseKeyString(keyString);
} };
/** /**
* Given any number of identifiers, will return true if they are all equal, otherwise false. * Given any number of identifiers, will return true if they are all equal, otherwise false.
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers * @param {module:openmct.ObjectAPI~Identifier[]} identifiers
*/ */
areIdsEqual(...identifiers) { ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
return identifiers.map(utils.parseKeyString) return identifiers.map(utils.parseKeyString)
.every(identifier => { .every(identifier => {
return identifier === identifiers[0] return identifier === identifiers[0]
|| (identifier.namespace === identifiers[0].namespace || (identifier.namespace === identifiers[0].namespace
&& identifier.key === identifiers[0].key); && identifier.key === identifiers[0].key);
}); });
} };
getOriginalPath(identifier, path = []) { ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
return this.get(identifier).then((domainObject) => { return this.get(identifier).then((domainObject) => {
path.push(domainObject); path.push(domainObject);
let location = domainObject.location; let location = domainObject.location;
@ -623,22 +585,58 @@ export default class ObjectAPI {
return path; return path;
} }
}); });
} };
isObjectPathToALink(domainObject, objectPath) { ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
return objectPath !== undefined return objectPath !== undefined
&& objectPath.length > 1 && objectPath.length > 1
&& domainObject.location !== this.makeKeyString(objectPath[1].identifier); && domainObject.location !== this.makeKeyString(objectPath[1].identifier);
} };
isTransactionActive() { ObjectAPI.prototype.isTransactionActive = function () {
return Boolean(this.transaction && this.openmct.editor.isEditing()); return Boolean(this.transaction && this.openmct.editor.isEditing());
} };
#hasAlreadyBeenPersisted(domainObject) { /**
* Uniquely identifies a domain object.
*
* @typedef Identifier
* @memberof module:openmct.ObjectAPI~
* @property {string} namespace the namespace to/from which this domain
* object should be loaded/stored.
* @property {string} key a unique identifier for the domain object
* within that namespace
*/
/**
* A domain object is an entity of relevance to a user's workflow, that
* should appear as a distinct and meaningful object within the user
* interface. Examples of domain objects are folders, telemetry sensors,
* and so forth.
*
* A few common properties are defined for domain objects. Beyond these,
* individual types of domain objects may add more as they see fit.
*
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
* uniquely identifies this domain object
* @property {string} type the type of domain object
* @property {string} name the human-readable name for this domain object
* @property {string} [creator] the user name of the creator of this domain
* object
* @property {number} [modified] the time, in milliseconds since the UNIX
* epoch, at which this domain object was last modified
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
* present, this will be used by the default composition provider
* to load domain objects
* @typedef DomainObject
* @memberof module:openmct
*/
function hasAlreadyBeenPersisted(domainObject) {
const result = domainObject.persisted !== undefined const result = domainObject.persisted !== undefined
&& domainObject.persisted >= domainObject.modified; && domainObject.persisted >= domainObject.modified;
return result; return result;
} }
}
export default ObjectAPI;

View File

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

View File

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

View File

@ -121,18 +121,6 @@ define([
return _.sortBy(matchingMetadata, ...iteratees); 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 () { TelemetryMetadataManager.prototype.getFilterableValues = function () {
return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0); return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0);
}; };

View File

@ -43,23 +43,9 @@ define([
}; };
this.valueMetadata = valueMetadata; this.valueMetadata = valueMetadata;
this.formatter = formatMap.get(valueMetadata.format) || numberFormatter;
function getNonArrayValue(value) { if (valueMetadata.format === 'enum') {
//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') {
this.formatter = {}; this.formatter = {};
this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) { this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) {
vm.byValue[e.value] = e.string; vm.byValue[e.value] = e.string;
@ -91,13 +77,13 @@ define([
// Check for formatString support once instead of per format call. // Check for formatString support once instead of per format call.
if (valueMetadata.formatString) { if (valueMetadata.formatString) {
const baseFormat = this.formatter.format; const baseFormat = this.formatter.format;
const formatString = getNonArrayValue(valueMetadata.formatString); const formatString = valueMetadata.formatString;
this.formatter.format = function (value) { this.formatter.format = function (value) {
return printj.sprintf(formatString, baseFormat.call(this, value)); return printj.sprintf(formatString, baseFormat.call(this, value));
}; };
} }
if (valueMetadataFormat === 'string') { if (valueMetadata.format === 'string') {
this.formatter.parse = function (value) { this.formatter.parse = function (value) {
if (value === undefined) { if (value === undefined) {
return ''; return '';
@ -122,14 +108,7 @@ define([
TelemetryValueFormatter.prototype.parse = function (datum) { TelemetryValueFormatter.prototype.parse = function (datum) {
if (_.isObject(datum)) { if (_.isObject(datum)) {
const objectDatum = datum[this.valueMetadata.source]; return this.formatter.parse(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); return this.formatter.parse(datum);
@ -137,14 +116,7 @@ define([
TelemetryValueFormatter.prototype.format = function (datum) { TelemetryValueFormatter.prototype.format = function (datum) {
if (_.isObject(datum)) { if (_.isObject(datum)) {
const objectDatum = datum[this.valueMetadata.source]; return this.formatter.format(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); return this.formatter.format(datum);

View File

@ -66,13 +66,13 @@ export default class StatusAPI extends EventEmitter {
* @returns {Promise<Boolean>} true if operation was successful, otherwise false. * @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/ */
async setPollQuestion(questionText) { async setPollQuestion(questionText) {
const canSetPollQuestion = await this.canSetPollQuestion(); if (this.canSetPollQuestion()) {
if (canSetPollQuestion) {
const provider = this.#userAPI.getProvider(); const provider = this.#userAPI.getProvider();
const result = await provider.setPollQuestion(questionText); const result = await provider.setPollQuestion(questionText);
// TODO re-implement clearing all statuses
try { try {
await this.resetAllStatuses(); await this.resetAllStatuses();
} catch (error) { } catch (error) {
@ -124,8 +124,11 @@ export default class StatusAPI extends EventEmitter {
if (provider.getStatusForRole) { if (provider.getStatusForRole) {
const status = await provider.getStatusForRole(role); const status = await provider.getStatusForRole(role);
if (status !== undefined) {
return status; return status;
} else {
return undefined;
}
} else { } else {
this.#userAPI.error("User provider does not support role status"); this.#userAPI.error("User provider does not support role status");
} }
@ -275,10 +278,10 @@ export default class StatusAPI extends EventEmitter {
} }
/** /**
* @typedef {import('./UserProvider')} UserProvider * @typedef {import('./UserAPI').UserProvider} UserProvider
*/ */
/** /**
* @typedef {import('./StatusUserProvider')} StatusUserProvider * @typedef {import('./UserAPI').StatusUserProvider} StatusUserProvider
*/ */
/** /**
* The PollQuestion type * The PollQuestion type

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,155 +20,18 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<div class="c-bar-graph-options js-bar-plot-option"> <ul class="c-tree c-bar-graph-options">
<ul class="c-tree">
<h2 title="Display properties for this object">Bar Graph Series</h2> <h2 title="Display properties for this object">Bar Graph Series</h2>
<li> <li
<series-options v-for="series in domainObject.composition"
v-for="series in plotSeries"
:key="series.key" :key="series.key"
>
<series-options
:item="series" :item="series"
:color-palette="colorPalette" :color-palette="colorPalette"
/> />
</li> </li>
</ul> </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>
</template> </template>
<script> <script>
@ -182,17 +45,8 @@ export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data() { data() {
return { return {
xKey: this.domainObject.configuration.axes.xKey,
yKey: this.domainObject.configuration.axes.yKey,
xKeyLabel: '',
yKeyLabel: '',
plotSeries: [],
yKeyOptions: [],
xKeyOptions: [],
isEditing: this.openmct.editor.isEditing(), isEditing: this.openmct.editor.isEditing(),
colorPalette: this.colorPalette, colorPalette: this.colorPalette
useInterpolation: this.domainObject.configuration.useInterpolation,
useBar: this.domainObject.configuration.useBar
}; };
}, },
computed: { computed: {
@ -205,187 +59,13 @@ export default {
}, },
mounted() { mounted() {
this.openmct.editor.on('isEditing', this.setEditState); this.openmct.editor.on('isEditing', this.setEditState);
this.composition = this.openmct.composition.get(this.domainObject);
this.registerListeners();
this.composition.load();
}, },
beforeDestroy() { beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState); this.openmct.editor.off('isEditing', this.setEditState);
this.stopListening();
}, },
methods: { methods: {
setEditState(isEditing) { setEditState(isEditing) {
this.isEditing = 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 class="c-object-label__name">{{ name }}</div>
</div> </div>
</li> </li>
<ul class="grid-properties">
<li class="grid-row">
<ColorSwatch <ColorSwatch
v-if="expanded" v-if="expanded"
:current-color="currentColor" :current-color="currentColor"
title="Manually set the color for this bar graph series." title="Manually set the color for this bar graph series."
edit-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." view-title="The color for this bar graph series."
short-label="Color" short-label="Color"
class="grid-properties"
@colorSet="setColor" @colorSet="setColor"
/> />
</li>
</ul>
</ul> </ul>
</template> </template>
@ -112,6 +109,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.key = this.openmct.objects.makeKeyString(this.item);
this.initColorAndName(); this.initColorAndName();
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName); this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
}, },
@ -122,7 +120,6 @@ export default {
}, },
methods: { methods: {
initColorAndName() { initColorAndName() {
this.key = this.openmct.objects.makeKeyString(this.item.identifier);
// this is called before the plot is initialized // this is called before the plot is initialized
if (!this.domainObject.configuration.barStyles.series[this.key]) { if (!this.domainObject.configuration.barStyles.series[this.key]) {
const color = this.colorPalette.getNextColor().asHexString(); const color = this.colorPalette.getNextColor().asHexString();

View File

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

View File

@ -57,18 +57,18 @@ describe("the plugin", function () {
const testTelemetry = [ const testTelemetry = [
{ {
'utc': 1, 'utc': 1,
'some-key': ['1.3222'], 'some-key': 'some-value 1',
'some-other-key': [1] 'some-other-key': 'some-other-value 1'
}, },
{ {
'utc': 2, 'utc': 2,
'some-key': ['2.555'], 'some-key': 'some-value 2',
'some-other-key': [2] 'some-other-key': 'some-other-value 2'
}, },
{ {
'utc': 3, 'utc': 3,
'some-key': ['3.888'], 'some-key': 'some-value 3',
'some-other-key': [3] 'some-other-key': 'some-other-value 3'
} }
]; ];
@ -123,6 +123,7 @@ describe("the plugin", function () {
}); });
describe("The bar graph view", () => { describe("The bar graph view", () => {
let testDomainObject;
let barGraphObject; let barGraphObject;
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
let component; let component;
@ -134,21 +135,51 @@ describe("the plugin", function () {
namespace: "", namespace: "",
key: "test-plot" key: "test-plot"
}, },
configuration: {
barStyles: {
series: {}
},
axes: {},
useInterpolation: 'linear',
useBar: true
},
type: "telemetry.plot.bar-graph", type: "telemetry.plot.bar-graph",
name: "Test 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 = new EventEmitter();
mockComposition.load = () => { mockComposition.load = () => {
return []; mockComposition.emit('add', testDomainObject);
return [testDomainObject];
}; };
spyOn(openmct.composition, 'get').and.returnValue(mockComposition); spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
@ -216,116 +247,15 @@ describe("the plugin", function () {
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath); const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW); 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); barGraphView.show(child, true);
expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object");
mockComposition.emit('add', dotFullTelemetryObject); 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(); 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", () => { describe("the bar graph objects", () => {
const mockObject = { const mockObject = {
name: 'A very nice bar graph', name: 'A very nice bar graph',
@ -482,7 +412,7 @@ describe("the plugin", function () {
testDomainObject = { testDomainObject = {
identifier: { identifier: {
namespace: "", namespace: "",
key: "~Some~foo.bar" key: "test-object"
}, },
type: "test-object", type: "test-object",
name: "Test Object", name: "Test Object",
@ -530,17 +460,12 @@ describe("the plugin", function () {
isAlias: true isAlias: true
} }
} }
}, }
axes: {},
useInterpolation: 'linear',
useBar: true
}, },
composition: [ composition: [
{ {
identifier: {
key: '~Some~foo.bar' key: '~Some~foo.bar'
} }
}
] ]
} }
} }

View File

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

View File

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

View File

@ -22,7 +22,7 @@
import Condition from "./Condition"; import Condition from "./Condition";
import { getLatestTimestamp } from './utils/time'; import { getLatestTimestamp } from './utils/time';
import { v4 as uuid } from 'uuid'; import uuid from "uuid";
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
export default class ConditionManager extends 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.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {
this.openmct.telemetry.request(conditionSetDomainObject) this.openmct.telemetry.request(conditionSetDomainObject)
.then(output => { .then(output => {
if (output && output.length && (this.conditionSetIdentifier && this.openmct.objects.areIdsEqual(conditionSetDomainObject.identifier, this.conditionSetIdentifier))) { if (output && output.length) {
this.handleConditionSetResultUpdated(output[0]); 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 Criterion from './Criterion.vue';
import ConditionDescription from "./ConditionDescription.vue"; import ConditionDescription from "./ConditionDescription.vue";
import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants"; import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
import { v4 as uuid } from 'uuid'; import uuid from 'uuid';
export default { export default {
components: { components: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ describe('the plugin', function () {
element.appendChild(child); element.appendChild(child);
openmct.on('start', done); openmct.on('start', done);
openmct.start(child); openmct.startHeadless();
}); });
afterEach(() => { afterEach(() => {
@ -88,35 +88,6 @@ describe('the plugin', function () {
expect(displayLayoutViewProvider).toBeDefined(); 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', () => { describe('the alpha numeric format view', () => {
let displayLayoutItem; let displayLayoutItem;
let telemetryItem; let telemetryItem;
@ -380,7 +351,7 @@ describe('the plugin', function () {
it('provides controls including separators', () => { it('provides controls including separators', () => {
const displayLayoutToolbar = openmct.toolbars.get(selection); 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. * 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 * 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 JSONExporter from '/src/exporters/JSONExporter.js';
import _ from 'lodash'; import _ from 'lodash';
import { v4 as uuid } from 'uuid'; import uuid from "uuid";
export default class ExportAsJSONAction { export default class ExportAsJSONAction {
constructor(openmct) { 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));
};
}

View File

@ -1,90 +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__search-row">
<Search
class="c-fault-mgmt-search"
:value="searchTerm"
@input="updateSearchTerm"
@clear="updateSearchTerm"
/>
<SelectField
class="c-fault-mgmt-viewButton"
title="View Filter"
:model="model"
@onChange="onChange"
/>
</div>
</template>
<script>
import SelectField from '@/api/forms/components/controls/SelectField.vue';
import Search from '@/ui/components/search.vue';
import { FILTER_ITEMS } from './constants';
export default {
components: {
SelectField,
Search
},
inject: ['openmct', 'domainObject'],
props: {
searchTerm: {
type: String,
default: ''
}
},
data() {
return {
items: []
};
},
computed: {
model() {
return {
options: this.items,
value: this.items[0] ? this.items[0].value : FILTER_ITEMS[0].toLowerCase()
};
}
},
mounted() {
this.items = FILTER_ITEMS
.map(item => {
return {
name: item,
value: item.toLowerCase()
};
});
},
methods: {
onChange(data) {
this.$emit('filterChanged', data);
},
updateSearchTerm(searchTerm) {
this.$emit('updateSearchTerm', searchTerm);
}
}
};
</script>

View File

@ -1,102 +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__toolbar">
<button
class="c-icon-button icon-bell"
title="Acknowledge selected faults"
:disabled="disableAcknowledge"
@click="acknowledgeSelected"
>
<div
title="Acknowledge selected faults"
class="c-icon-button__label"
>
Acknowledge
</div>
</button>
<button
class="c-icon-button icon-timer"
title="Shelve selected faults"
:disabled="disableShelve"
@click="shelveSelected"
>
<div
title="Shelve selected items"
class="c-icon-button__label"
>
Shelve
</div>
</button>
</div>
</template>
<script>
export default {
inject: ['openmct', 'domainObject'],
props: {
selectedFaults: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
disableAcknowledge: true,
disableShelve: true
};
},
watch: {
selectedFaults(newSelectedFaults) {
const selectedfaults = Object.values(newSelectedFaults);
let disableAcknowledge = true;
let disableShelve = true;
selectedfaults.forEach(fault => {
if (!fault.shelved) {
disableShelve = false;
}
if (!fault.acknowledged) {
disableAcknowledge = false;
}
});
this.disableAcknowledge = disableAcknowledge;
this.disableShelve = disableShelve;
}
},
methods: {
acknowledgeSelected() {
this.$emit('acknowledgeSelected');
},
shelveSelected() {
this.$emit('shelveSelected');
}
}
};
</script>

View File

@ -1,77 +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">
<FaultManagementListView
:faults-list="faultsList"
/>
</div>
</template>
<script>
import FaultManagementListView from './FaultManagementListView.vue';
import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS } from './constants';
export default {
components: {
FaultManagementListView
},
inject: ['openmct', 'domainObject'],
data() {
return {
faultsList: []
};
},
mounted() {
this.updateFaultList();
this.unsubscribe = this.openmct.faults
.subscribe(this.domainObject, this.updateFault);
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
}
},
methods: {
updateFault({ fault, type }) {
if (type === FAULT_MANAGEMENT_GLOBAL_ALARMS) {
this.updateFaultList();
} else if (type === FAULT_MANAGEMENT_ALARMS) {
this.faultsList.forEach((faultValue, i) => {
if (fault.id === faultValue.id) {
this.$set(this.faultsList, i, fault);
}
});
}
},
updateFaultList() {
this.openmct.faults
.request(this.domainObject)
.then(faultsData => {
this.faultsList = faultsData.map(fd => fd.fault);
});
}
}
};
</script>

View File

@ -1,69 +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 FaultManagementView from './FaultManagementView.vue';
import { FAULT_MANAGEMENT_TYPE, FAULT_MANAGEMENT_VIEW } from './constants';
import Vue from 'vue';
export default class FaultManagementViewProvider {
constructor(openmct) {
this.openmct = openmct;
this.key = FAULT_MANAGEMENT_VIEW;
}
canView(domainObject) {
return domainObject.type === FAULT_MANAGEMENT_TYPE;
}
canEdit(domainObject) {
return false;
}
view(domainObject) {
let component;
const openmct = this.openmct;
return {
show: (element) => {
component = new Vue({
el: element,
components: {
FaultManagementView
},
provide: {
openmct,
domainObject
},
template: '<FaultManagementView></FaultManagementView>'
});
},
destroy: () => {
if (!component) {
return;
}
component.$destroy();
component = undefined;
}
};
}
}

View File

@ -1,122 +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 FAULT_SEVERITY = {
'CRITICAL': {
name: 'CRITICAL',
value: 'critical',
priority: 0
},
'WARNING': {
name: 'WARNING',
value: 'warning',
priority: 1
},
'WATCH': {
name: 'WATCH',
value: 'watch',
priority: 2
}
};
export const FAULT_MANAGEMENT_TYPE = 'faultManagement';
export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector';
export const FAULT_MANAGEMENT_ALARMS = 'alarms';
export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status';
export const FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS = [
{
name: '5 Minutes',
value: 300000
},
{
name: '10 Minutes',
value: 600000
},
{
name: '15 Minutes',
value: 900000
},
{
name: 'Indefinite',
value: 0
}
];
export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view';
export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy';
export const FILTER_ITEMS = [
'Standard View',
'Acknowledged',
'Unacknowledged',
'Shelved'
];
export const SORT_ITEMS = {
'newest-first': {
name: 'Newest First',
value: 'newest-first',
sortFunction: (a, b) => {
if (b.triggerTime > a.triggerTime) {
return 1;
}
if (a.triggerTime > b.triggerTime) {
return -1;
}
return 0;
}
},
'oldest-first': {
name: 'Oldest First',
value: 'oldest-first',
sortFunction: (a, b) => {
if (a.triggerTime > b.triggerTime) {
return 1;
}
if (a.triggerTime < b.triggerTime) {
return -1;
}
return 0;
}
},
'severity': {
name: 'Severity',
value: 'severity',
sortFunction: (a, b) => {
const diff = FAULT_SEVERITY[a.severity].priority - FAULT_SEVERITY[b.severity].priority;
if (diff !== 0) {
return diff;
}
if (b.triggerTime > a.triggerTime) {
return 1;
}
if (a.triggerTime > b.triggerTime) {
return -1;
}
return 0;
}
}
};

View File

@ -1,234 +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.
*****************************************************************************/
/*********************************************** FAULT PROPERTIES */
.is-severity-critical{
@include glyphBefore($glyph-icon-alert-triangle);
color: $colorStatusError;
}
.is-severity-warning{
@include glyphBefore($glyph-icon-alert-rect);
color: $colorStatusAlert;
}
.is-severity-watch{
@include glyphBefore($glyph-icon-info);
color: $colorCommand;
}
.is-unacknowledged{
.c-fault-mgmt__list-severity{
@include pulse($animName: severityAnim, $dur: 200ms);
}
}
.is-selected {
background: $colorSelectedBg;
}
.is-shelved{
.c-fault-mgmt__list-content{
opacity: 50% !important;
font-style: italic;
}
.c-fault-mgmt__list-severity{
@include pulse($animName: shelvedAnim, $dur: 0ms);
}
}
/*********************************************** SEARCH */
.c-fault-mgmt__search-row{
display: flex;
align-items: center;
> * + * {
margin-left: 10px;
float: right;
}
}
.c-fault-mgmt-search{
width: 95%;
}
/*********************************************** TOOLBAR */
.c-fault-mgmt__toolbar{
display: flex;
justify-content: center;
> * {
font-size: 1.25em;
}
}
/*********************************************** LIST VIEW */
.c-faults-list-view {
display: flex;
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
}
}
/*********************************************** FAULT ITEM */
.c-fault-mgmt__list{
background: rgba($colorBodyFg, 0.1);
margin-bottom: 5px;
padding: 4px;
display: flex;
align-items: center;
> * {
margin-left: $interiorMargin;
}
&-severity{
font-size: 2em;
margin-left: $interiorMarginLg;
}
&-pathname{
flex-wrap: wrap;
flex: 1 1 auto;
}
&-path{
font-size: .75em;
}
&-faultname{
font-weight: bold;
font-size: 1.3em;
}
&-content{
display: flex;
flex-wrap: wrap;
flex: 1 1 auto;
align-items: center;
}
&-content-right{
margin-left: auto;
display: flex;
flex-wrap: wrap;
}
&-trigVal, &-curVal, &-trigTime{
@include ellipsize;
border-radius: $controlCr;
padding: $interiorMargin;
width: 80px;
margin-right: $interiorMarginLg;
}
&-trigVal {
@include isLimit();
background: rgba($colorBodyFg, 0.25);
}
&-curVal {
@include isLimit();
background: rgba($colorBodyFg, 0.25);
&-alert{
background: $colorWarningHi;
}
}
&-trigTime{
width: auto;
}
&-action-wrapper{
display: flex;
align-content: right;
width: 100px;
}
&-action-button{
flex: 0 0 auto;
margin-left: auto;
justify-content: right;
}
}
/*********************************************** LIST HEADER */
.c-fault-mgmt__list-header{
display: flex;
background: rgba($colorBodyFg, .23);
border-radius: $controlCr;
&-tripVal, &-liveVal, &-trigTime{
background: none;
}
&-trigTime{
width: 160px;
}
&-sortButton{
flex: 0 0 auto;
margin-left: auto;
justify-content: right;
display: flex;
align-content: right;
width: 100px;
}
}
.is-severity-critical{
@include glyphBefore($glyph-icon-alert-triangle);
color: $colorStatusError;
}
.is-severity-warning{
@include glyphBefore($glyph-icon-alert-rect);
color: $colorStatusAlert;
}
.is-severity-watch{
@include glyphBefore($glyph-icon-info);
color: $colorCommand;
}
.is-unacknowledged{
.c-fault-mgmt__list-severity{
@include pulse($animName: severityAnim, $dur: 200ms);
}
}
.is-selected {
background: $colorSelectedBg;
}
.is-shelved{
.c-fault-mgmt__list-content{
opacity: 60% !important;
font-style: italic;
}
.c-fault-mgmt__list-severity{
@include pulse($animName: shelvedAnim, $dur: 0ms);
}
}

View File

@ -1,52 +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 { FAULT_MANAGEMENT_TYPE } from './constants';
describe("The Fault Management Plugin", () => {
let openmct;
beforeEach(() => {
openmct = createOpenMct();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('is not installed by default', () => {
let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
expect(typeDef.name).toBe('Unknown Type');
});
it('can be installed', () => {
openmct.install(openmct.plugins.FaultManagement());
let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
expect(typeDef.name).toBe('Fault Management');
});
});

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