This commit is contained in:
Joshi 2022-06-07 14:02:58 -07:00
commit 7bbaec4006
204 changed files with 11399 additions and 2476 deletions

View File

@ -31,7 +31,7 @@ commands:
type: string
steps:
- when:
condition:
condition:
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
steps:
- restore_cache:
@ -41,7 +41,7 @@ commands:
parameters:
node-version:
type: string
steps:
steps:
- save_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
paths:
@ -61,7 +61,7 @@ commands:
upload_code_covio:
description: "Command to upload code coverage reports to codecov.io"
steps:
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
orbs:
node: circleci/node@4.9.0
browser-tools: circleci/browser-tools@1.3.0
@ -101,7 +101,7 @@ jobs:
equal: [ "FirefoxESR", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox:
version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
- when:
condition:
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
@ -158,7 +158,7 @@ workflows:
- lint:
name: node16-lint
node-version: lts/gallium
- unit-test:
- unit-test:
name: node14-chrome
node-version: lts/fermium
browser: ChromeHeadless

View File

@ -4,9 +4,9 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
retries: 1, //Only for debugging purposes
testDir: 'tests/performance/',
timeout: 30 * 1000,
timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'npm run start',
@ -20,7 +20,7 @@ const config = {
headless: Boolean(process.env.CI), //Only if running locally
ignoreHTTPSErrors: true,
screenshot: 'off',
trace: 'off',
trace: 'on-first-retry',
video: 'off'
},
projects: [

View File

@ -1 +1 @@
{"openmct":{"21338566-d472-4377-aed1-21b79272c8de":{"identifier":{"key":"21338566-d472-4377-aed1-21b79272c8de","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":1,"y":1,"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"5aeb5a71-3149-41ed-9d8a-d34b0a18b053"}],"layoutGrid":[10,10]},"modified":1652228997384,"location":"mine","persisted":1652228997384},"644c2e47-2903-475f-8a4a-6be1588ee02f":{"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1}},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1652228997375,"location":"21338566-d472-4377-aed1-21b79272c8de","persisted":1652228997375}},"rootId":"21338566-d472-4377-aed1-21b79272c8de"}
{"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

@ -103,10 +103,10 @@ test.describe('Performance tests', () => {
await page.goto('/');
// Search Available after Launch
await page.locator('input[type="search"]').click();
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('input[type="search"]').fill('Performance Display Layout');
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([
@ -164,7 +164,7 @@ test.describe('Performance tests', () => {
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
// Click Close Icon
await page.locator('.c-click-icon').click();
await page.locator('[aria-label="Close"]').click();
await page.evaluate(() => window.performance.mark("view-large-close-button"));
//await client.send('HeapProfiler.enable');

View File

@ -64,9 +64,9 @@ test.describe.skip('Memory Performance tests', () => {
await page.goto('/', {waitUntil: 'networkidle'});
// To to Search Available after Launch
await page.locator('input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill Search input
await page.locator('input[type="search"]').fill('Performance Display Layout');
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(),

View File

@ -98,10 +98,10 @@ test.describe('Performance tests', () => {
await page.goto('/');
// To to Search Available after Launch
await page.locator('input[type="search"]').click();
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('input[type="search"]').fill('Performance Notebook');
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([

View File

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

View File

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

View File

@ -32,7 +32,6 @@ const { expect } = require('@playwright/test');
test.describe('Example Imagery', () => {
test.beforeEach(async ({ page }) => {
page.on('console', msg => console.log(msg.text()));
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
@ -61,19 +60,19 @@ test.describe('Example Imagery', () => {
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const deltaYStep = 100; //equivalent to 1x zoom
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
// zoom in
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
await page.mouse.wheel(0, deltaYStep * 2);
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
// zoom out
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
await page.mouse.wheel(0, -deltaYStep);
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
@ -88,11 +87,11 @@ test.describe('Example Imagery', () => {
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
// zoom in
await page.mouse.wheel(0, deltaYStep * 2);
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
@ -151,22 +150,22 @@ test.describe('Example Imagery', () => {
test('Can use + - buttons to zoom on the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover();
const zoomInBtn = page.locator('.t-btn-zoom-in');
const zoomOutBtn = page.locator('.t-btn-zoom-out');
await bgImageLocator.hover({trial: true});
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0);
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomOutBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
@ -176,18 +175,18 @@ test.describe('Example Imagery', () => {
test('Can use the reset button to reset the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const zoomInBtn = page.locator('.t-btn-zoom-in');
const zoomResetBtn = page.locator('.t-btn-zoom-reset');
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0);
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
@ -195,7 +194,7 @@ test.describe('Example Imagery', () => {
await zoomResetBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const resetBoundingBox = await bgImageLocator.boundingBox();
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
@ -209,18 +208,18 @@ test.describe('Example Imagery', () => {
const bgImageLocator = page.locator(backgroundImageSelector);
const pausePlayButton = page.locator('.c-button.pause-play');
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
// open the time conductor drop down
await page.locator('.c-conductor__controls button.c-mode-button').click();
await page.locator('button:has-text("Fixed Timespan")').click();
// Click local clock
await page.locator('.icon-clock >> text=Local Clock').click();
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
const zoomInBtn = page.locator('.t-btn-zoom-in');
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
});
@ -267,7 +266,7 @@ test('Example Imagery in Display layout', async ({ page }) => {
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
// Click previous image button
const previousImageButton = page.locator('.c-nav--prev');
@ -279,7 +278,7 @@ test('Example Imagery in Display layout', async ({ page }) => {
// Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * 2);
const zoomedBoundingBox = await bgImageLocator.boundingBox();
@ -287,7 +286,7 @@ test('Example Imagery in Display layout', async ({ page }) => {
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// Wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
@ -311,11 +310,11 @@ test('Example Imagery in Display layout', async ({ page }) => {
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// Zoom in on next image
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
await page.mouse.wheel(0, deltaYStep * 2);
// Wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);

View File

@ -0,0 +1,30 @@
/*****************************************************************************
* 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

@ -0,0 +1,198 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
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

@ -0,0 +1,264 @@
/*****************************************************************************
* 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');
});
});

View File

@ -0,0 +1,101 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
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

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

View File

@ -192,5 +192,17 @@ test('Visual - Save Successful Banner', async ({ page }) => {
//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

@ -0,0 +1,33 @@
/*****************************************************************************
* 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

@ -0,0 +1,19 @@
{
"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

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

View File

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

View File

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

View File

@ -0,0 +1,83 @@
/*****************************************************************************
* 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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
{
"name": "openmct",
"version": "2.0.4-SNAPSHOT",
"version": "2.0.5-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.16.3",
"@babel/eslint-parser": "7.18.2",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.2.1",
"@percy/playwright": "1.0.4",
@ -25,10 +25,9 @@
"eslint": "8.13.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.9.0",
"eslint-plugin-vue": "8.5.0",
"eslint-plugin-vue": "9.1.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"exports-loader": "0.7.0",
"express": "4.13.1",
"file-saver": "2.0.5",
"git-rev-sync": "3.0.2",
@ -61,21 +60,20 @@
"printj": "1.3.1",
"request": "2.88.2",
"resolve-url-loader": "5.0.0",
"sass": "1.49.9",
"sass": "1.52.2",
"sass-loader": "12.6.0",
"sinon": "14.0.0",
"style-loader": "^1.0.1",
"uuid": "8.3.2",
"vue": "2.6.14",
"vue-eslint-parser": "8.3.0",
"vue-eslint-parser": "9.0.2",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"webpack": "5.68.0",
"webpack-cli": "4.9.2",
"webpack-dev-middleware": "5.3.3",
"webpack-hot-middleware": "2.25.1",
"webpack-merge": "5.8.0",
"zepto": "1.2.0"
"webpack-merge": "5.8.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",

View File

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

View File

@ -0,0 +1,275 @@
/*****************************************************************************
* 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

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

View File

@ -0,0 +1,106 @@
/*****************************************************************************
* 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

@ -0,0 +1,144 @@
/*****************************************************************************
* 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,18 +44,14 @@
>
{{ section.name }}
</h2>
<div
<FormRow
v-for="(row, index) in section.rows"
:key="row.id"
class="u-contents"
>
<FormRow
:css-class="section.cssClass"
:first="index < 1"
:row="row"
@onChange="onChange"
/>
</div>
:css-class="row.cssClass"
:first="index < 1"
:row="row"
@onChange="onChange"
/>
</div>
</form>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

295
src/api/user/StatusAPI.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,103 @@
/*****************************************************************************
* 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,10 +9,6 @@
> *:first-child {
flex: 1 1 auto;
}
&.is-in-small-container {
//background: rgba(blue, 0.1);
}
}
.c-frame__move-bar {

View File

@ -17,14 +17,14 @@
}
}
> * + * {
margin-left: $interiorMargin;
}
&__value {
@include isLimit();
}
&__label {
margin-right: $interiorMargin;
}
.c-frame & {
@include abs();
border: 1px solid transparent;

View File

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

View File

@ -0,0 +1,129 @@
/*****************************************************************************
* 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

@ -0,0 +1,71 @@
/*****************************************************************************
* 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

@ -0,0 +1,103 @@
/*****************************************************************************
* 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

@ -0,0 +1,191 @@
/*****************************************************************************
* 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-' + fault.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

@ -0,0 +1,299 @@
/*****************************************************************************
* 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

@ -0,0 +1,56 @@
/*****************************************************************************
* 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

@ -0,0 +1,42 @@
/*****************************************************************************
* 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

@ -0,0 +1,90 @@
/*****************************************************************************
* 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

@ -0,0 +1,102 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<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

@ -0,0 +1,77 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<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

@ -0,0 +1,69 @@
/*****************************************************************************
* 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

@ -0,0 +1,122 @@
/*****************************************************************************
* 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

@ -0,0 +1,234 @@
/*****************************************************************************
* 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

@ -0,0 +1,52 @@
/*****************************************************************************
* 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');
});
});

View File

@ -76,6 +76,13 @@ export default class EditPropertiesAction extends PropertiesAction {
}
}
/**
* @private
*/
_onCancel() {
//noop
}
/**
* @private
*/
@ -87,6 +94,7 @@ export default class EditPropertiesAction extends PropertiesAction {
formStructure.title = 'Edit ' + this.domainObject.name;
return this.openmct.forms.showForm(formStructure)
.then(this._onSave.bind(this));
.then(this._onSave.bind(this))
.catch(this._onCancel.bind(this));
}
}

View File

@ -123,6 +123,9 @@ describe('EditPropertiesAction plugin', () => {
}
editPropertiesAction.invoke([domainObject])
.then(() => {
done();
})
.catch(() => {
done();
});
@ -208,6 +211,10 @@ describe('EditPropertiesAction plugin', () => {
};
editPropertiesAction.invoke([domainObject])
.then(() => {
expect(domainObject.name).toEqual(name);
done();
})
.catch(() => {
expect(domainObject.name).toEqual(name);

View File

@ -49,6 +49,7 @@ export default function () {
gaugeType: GAUGE_TYPES[0][1],
isDisplayMinMax: true,
isDisplayCurVal: true,
isDisplayUnits: true,
isUseTelemetryLimits: true,
limitLow: 10,
limitHigh: 90,
@ -59,6 +60,23 @@ export default function () {
};
},
form: [
{
name: "Gauge type",
options: GAUGE_TYPES.map(type => {
return {
name: type[0],
value: type[1]
};
}),
control: "select",
cssClass: "l-input-sm",
key: "gaugeController",
property: [
"configuration",
"gaugeController",
"gaugeType"
]
},
{
name: "Display current value",
control: "toggleSwitch",
@ -70,6 +88,17 @@ export default function () {
"isDisplayCurVal"
]
},
{
name: "Display units",
control: "toggleSwitch",
cssClass: "l-input",
key: "isDisplayUnits",
property: [
"configuration",
"gaugeController",
"isDisplayUnits"
]
},
{
name: "Display range values",
control: "toggleSwitch",
@ -92,23 +121,6 @@ export default function () {
"precision"
]
},
{
name: "Gauge type",
options: GAUGE_TYPES.map(type => {
return {
name: type[0],
value: type[1]
};
}),
control: "select",
cssClass: "l-input-sm",
key: "gaugeController",
property: [
"configuration",
"gaugeController",
"gaugeType"
]
},
{
name: "Value ranges and limits",
control: "gauge-controller",

View File

@ -63,30 +63,30 @@ describe('Gauge plugin', () => {
});
it('Plugin installed by default', () => {
const gaugueType = openmct.types.get('gauge');
const GaugeType = openmct.types.get('gauge');
expect(gaugueType).not.toBeNull();
expect(gaugueType.definition.name).toEqual('Gauge');
expect(GaugeType).not.toBeNull();
expect(GaugeType.definition.name).toEqual('Gauge');
});
it('Gaugue plugin is creatable', () => {
const gaugueType = openmct.types.get('gauge');
it('Gauge plugin is creatable', () => {
const GaugeType = openmct.types.get('gauge');
expect(gaugueType.definition.creatable).toBeTrue();
expect(GaugeType.definition.creatable).toBeTrue();
});
it('Gaugue plugin is creatable', () => {
const gaugueType = openmct.types.get('gauge');
it('Gauge plugin is creatable', () => {
const GaugeType = openmct.types.get('gauge');
expect(gaugueType.definition.creatable).toBeTrue();
expect(GaugeType.definition.creatable).toBeTrue();
});
it('Gaugue form controller', () => {
it('Gauge form controller', () => {
const gaugeController = openmct.forms.getFormControl('gauge-controller');
expect(gaugeController).toBeDefined();
});
describe('Gaugue with Filled Dial', () => {
describe('Gauge with Filled Dial', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@ -105,6 +105,7 @@ describe('Gauge plugin', () => {
gaugeType: 'dial-filled',
isDisplayMinMax: true,
isDisplayCurVal: true,
isDisplayUnits: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
@ -222,7 +223,7 @@ describe('Gauge plugin', () => {
});
});
describe('Gaugue with Needle Dial', () => {
describe('Gauge with Needle Dial', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@ -240,6 +241,7 @@ describe('Gauge plugin', () => {
gaugeType: 'dial-needle',
isDisplayMinMax: true,
isDisplayCurVal: true,
isDisplayUnits: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
@ -357,7 +359,7 @@ describe('Gauge plugin', () => {
});
});
describe('Gaugue with Vertical Meter', () => {
describe('Gauge with Vertical Meter', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@ -375,6 +377,7 @@ describe('Gauge plugin', () => {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
isDisplayUnits: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
@ -492,7 +495,7 @@ describe('Gauge plugin', () => {
});
});
describe('Gaugue with Vertical Meter Inverted', () => {
describe('Gauge with Vertical Meter Inverted', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@ -506,6 +509,7 @@ describe('Gauge plugin', () => {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
isDisplayUnits: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
@ -574,7 +578,7 @@ describe('Gauge plugin', () => {
});
});
describe('Gaugue with Horizontal Meter', () => {
describe('Gauge with Horizontal Meter', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@ -588,6 +592,7 @@ describe('Gauge plugin', () => {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
isDisplayUnits: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
@ -656,7 +661,7 @@ describe('Gauge plugin', () => {
});
});
describe('Gaugue with Filled Dial with Use Telemetry Limits', () => {
describe('Gauge with Filled Dial with Use Telemetry Limits', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@ -673,6 +678,7 @@ describe('Gauge plugin', () => {
gaugeType: 'dial-filled',
isDisplayMinMax: true,
isDisplayCurVal: true,
isDisplayUnits: true,
isUseTelemetryLimits: true,
limitLow: 10,
limitHigh: 90,

View File

@ -64,11 +64,11 @@
</svg>
<svg
v-if="displayCurVal"
class="c-dial__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<svg
v-if="displayCurVal"
class="c-dial__current-value-text-sizer"
:viewBox="curValViewBox"
>
@ -79,6 +79,17 @@
style="transform: translate(50%, 70%)"
>{{ curVal }}</text>
</svg>
<svg
class="c-gauge__units c-dial__units"
viewBox="0 0 50 100"
>
<text
class="c-dial__units-text"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 72%)"
>{{ units }}</text>
</svg>
</svg>
<svg
@ -261,7 +272,23 @@
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text>
>
<tspan>{{ curVal }}</tspan>
<tspan
v-if="typeMeterHorizontal && displayUnits"
class="c-gauge__units"
font-size="10"
>{{ units }}</tspan>
</text>
<text
v-if="typeMeterVertical && displayUnits"
dy="12"
class="c-gauge__units"
font-size="10"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ units }}</text>
</svg>
</svg>
</div>
@ -288,12 +315,15 @@ export default {
precision: gaugeController.precision,
displayMinMax: gaugeController.isDisplayMinMax,
displayCurVal: gaugeController.isDisplayCurVal,
displayUnits: gaugeController.isDisplayUnits,
limitHigh: gaugeController.limitHigh,
limitLow: gaugeController.limitLow,
rangeHigh: gaugeController.max,
rangeLow: gaugeController.min,
gaugeType: gaugeController.gaugeType,
activeTimeSystem: this.openmct.time.timeSystem()
showUnits: gaugeController.showUnits,
activeTimeSystem: this.openmct.time.timeSystem(),
units: ''
};
},
computed: {
@ -524,6 +554,8 @@ export default {
const length = values.length;
this.updateValue(values[length - 1]);
});
this.units = this.metadata.value(this.valueKey).unit || '';
},
round(val, decimals = this.precision) {
let precision = Math.pow(10, decimals);

View File

@ -111,6 +111,7 @@ export default {
isUseTelemetryLimits: this.model.value.isUseTelemetryLimits,
isDisplayMinMax: this.model.value.isDisplayMinMax,
isDisplayCurVal: this.model.value.isDisplayCurVal,
isDisplayUnits: this.model.value.isDisplayUnits,
limitHigh: this.model.value.limitHigh,
limitLow: this.model.value.limitLow,
max: this.model.value.max,
@ -125,6 +126,7 @@ export default {
gaugeType: this.model.value.gaugeType,
isDisplayMinMax: this.isDisplayMinMax,
isDisplayCurVal: this.isDisplayCurVal,
isDisplayUnits: this.isDisplayUnits,
isUseTelemetryLimits: this.isUseTelemetryLimits,
limitLow: this.limitLow,
limitHigh: this.limitHigh,

View File

@ -16,13 +16,12 @@
// Both dial and meter types
overflow: hidden;
&__range {
&__range,
&__units,
&__units text {
$c: $colorGaugeRange;
color: $c;
text {
fill: $c;
}
fill: $c;
}
&__wrapper {
@ -66,7 +65,8 @@ svg[class*='c-dial'] {
transition: transform $transitionTimeGauge;
}
&__current-value-text {
&__current-value-text,
&__units-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
}

View File

@ -0,0 +1,74 @@
<template>
<div
class="c-control-menu c-menu--to-left c-menu--has-close-btn c-image-controls c-image-controls--filters"
@click="handleClose"
>
<div
class="c-image-controls__controls"
@click="$event.stopPropagation()"
>
<span class="c-image-controls__sliders">
<div class="c-image-controls__slider-wrapper icon-brightness">
<input
v-model="filters.brightness"
type="range"
min="0"
max="500"
@change="notifyFiltersChanged"
@input="notifyFiltersChanged"
>
</div>
<div class="c-image-controls__slider-wrapper icon-contrast">
<input
v-model="filters.contrast"
type="range"
min="0"
max="500"
@change="notifyFiltersChanged"
@input="notifyFiltersChanged"
>
</div>
</span>
<span class="c-image-controls__reset-btn">
<a
class="s-icon-button icon-reset t-btn-reset"
@click="resetFilters"
></a>
</span>
</div>
<button class="c-click-icon icon-x t-btn-close c-switcher-menu__close-button"></button>
</div>
</template>
<script>
export default {
inject: ['openmct'],
data() {
return {
filters: {
brightness: 100,
contrast: 100
}
};
},
methods: {
handleClose(e) {
const closeButton = e.target.classList.contains('c-switcher-menu__close-button');
if (!closeButton) {
e.stopPropagation();
}
},
notifyFiltersChanged() {
this.$emit('filterChanged', this.filters);
},
resetFilters() {
this.filters = {
brightness: 100,
contrast: 100
};
this.notifyFiltersChanged();
}
}
};
</script>

View File

@ -21,75 +21,62 @@
*****************************************************************************/
<template>
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
<div class="c-image-controls__control c-image-controls__zoom icon-magnify">
<div class="c-button-set c-button-set--strip-h">
<button
class="c-button t-btn-zoom-out icon-minus"
title="Zoom out"
@click="zoomOut"
></button>
<div class="h-local-controls h-local-controls--overlay-content h-local-controls--menus-aligned c-local-controls--show-on-hover">
<imagery-view-menu-switcher
:icon-class="'icon-brightness'"
:title="'Brightness and contrast'"
>
<filter-settings @filterChanged="updateFilterValues" />
</imagery-view-menu-switcher>
<button
class="c-button t-btn-zoom-in icon-plus"
title="Zoom in"
@click="zoomIn"
></button>
</div>
<imagery-view-menu-switcher
v-if="layers.length"
:icon-class="'icon-layers'"
:title="'Layers'"
>
<layer-settings
:layers="layers"
@toggleLayerVisibility="toggleLayerVisibility"
/>
</imagery-view-menu-switcher>
<button
class="c-button t-btn-zoom-lock"
title="Lock current zoom and pan across all images"
:class="{'icon-unlocked': !panZoomLocked, 'icon-lock': panZoomLocked}"
@click="toggleZoomLock"
></button>
<zoom-settings
class="--hide-if-less-than-220"
:pan-zoom-locked="panZoomLocked"
:zoom-factor="zoomFactor"
@zoomOut="zoomOut"
@zoomIn="zoomIn"
@toggleZoomLock="toggleZoomLock"
@handleResetImage="handleResetImage"
/>
<button
class="c-button icon-reset t-btn-zoom-reset"
title="Remove zoom and pan"
@click="handleResetImage"
></button>
<span class="c-image-controls__zoom-factor">x{{ formattedZoomFactor }}</span>
</div>
<div class="c-image-controls__control c-image-controls__brightness-contrast">
<span
class="c-image-controls__sliders"
draggable="true"
@dragstart.stop.prevent
>
<div class="c-image-controls__input icon-brightness">
<input
v-model="filters.contrast"
type="range"
min="0"
max="500"
@change="notifyFiltersChanged"
>
</div>
<div class="c-image-controls__input icon-contrast">
<input
v-model="filters.brightness"
type="range"
min="0"
max="500"
@change="notifyFiltersChanged"
>
</div>
</span>
<span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset">
<button
class="c-icon-link icon-reset t-btn-reset"
@click="handleResetFilters"
></button>
</span>
</div>
<imagery-view-menu-switcher
class="--show-if-less-than-220"
:icon-class="'icon-magnify'"
:title="'Zoom settings'"
>
<zoom-settings
:pan-zoom-locked="panZoomLocked"
:class="'c-control-menu c-menu--has-close-btn'"
:zoom-factor="zoomFactor"
:is-menu="true"
@zoomOut="zoomOut"
@zoomIn="zoomIn"
@toggleZoomLock="toggleZoomLock"
@handleResetImage="handleResetImage"
/>
</imagery-view-menu-switcher>
</div>
</template>
<script>
import _ from 'lodash';
import FilterSettings from "./FilterSettings.vue";
import LayerSettings from "./LayerSettings.vue";
import ZoomSettings from "./ZoomSettings.vue";
import ImageryViewMenuSwitcher from "./ImageryViewMenuSwitcher.vue";
const DEFAULT_FILTER_VALUES = {
brightness: '100',
contrast: '100'
@ -101,15 +88,27 @@ const ZOOM_STEP = 1;
const ZOOM_WHEEL_SENSITIVITY_REDUCTION = 0.01;
export default {
components: {
FilterSettings,
LayerSettings,
ImageryViewMenuSwitcher,
ZoomSettings
},
inject: ['openmct', 'domainObject'],
props: {
layers: {
type: Array,
required: true
},
zoomFactor: {
type: Number,
required: true
},
imageUrl: {
type: String,
default: ''
default: () => {
return '';
}
}
},
data() {
@ -126,9 +125,6 @@ export default {
};
},
computed: {
formattedZoomFactor() {
return Number.parseFloat(this.zoomFactor).toPrecision(2);
},
cursorStates() {
const isPannable = this.altPressed && this.zoomFactor > 1;
const showCursorZoomIn = this.metaPressed && !this.shiftPressed;
@ -270,6 +266,13 @@ export default {
const newScaleFactor = this.zoomFactor + (this.shiftPressed ? -ZOOM_STEP : ZOOM_STEP);
this.zoomImage(newScaleFactor, e.clientX, e.clientY);
},
toggleLayerVisibility(index) {
this.$emit('toggleLayerVisibility', index);
},
updateFilterValues(filters) {
this.filters = filters;
this.notifyFiltersChanged();
}
}
};

View File

@ -28,34 +28,34 @@
@keydown="arrowDownHandler"
@mouseover="focusElement"
>
<div class="c-imagery__main-image-wrapper has-local-controls">
<div
class="c-imagery__main-image-wrapper has-local-controls"
:class="imageWrapperStyle"
@mousedown="handlePanZoomClick"
>
<ImageControls
ref="imageControls"
:zoom-factor="zoomFactor"
:image-url="imageUrl"
:layers="layers"
@resetImage="resetImage"
@panZoomUpdated="handlePanZoomUpdate"
@filtersUpdated="setFilters"
@cursorsUpdated="setCursorStates"
@startPan="startPan"
@toggleLayerVisibility="toggleLayerVisibility"
/>
<div
ref="imageBG"
class="c-imagery__main-image__bg"
:class="{
'paused unnsynced': isPaused && !isFixed,
'stale': false,
'pannable': cursorStates.isPannable,
'cursor-zoom-in': cursorStates.showCursorZoomIn,
'cursor-zoom-out': cursorStates.showCursorZoomOut
}"
@click="expand"
>
<div
v-if="zoomFactor > 1"
class="c-imagery__hints"
>{{ formatImageAltText }}</div>
>
{{ formatImageAltText }}
</div>
<div
ref="focusedImageWrapper"
class="image-wrapper"
@ -65,6 +65,13 @@
}"
@mousedown="handlePanZoomClick"
>
<div
v-for="(layer, index) in visibleLayers"
:key="index"
class="layer-image s-image-layer c-imagery__layer-image js-layer-image"
:style="getVisibleLayerStyles(layer)"
>
</div>
<img
ref="focusedImage"
class="c-imagery__main-image__image js-imageryView-image "
@ -81,25 +88,7 @@
ref="focusedImageElement"
class="c-imagery__main-image__background-image"
:draggable="!isSelectable"
:style="{
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`,
'background-image':
`${imageUrl ? (
`url(${imageUrl}),
repeating-linear-gradient(
45deg,
transparent,
transparent 4px,
rgba(125,125,125,.2) 4px,
rgba(125,125,125,.2) 8px
)`
) : ''}`,
'transform': `scale(${zoomFactor}) translate(${imageTranslateX}px, ${imageTranslateY}px)`,
'transition': `${!pan && animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
'width': `${sizedImageWidth}px`,
'height': `${sizedImageHeight}px`,
}"
:style="focusImageStyles"
></div>
<Compass
v-if="shouldDisplayCompass"
@ -260,6 +249,9 @@ export default {
this.requestCount = 0;
return {
timeFormat: '',
layers: [],
visibleLayers: [],
durationFormatter: undefined,
imageHistory: [],
timeSystem: timeSystem,
@ -323,12 +315,41 @@ export default {
displayThumbnailsSmall() {
return this.viewHeight > SHOW_THUMBS_THRESHOLD_HEIGHT && this.viewHeight <= SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT;
},
focusImageStyles() {
return {
'filter': `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
'background-image':
`${this.imageUrl ? (
`url(${this.imageUrl}),
repeating-linear-gradient(
45deg,
transparent,
transparent 4px,
rgba(125,125,125,.2) 4px,
rgba(125,125,125,.2) 8px
)`
) : ''}`,
'transform': `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
'transition': `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
'width': `${this.sizedImageWidth}px`,
'height': `${this.sizedImageHeight}px`
};
},
time() {
return this.formatTime(this.focusedImage);
},
imageUrl() {
return this.formatImageUrl(this.focusedImage);
},
imageWrapperStyle() {
return {
'cursor-zoom-in': this.cursorStates.showCursorZoomIn,
'cursor-zoom-out': this.cursorStates.showCursorZoomOut,
'pannable': this.cursorStates.isPannable,
'paused unnsynced': this.isPaused && !this.isFixed,
'stale': false
};
},
isImageNew() {
let cutoff = FIVE_MINUTES;
if (this.imageFreshnessOptions) {
@ -593,8 +614,10 @@ export default {
}
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
this.loadVisibleLayers();
},
beforeDestroy() {
this.persistVisibleLayers();
this.stopFollowingTimeContext();
if (this.thumbWrapperResizeObserver) {
@ -625,6 +648,13 @@ export default {
calculateViewHeight() {
this.viewHeight = this.$el.clientHeight;
},
getVisibleLayerStyles(layer) {
return {
'background-image': `url(${layer.source})`,
'transform': `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
'transition': `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`
};
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
@ -693,6 +723,37 @@ export default {
return mostRecent[valueKey];
},
loadVisibleLayers() {
const metaDataValues = this.metadata.valuesForHints(['image'])[0];
this.imageFormat = this.openmct.telemetry.getValueFormatter(metaDataValues);
let layersMetadata = metaDataValues.layers;
if (layersMetadata) {
this.layers = layersMetadata;
if (this.domainObject.configuration) {
let persistedLayers = this.domainObject.configuration.layers;
layersMetadata.forEach((layer) => {
const persistedLayer = persistedLayers.find(object => object.name === layer.name);
if (persistedLayer) {
layer.visible = persistedLayer.visible === true;
}
});
this.visibleLayers = this.layers.filter(layer => layer.visible);
} else {
this.visibleLayers = [];
this.layers.forEach((layer) => {
layer.visible = false;
});
}
}
},
persistVisibleLayers() {
if (this.domainObject.configuration) {
this.openmct.objects.mutate(this.domainObject, 'configuration.layers', this.layers);
}
this.visibleLayers = [];
this.layers = [];
},
// will subscribe to data for this key if not already done
subscribeToDataForKey(key) {
if (this.relatedTelemetry[key].isSubscribed) {
@ -1030,7 +1091,6 @@ export default {
this.resizingWindow = false;
});
},
// debounced method
clearWheelZoom() {
this.$refs.imageControls.clearWheelZoom();
},
@ -1093,6 +1153,11 @@ export default {
},
setCursorStates(states) {
this.cursorStates = states;
},
toggleLayerVisibility(index) {
let isVisible = this.layers[index].visible === true;
this.layers[index].visible = !isVisible;
this.visibleLayers = this.layers.filter(layer => layer.visible);
}
}
};

View File

@ -0,0 +1,65 @@
<template>
<div class="c-switcher-menu">
<button
:id="id"
class="c-button c-button--menu c-switcher-menu__button"
:class="iconClass"
:title="title"
@click="toggleMenu"
>
<span class="c-button__label"></span>
</button>
<div
v-show="showMenu"
class="c-switcher-menu__content"
>
<slot></slot>
</div>
</div>
</template>
<script>
import {v4 as uuid} from 'uuid';
export default {
inject: ['openmct'],
props: {
iconClass: {
type: String,
default() {
return '';
}
},
title: {
type: String,
default() {
return '';
}
}
},
data() {
return {
id: uuid(),
showMenu: false
};
},
mounted() {
document.addEventListener('click', this.hideMenu);
},
destroyed() {
document.removeEventListener('click', this.hideMenu);
},
methods: {
toggleMenu() {
this.showMenu = !this.showMenu;
},
hideMenu(e) {
if (this.id === e.target.id) {
return;
}
this.showMenu = false;
}
}
};
</script>

View File

@ -0,0 +1,59 @@
<template>
<div
class="c-control-menu c-menu--to-left c-menu--has-close-btn c-image-controls"
@click="handleClose"
>
<div class="c-checkbox-list js-checkbox-menu c-menu--to-left c-menu--has-close-btn">
<ul
@click="$event.stopPropagation()"
>
<li
v-for="(layer, index) in layers"
:key="index"
>
<input
v-if="layer.visible"
:id="index + 'LayerControl'"
checked
type="checkbox"
@change="toggleLayerVisibility(index)"
>
<input
v-else
:id="index + 'LayerControl'"
type="checkbox"
@change="toggleLayerVisibility(index)"
>
<label :for="index + 'LayerControl'">{{ layer.name }}</label>
</li>
</ul>
</div>
<button class="c-click-icon icon-x t-btn-close c-switcher-menu__close-button"></button>
</div>
</template>
<script>
export default {
inject: ['openmct'],
props: {
layers: {
type: Array,
default() {
return [];
}
}
},
methods: {
handleClose(e) {
const closeButton = e.target.classList.contains('c-switcher-menu__close-button');
if (!closeButton) {
e.stopPropagation();
}
},
toggleLayerVisibility(index) {
this.$emit('toggleLayerVisibility', index);
}
}
};
</script>

View File

@ -0,0 +1,89 @@
<template>
<div
class="c-image-controls__controls-wrapper"
@click="handleClose"
>
<div class="c-image-controls__control c-image-controls__zoom">
<div class="c-button-set c-button-set--strip-h">
<button
class="c-button t-btn-zoom-out icon-minus"
title="Zoom out"
@click="zoomOut"
></button>
<button
class="c-button t-btn-zoom-in icon-plus"
title="Zoom in"
@click="zoomIn"
></button>
<button
class="c-button t-btn-zoom-lock"
title="Lock current zoom and pan across all images"
:class="{'icon-unlocked': !panZoomLocked, 'icon-lock': panZoomLocked}"
@click="toggleZoomLock"
></button>
<button
class="c-button icon-reset t-btn-zoom-reset"
title="Remove zoom and pan"
@click="handleResetImage"
></button>
</div>
<div class="c-image-controls__zoom-factor">x{{ formattedZoomFactor }}</div>
</div>
<button
v-if="isMenu"
class="c-click-icon icon-x t-btn-close c-switcher-menu__close-button"
></button>
</div>
</template>
<script>
export default {
inject: ['openmct'],
props: {
zoomFactor: {
type: Number,
required: true
},
panZoomLocked: {
type: Boolean,
required: true
},
isMenu: {
type: Boolean,
required: false
}
},
data() {
return {
};
},
computed: {
formattedZoomFactor() {
return Number.parseFloat(this.zoomFactor).toPrecision(2);
}
},
methods: {
handleClose(e) {
const closeButton = e.target.classList.contains('c-switcher-menu__close-button');
if (!closeButton) {
e.stopPropagation();
}
},
handleResetImage() {
this.$emit('handleResetImage');
},
toggleZoomLock() {
this.$emit('toggleZoomLock');
},
zoomIn() {
this.$emit('zoomIn');
},
zoomOut() {
this.$emit('zoomOut');
}
}
};
</script>

View File

@ -28,6 +28,27 @@
display: flex;
flex-direction: column;
flex: 1 1 auto;
&.unnsynced{
@include sUnsynced();
}
&.cursor-zoom-in {
cursor: zoom-in;
}
&.cursor-zoom-out {
cursor: zoom-out;
}
&.pannable {
@include cursorGrab();
}
}
.image-wrapper {
overflow: visible clip;
background-image: repeating-linear-gradient(45deg, transparent, transparent 4px, rgba(125, 125, 125, 0.2) 4px, rgba(125, 125, 125, 0.2) 8px);
}
.image-wrapper {
@ -45,19 +66,6 @@
flex: 1 1 auto;
height: 0;
overflow: hidden;
&.unnsynced{
@include sUnsynced();
}
&.cursor-zoom-in {
cursor: zoom-in;
}
&.cursor-zoom-out {
cursor: zoom-out;
}
&.pannable {
@include cursorGrab();
}
}
&__background-image {
background-position: center;
@ -77,6 +85,7 @@
background: rgba(black, 0.2);
border-radius: $smallCr;
padding: 2px $interiorMargin;
pointer-events: none;
position: absolute;
right: $m;
top: $m;
@ -146,6 +155,11 @@
}
&__layer-image {
pointer-events: none;
z-index: 1;
}
&__thumbs-wrapper {
display: flex; // Uses row layout
justify-content: flex-end;
@ -179,6 +193,50 @@
font-size: 0.8em;
margin: $interiorMarginSm;
}
.c-control-menu {
// Controls on left of flex column layout, close btn on right
@include menuOuter();
border-radius: $controlCr;
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: $interiorMargin;
width: min-content;
> * + * {
margin-left: $interiorMargin;
}
}
.c-switcher-menu {
display: contents;
&__content {
// Menu panel
top: 28px;
position: absolute;
.c-so-view & {
top: 25px;
}
}
}
}
.--width-less-than-220 .--show-if-less-than-220.c-switcher-menu {
display: contents !important;
}
.s-image-layer {
position: absolute;
height: 100%;
width: 100%;
opacity: 0.5;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/*************************************** THUMBS */
@ -229,70 +287,36 @@
/*************************************** IMAGERY LOCAL CONTROLS*/
.c-imagery {
.h-local-controls--overlay-content {
display: flex;
flex-direction: row;
position: absolute;
left: $interiorMargin; top: $interiorMargin;
z-index: 70;
background: $colorLocalControlOvrBg;
border-radius: $basicCr;
max-width: 250px;
min-width: 170px;
width: 35%;
align-items: center;
padding: $interiorMargin $interiorMarginLg;
input[type="range"] {
display: block;
width: 100%;
&:not(:first-child) {
margin-top: $interiorMarginLg;
}
&:before {
margin-right: $interiorMarginSm;
}
}
padding: $interiorMargin $interiorMargin;
.s-status-taking-snapshot & {
display: none;
}
}
&__lc {
&__reset-btn {
// Span that holds bracket graphics and button
$bc: $scrollbarTrackColorBg;
&:before,
&:after {
border-right: 1px solid $bc;
content:'';
display: block;
width: 5px;
height: 4px;
}
&:before {
border-top: 1px solid $bc;
margin-bottom: 2px;
}
&:after {
border-bottom: 1px solid $bc;
margin-top: 2px;
}
.c-icon-link {
color: $colorBtnFg;
}
[class*='--menus-aligned'] {
> * + * {
button { margin-left: $interiorMarginSm; }
}
}
}
.c-image-controls {
&__controls-wrapper {
// Wraps __controls and __close-btn
display: flex;
}
&__controls {
display: flex;
align-items: stretch;
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
@ -314,31 +338,67 @@
}
&__input {
// A wrapper is needed to add the type icon to left of each control
input[type='range'] {
//width: 100%; // Do we need this?
}
}
&__zoom {
> * + * { margin-left: $interiorMargin; }
> * + * { margin-left: $interiorMargin; } // Is this used?
}
&__sliders {
display: flex;
flex: 1 1 auto;
flex-direction: column;
&--filters {
// Styles specific to the brightness and contrast controls
> * + * {
margin-top: 11px;
.c-image-controls {
&__sliders {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-width: 80px;
> * + * {
margin-top: 11px;
}
input[type="range"] {
display: block;
width: 100%;
}
}
&__slider-wrapper {
display: flex;
align-items: center;
&:before { margin-right: $interiorMargin; }
}
&__reset-btn {
// Span that holds bracket graphics and button
$bc: $scrollbarTrackColorBg;
flex: 0 0 auto;
&:before,
&:after {
border-right: 1px solid $bc;
content:'';
display: block;
width: 5px;
height: 4px;
}
&:before {
border-top: 1px solid $bc;
margin-bottom: 2px;
}
&:after {
border-bottom: 1px solid $bc;
margin-top: 2px;
}
.c-icon-link {
color: $colorBtnFg;
}
}
}
}
&__btn-reset {
flex: 0 0 auto;
}
}
/*************************************** BUTTONS */
@ -383,7 +443,7 @@
@include cArrowButtonSizing($dimOuter: 48px);
border-radius: $controlCr;
.is-in-small-container & {
.--width-less-than-600 & {
@include cArrowButtonSizing($dimOuter: 32px);
}
}
@ -409,10 +469,6 @@
background-color: $colorBodyFg;
}
//[class*='__image-placeholder'] {
// display: none;
//}
img {
display: block !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -100,12 +100,24 @@ describe("The Imagery View Layouts", () => {
location: "parentId",
modified: 0,
persisted: 0,
configuration: {
layers: [{
name: '16:9',
visible: true
}]
},
telemetry: {
values: [
{
"name": "Image",
"key": "url",
"format": "image",
"layers": [
{
source: location.host + '/images/bg-splash.jpg',
name: '16:9'
}
],
"hints": {
"image": 1,
"priority": 3
@ -366,6 +378,18 @@ describe("The Imagery View Layouts", () => {
});
});
it("on mount should show the any image layers", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick().then(() => {
Vue.nextTick(() => {
const layerEls = parent.querySelectorAll('.js-layer-image');
console.log(layerEls);
expect(layerEls.length).toEqual(1);
done();
});
});
});
it("should show the clicked thumbnail as the main image", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick(() => {

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