From 35c42ba43de5f1ee5348d686e8fd4d498ec42d00 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Wed, 28 Sep 2022 17:13:41 -0700 Subject: [PATCH 001/594] Reviewer to smoke test PRs before merge (#5771) * Reviewer to smoke test PRs before merge * Update PULL_REQUEST_TEMPLATE.md * Update PULL_REQUEST_TEMPLATE.md * Update PULL_REQUEST_TEMPLATE.md Changes based on feedback * Update PULL_REQUEST_TEMPLATE.md Tweaking the language slightly for clarity. * Update PULL_REQUEST_TEMPLATE.md Co-authored-by: Jesse Mazzella --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a5525f4ce9..f6728a7d26 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,9 +21,9 @@ Closes -# Building Applications With Open MCT +# Developing Applications With Open MCT ## Scope and purpose of this document @@ -72,8 +72,7 @@ MCT, as well as addressing some common developer use cases. ## Building From Source The latest version of Open MCT is available from [our GitHub repository](https://github.com/nasa/openmct). -If you have `git`, and `node` installed, you can build Open MCT with the -commands +If you have `git`, and `node` installed, you can build Open MCT with the commands ```bash git clone https://github.com/nasa/openmct.git @@ -86,7 +85,7 @@ build a minified version that can be included in your application. The output of the build process is placed in a `dist` folder under the openmct source directory, which can be copied out to another location as needed. The contents of this folder will include a minified javascript file named `openmct.js` as -well as assets such as html, css, and images necessary for the UI. +well as assets such as html, css, and images necessary for the UI. ## Starting an Open MCT application diff --git a/Procfile b/Procfile deleted file mode 100644 index 1e13b4ae05..0000000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: node app.js --port $PORT diff --git a/README.md b/README.md index dc685fac11..463961d333 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Building and running Open MCT in your local dev environment is very easy. Be sur Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/) +Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/). + ## Documentation Documentation is available on the [Open MCT website](https://nasa.github.io/openmct/documentation/). @@ -43,11 +45,9 @@ our documentation. We want Open MCT to be as easy to use, install, run, and develop for as possible, and your feedback will help us get there! Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose), [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions), or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov). -## Building Applications With Open MCT +## Developing Applications With Open MCT -Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/). - -See our documentation for a guide on [building Applications with Open MCT](https://github.com/nasa/openmct/blob/master/API.md#starting-an-open-mct-application). +For more on developing with Open MCT, see our documentation for a guide on [Developing Applications with Open MCT](./API.md#starting-an-open-mct-application). ## Compatibility @@ -64,7 +64,7 @@ that is intended to be added or removed as a single unit. As well as providing an extension mechanism, most of the core Open MCT codebase is also written as plugins. -For information on writing plugins, please see [our API documentation](https://github.com/nasa/openmct/blob/master/API.md#plugins). +For information on writing plugins, please see [our API documentation](./API.md#plugins). ## Tests diff --git a/app.js b/app.js deleted file mode 100644 index c7ecd9de3b..0000000000 --- a/app.js +++ /dev/null @@ -1,92 +0,0 @@ -/*global process*/ - -/** - * Usage: - * - * npm install minimist express - * node app.js [options] - */ - -const options = require('minimist')(process.argv.slice(2)); -const express = require('express'); -const app = express(); -const fs = require('fs'); -const request = require('request'); -const __DEV__ = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; - -// Defaults -options.port = options.port || options.p || 8080; -options.host = options.host || 'localhost'; -options.directory = options.directory || options.D || '.'; - -// Show command line options -if (options.help || options.h) { - console.log("\nUsage: node app.js [options]\n"); - console.log("Options:"); - console.log(" --help, -h Show this message."); - console.log(" --port, -p Specify port."); - console.log(" --directory, -D Serve files from specified directory."); - console.log(""); - process.exit(0); -} - -app.disable('x-powered-by'); - -app.use('/proxyUrl', function proxyRequest(req, res, next) { - console.log('Proxying request to: ', req.query.url); - req.pipe(request({ - url: req.query.url, - strictSSL: false - }).on('error', next)).pipe(res); -}); - -class WatchRunPlugin { - apply(compiler) { - compiler.hooks.emit.tapAsync('WatchRunPlugin', (compilation, callback) => { - console.log('Begin compile at ' + new Date()); - callback(); - }); - } -} - -const webpack = require('webpack'); -let webpackConfig; -if (__DEV__) { - webpackConfig = require('./webpack.dev'); - webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); - webpackConfig.entry.openmct = [ - 'webpack-hot-middleware/client?reload=true', - webpackConfig.entry.openmct - ]; - webpackConfig.plugins.push(new WatchRunPlugin()); -} else { - webpackConfig = require('./webpack.coverage'); -} - -const compiler = webpack(webpackConfig); - -app.use(require('webpack-dev-middleware')( - compiler, - { - publicPath: '/dist', - stats: 'errors-warnings' - } -)); - -if (__DEV__) { - app.use(require('webpack-hot-middleware')( - compiler, - {} - )); -} - -// Expose index.html for development users. -app.get('/', function (req, res) { - fs.createReadStream('index.html').pipe(res); -}); - -// Finally, open the HTTP server and log the instance to the console -app.listen(options.port, options.host, function () { - console.log('Open MCT application running at %s:%s', options.host, options.port); -}); - diff --git a/docs/footer.html b/docs/footer.html deleted file mode 100644 index a6878b0cac..0000000000 --- a/docs/footer.html +++ /dev/null @@ -1,3 +0,0 @@ -
- - diff --git a/docs/gendocs.js b/docs/gendocs.js deleted file mode 100644 index a5f3188e92..0000000000 --- a/docs/gendocs.js +++ /dev/null @@ -1,209 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2022, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -/*global require,process,__dirname,GLOBAL*/ -/*jslint nomen: false */ - - -// Usage: -// node gendocs.js --in --out - -var CONSTANTS = { - DIAGRAM_WIDTH: 800, - DIAGRAM_HEIGHT: 500 - }, - TOC_HEAD = "# Table of Contents"; - -GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined -(function () { - "use strict"; - - var fs = require("fs"), - mkdirp = require("mkdirp"), - path = require("path"), - glob = require("glob"), - marked = require("marked"), - split = require("split"), - stream = require("stream"), - nomnoml = require('nomnoml'), - toc = require("markdown-toc"), - Canvas = require('canvas'), - header = fs.readFileSync(path.resolve(__dirname, 'header.html')), - footer = fs.readFileSync(path.resolve(__dirname, 'footer.html')), - options = require("minimist")(process.argv.slice(2)); - - // Convert from nomnoml source to a target PNG file. - function renderNomnoml(source, target) { - var canvas = - new Canvas(CONSTANTS.DIAGRAM_WIDTH, CONSTANTS.DIAGRAM_HEIGHT); - nomnoml.draw(canvas, source, 1.0); - canvas.pngStream().pipe(fs.createWriteStream(target)); - } - - // Stream transform. - // Pulls out nomnoml diagrams from fenced code blocks and renders them - // as PNG files in the output directory, prefixed with a provided name. - // The fenced code blocks will be replaced with Markdown in the - // output of this stream. - function nomnomlifier(outputDirectory, prefix) { - var transform = new stream.Transform({ objectMode: true }), - isBuilding = false, - counter = 1, - outputPath, - source = ""; - - transform._transform = function (chunk, encoding, done) { - if (!isBuilding) { - if (chunk.trim().indexOf("```nomnoml") === 0) { - var outputFilename = prefix + '-' + counter + '.png'; - outputPath = path.join(outputDirectory, outputFilename); - this.push([ - "\n![Diagram ", - counter, - "](", - outputFilename, - ")\n\n" - ].join("")); - isBuilding = true; - source = ""; - counter += 1; - } else { - // Otherwise, pass through - this.push(chunk + '\n'); - } - } else { - if (chunk.trim() === "```") { - // End nomnoml - renderNomnoml(source, outputPath); - isBuilding = false; - } else { - source += chunk + '\n'; - } - } - done(); - }; - - return transform; - } - - // Convert from Github-flavored Markdown to HTML - function gfmifier(renderTOC) { - var transform = new stream.Transform({ objectMode: true }), - markdown = ""; - transform._transform = function (chunk, encoding, done) { - markdown += chunk; - done(); - }; - transform._flush = function (done) { - if (renderTOC){ - // Prepend table of contents - markdown = - [ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n"); - } - this.push(header); - this.push(marked(markdown)); - this.push(footer); - done(); - }; - return transform; - } - - // Custom renderer for marked; converts relative links from md to html, - // and makes headings linkable. - function CustomRenderer() { - var renderer = new marked.Renderer(), - customRenderer = Object.create(renderer); - customRenderer.heading = function (text, level) { - var escapedText = (text || "").trim().toLowerCase().replace(/\W/g, "-"), - aOpen = "", - aClose = ""; - return aOpen + renderer.heading.apply(renderer, arguments) + aClose; - }; - // Change links to .md files to .html - customRenderer.link = function (href, title, text) { - // ...but only if they look like relative paths - return (href || "").indexOf(":") === -1 && href[0] !== "/" ? - renderer.link(href.replace(/\.md/, ".html"), title, text) : - renderer.link.apply(renderer, arguments); - }; - return customRenderer; - } - - options['in'] = options['in'] || options.i; - options.out = options.out || options.o; - - marked.setOptions({ - renderer: new CustomRenderer(), - gfm: true, - tables: true, - breaks: false, - pedantic: false, - sanitize: true, - smartLists: true, - smartypants: false - }); - - // Convert all markdown files. - // First, pull out nomnoml diagrams. - // Then, convert remaining Markdown to HTML. - glob(options['in'] + "/**/*.md", {}, function (err, files) { - files.forEach(function (file) { - var destination = file.replace(options['in'], options.out) - .replace(/md$/, "html"), - destPath = path.dirname(destination), - prefix = path.basename(destination).replace(/\.html$/, ""), - //Determine whether TOC should be rendered for this file based - //on regex provided as command line option - renderTOC = file.match(options['suppress-toc'] || "") === null; - - mkdirp(destPath, function (err) { - fs.createReadStream(file, { encoding: 'utf8' }) - .pipe(split()) - .pipe(nomnomlifier(destPath, prefix)) - .pipe(gfmifier(renderTOC)) - .pipe(fs.createWriteStream(destination, { - encoding: 'utf8' - })); - }); - }); - }); - - // Also copy over all HTML, CSS, or PNG files - glob(options['in'] + "/**/*.@(html|css|png)", {}, function (err, files) { - files.forEach(function (file) { - var destination = file.replace(options['in'], options.out), - destPath = path.dirname(destination), - streamOptions = {}; - if (file.match(/png$/)) { - streamOptions.encoding = null; - } else { - streamOptions.encoding = 'utf8'; - } - - mkdirp(destPath, function (err) { - fs.createReadStream(file, streamOptions) - .pipe(fs.createWriteStream(destination, streamOptions)); - }); - }); - }); - -}()); diff --git a/docs/header.html b/docs/header.html deleted file mode 100644 index c945996f4a..0000000000 --- a/docs/header.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/e2e/README.md b/e2e/README.md index 2ce8727107..6cfd604a20 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -151,7 +151,7 @@ Current list of test tags: - `@ipad` - Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no Create button). - `@gds` - Denotes a GDS Test Case used in the VIPER Mission. -- `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of app.js. +- `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`. - `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). - `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container. - `@unstable` - A new test or test which is known to be flaky. diff --git a/e2e/playwright-ci.config.js b/e2e/playwright-ci.config.js index aeff668823..cd8fd2e7e9 100644 --- a/e2e/playwright-ci.config.js +++ b/e2e/playwright-ci.config.js @@ -14,7 +14,7 @@ const config = { testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js timeout: 60 * 1000, webServer: { - command: 'cross-env NODE_ENV=test npm run start', + command: 'npm run start:coverage', url: 'http://localhost:8080/#', timeout: 200 * 1000, reuseExistingServer: false diff --git a/e2e/playwright-local.config.js b/e2e/playwright-local.config.js index 87365fee2b..845cf68124 100644 --- a/e2e/playwright-local.config.js +++ b/e2e/playwright-local.config.js @@ -12,10 +12,7 @@ const config = { testIgnore: '**/*.perf.spec.js', timeout: 30 * 1000, webServer: { - env: { - NODE_ENV: 'test' - }, - command: 'npm run start', + command: 'npm run start:coverage', url: 'http://localhost:8080/#', timeout: 120 * 1000, reuseExistingServer: true diff --git a/e2e/playwright-performance.config.js b/e2e/playwright-performance.config.js index de79304f11..7f4fae91c6 100644 --- a/e2e/playwright-performance.config.js +++ b/e2e/playwright-performance.config.js @@ -6,12 +6,12 @@ const CI = process.env.CI === 'true'; /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { - retries: 1, //Only for debugging purposes because trace is enabled only on first retry + retries: 1, //Only for debugging purposes for trace: 'on-first-retry' testDir: 'tests/performance/', timeout: 60 * 1000, workers: 1, //Only run in serial with 1 worker webServer: { - command: 'cross-env NODE_ENV=test npm run start', + command: 'npm run start', //coverage not generated url: 'http://localhost:8080/#', timeout: 200 * 1000, reuseExistingServer: !CI diff --git a/e2e/playwright-visual.config.js b/e2e/playwright-visual.config.js index 1123de8087..34bc13d390 100644 --- a/e2e/playwright-visual.config.js +++ b/e2e/playwright-visual.config.js @@ -4,13 +4,13 @@ /** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */ const config = { - retries: 1, // visual tests should never retry due to snapshot comparison errors. Leaving as a shim + retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim testDir: 'tests/visual', testMatch: '**/*.visual.spec.js', // only run visual tests timeout: 60 * 1000, workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067 webServer: { - command: 'cross-env NODE_ENV=test npm run start', + command: 'npm run start:coverage', url: 'http://localhost:8080/#', timeout: 200 * 1000, reuseExistingServer: !process.env.CI @@ -31,7 +31,7 @@ const config = { } }, { - name: 'chrome-snow-theme', + name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled use: { browserName: 'chromium', theme: 'snow' diff --git a/e2e/tests/framework/baseFixtures.e2e.spec.js b/e2e/tests/framework/baseFixtures.e2e.spec.js index 86ae7c7d30..aa5bf744e5 100644 --- a/e2e/tests/framework/baseFixtures.e2e.spec.js +++ b/e2e/tests/framework/baseFixtures.e2e.spec.js @@ -23,7 +23,7 @@ /* This test suite is dedicated to testing our use of the playwright framework as it relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions made in our dev environment -(app.js and ./e2e/webpack-dev-middleware.js) +(`npm start` and ./e2e/webpack-dev-middleware.js) */ const { test } = require('../../baseFixtures.js'); diff --git a/e2e/tests/visual/controlledClock.visual.spec.js b/e2e/tests/visual/controlledClock.visual.spec.js index c4d6b57679..0f5de10c63 100644 --- a/e2e/tests/visual/controlledClock.visual.spec.js +++ b/e2e/tests/visual/controlledClock.visual.spec.js @@ -22,7 +22,7 @@ /* Collection of Visual Tests set to run in a default context. The tests within this suite -are only meant to run against openmct's app.js started by `npm run start` within the +are only meant to run against openmct started by `npm start` within the `./e2e/playwright-visual.config.js` file. */ diff --git a/e2e/tests/visual/default.visual.spec.js b/e2e/tests/visual/default.visual.spec.js index b8c10b35f7..0a2a880055 100644 --- a/e2e/tests/visual/default.visual.spec.js +++ b/e2e/tests/visual/default.visual.spec.js @@ -22,7 +22,7 @@ /* Collection of Visual Tests set to run in a default context. The tests within this suite -are only meant to run against openmct's app.js started by `npm run start` within the +are only meant to run against openmct started by `npm start` within the `./e2e/playwright-visual.config.js` file. These should only use functional expect statements to verify assumptions about the state diff --git a/jsdoc.json b/jsdoc.json deleted file mode 100644 index ac485a5efa..0000000000 --- a/jsdoc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "source": { - "include": [ - "src/" - ], - "includePattern": "src/.+\\.js$", - "excludePattern": ".+\\Spec\\.js$|lib/.+" - }, - "plugins": [ - "plugins/markdown" - ] -} diff --git a/karma.conf.js b/karma.conf.js index a24f6d0f31..75a9c04fc5 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -23,14 +23,32 @@ /*global module,process*/ module.exports = (config) => { - const webpackConfig = require('./webpack.coverage.js'); + let webpackConfig; + let browsers; + let singleRun; + + if (process.env.KARMA_DEBUG) { + webpackConfig = require('./webpack.dev.js'); + browsers = ['ChromeDebugging']; + singleRun = false; + } else { + webpackConfig = require('./webpack.coverage.js'); + browsers = ['ChromeHeadless']; + singleRun = true; + } + delete webpackConfig.output; + // karma doesn't support webpack entry + delete webpackConfig.entry; config.set({ basePath: '', - frameworks: ['jasmine'], + frameworks: ['jasmine', 'webpack'], files: [ 'indexTest.js', + // included means: should the files be included in the browser using + + diff --git a/src/ui/layout/RecentObjectsListItem.vue b/src/ui/layout/RecentObjectsListItem.vue new file mode 100644 index 0000000000..c7b651e9e2 --- /dev/null +++ b/src/ui/layout/RecentObjectsListItem.vue @@ -0,0 +1,134 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + + + + diff --git a/src/ui/layout/layout.scss b/src/ui/layout/layout.scss index f9dc4d8c15..c2c10d8cb2 100644 --- a/src/ui/layout/layout.scss +++ b/src/ui/layout/layout.scss @@ -289,7 +289,7 @@ } &__pane-tree { - width: 300px; + width: 100%; padding-left: nth($shellPanePad, 2); } diff --git a/src/ui/layout/mct-tree.scss b/src/ui/layout/mct-tree.scss index 3dadf18c83..f782898203 100644 --- a/src/ui/layout/mct-tree.scss +++ b/src/ui/layout/mct-tree.scss @@ -108,6 +108,10 @@ color: $colorItemTreeSelectedFg; } } + &.is-targeted-item { + $c: $colorBodyFg; + @include pulseProp($animName: flashTarget, $dur: 500ms, $iter: 8, $prop: background, $valStart: rgba($c, 0.4), $valEnd: rgba($c, 0)); + } &.is-new { animation-name: animTemporaryHighlight; diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue index 9c2151d58a..a59497a56a 100644 --- a/src/ui/layout/mct-tree.vue +++ b/src/ui/layout/mct-tree.vue @@ -88,10 +88,12 @@ :item-height="itemHeight" :open-items="openTreeItems" :loading-items="treeItemLoading" + :targeted-path="targetedPath" @tree-item-mounted="scrollToCheck($event)" @tree-item-destroyed="removeCompositionListenerFor($event)" @tree-item-action="treeItemAction(treeItem, $event)" @tree-item-selection="treeItemSelection(treeItem)" + @targeted-path-animation-end="targetedPathAnimationEnd()" />
item.navigationPath === navigationPath); const scrollTopAmount = indexOfScroll * this.itemHeight; diff --git a/src/ui/layout/pane.scss b/src/ui/layout/pane.scss index b830f24aec..1a1c900e26 100644 --- a/src/ui/layout/pane.scss +++ b/src/ui/layout/pane.scss @@ -83,6 +83,8 @@ &[class*="--vertical"] { padding-top: $interiorMargin; padding-bottom: $interiorMargin; + min-height: 30px; // For Recents holder + &.l-pane--collapsed { padding-top: 0 !important; padding-bottom: 0 !important; diff --git a/src/ui/layout/pane.vue b/src/ui/layout/pane.vue index 75c4353ced..4126e89557 100644 --- a/src/ui/layout/pane.vue +++ b/src/ui/layout/pane.vue @@ -1,20 +1,12 @@ diff --git a/src/ui/components/tags/tags.scss b/src/ui/components/tags/tags.scss index ebd3e7a184..964b2361ab 100644 --- a/src/ui/components/tags/tags.scss +++ b/src/ui/components/tags/tags.scss @@ -54,6 +54,10 @@ } } +.c-tag-btn__label { + overflow: visible!important; +} + /******************************* HOVERS */ .has-tag-applier { // Apply this class to all components that should trigger tag removal btn on hover From 4d84b16d8be1ddb614bedddec99781b5f85b9b54 Mon Sep 17 00:00:00 2001 From: Jamie V Date: Fri, 20 Jan 2023 18:27:19 -0800 Subject: [PATCH 113/594] [Notebook] Convert full links in entries, into clickable links (#6090) * Automatically promote urls to hyperlinks if matches whitelist * Disable v-html lint warning for notebook entries * Check whether domain endswith given partial Co-authored-by: Jesse Mazzella Co-authored-by: Andrew Henry --- .../plugins/notebook/notebook.e2e.spec.js | 73 +++++++++++++++++++ .../notebook/notebookWithCouchDB.e2e.spec.js | 4 + package.json | 1 + src/plugins/notebook/NotebookViewProvider.js | 7 +- .../notebook/components/NotebookEntry.vue | 54 +++++++++++++- src/plugins/notebook/plugin.js | 12 +-- 6 files changed, 139 insertions(+), 12 deletions(-) diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index 66c1b038c0..8d867e7517 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -263,4 +263,77 @@ test.describe('Notebook entry tests', () => { }); test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); test.fixme('previous and new entries can be deleted', async ({ page }) => {}); + test.fixme('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { + const TEST_LINK = 'http://www.google.com'; + await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); + + // Create Notebook + const notebook = await createDomainObjectWithDefaults(page, { + type: 'Notebook', + name: "Entry Link Test" + }); + + await expandTreePaneItemByName(page, 'My Items'); + + await page.goto(notebook.url); + + await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); + + const validLink = page.locator(`a[href="${TEST_LINK}"]`); + + // Start waiting for popup before clicking. Note no await. + const popupPromise = page.waitForEvent('popup'); + + await validLink.click(); + const popup = await popupPromise; + + // Wait for the popup to load. + await popup.waitForLoadState(); + expect.soft(popup.url()).toContain('www.google.com'); + + expect(await validLink.count()).toBe(1); + }); + test.fixme('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => { + const TEST_LINK = 'www.google.com'; + await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); + + // Create Notebook + const notebook = await createDomainObjectWithDefaults(page, { + type: 'Notebook', + name: "Entry Link Test" + }); + + await expandTreePaneItemByName(page, 'My Items'); + + await page.goto(notebook.url); + + await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); + + const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); + + expect(await invalidLink.count()).toBe(0); + }); + test.fixme('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ page }) => { + const TEST_LINK = 'http://www.google.com?bad='; + const TEST_LINK_BAD = `http://www.google.com?bad=`; + await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); + + // Create Notebook + const notebook = await createDomainObjectWithDefaults(page, { + type: 'Notebook', + name: "Entry Link Test" + }); + + await expandTreePaneItemByName(page, 'My Items'); + + await page.goto(notebook.url); + + await nbUtils.enterTextEntry(page, `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`); + + const sanitizedLink = page.locator(`a[href="${TEST_LINK}"]`); + const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`); + + expect.soft(await sanitizedLink.count()).toBe(1); + expect(await unsanitizedLink.count()).toBe(0); + }); }); diff --git a/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js index 87a352797d..9c01100472 100644 --- a/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js @@ -76,6 +76,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { await page.locator('text=To start a new entry, click here or drag and drop any object').click(); await page.locator('[aria-label="Notebook Entry Input"]').click(); await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`); + await page.locator('[aria-label="Notebook Entry Input"]').press('Enter'); await page.waitForLoadState('networkidle'); expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2); @@ -148,14 +149,17 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { await page.locator('text=To start a new entry, click here or drag and drop any object').click(); await page.locator('[aria-label="Notebook Entry Input"]').click(); await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`); + await page.locator('[aria-label="Notebook Entry Input"]').press('Enter'); await page.locator('text=To start a new entry, click here or drag and drop any object').click(); await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').click(); await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').fill(`Second Entry`); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter'); await page.locator('text=To start a new entry, click here or drag and drop any object').click(); await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').click(); await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').fill(`Third Entry`); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').press('Enter'); // Add three tags await page.hover(`button:has-text("Add Tag") >> nth=2`); diff --git a/package.json b/package.json index 23731bb152..53d773c28d 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "plotly.js-gl2d-dist": "2.17.1", "printj": "1.3.1", "resolve-url-loader": "5.0.0", + "sanitize-html": "2.8.1", "sass": "1.57.1", "sass-loader": "13.2.0", "sinon": "15.0.1", diff --git a/src/plugins/notebook/NotebookViewProvider.js b/src/plugins/notebook/NotebookViewProvider.js index 66617789c7..8bcaf1ad82 100644 --- a/src/plugins/notebook/NotebookViewProvider.js +++ b/src/plugins/notebook/NotebookViewProvider.js @@ -25,13 +25,14 @@ import Notebook from './components/Notebook.vue'; import Agent from '@/utils/agent/Agent'; export default class NotebookViewProvider { - constructor(openmct, name, key, type, cssClass, snapshotContainer) { + constructor(openmct, name, key, type, cssClass, snapshotContainer, entryUrlWhitelist) { this.openmct = openmct; this.key = key; this.name = `${name} View`; this.type = type; this.cssClass = cssClass; this.snapshotContainer = snapshotContainer; + this.entryUrlWhitelist = entryUrlWhitelist; } canView(domainObject) { @@ -43,6 +44,7 @@ export default class NotebookViewProvider { let openmct = this.openmct; let snapshotContainer = this.snapshotContainer; let agent = new Agent(window); + let entryUrlWhitelist = this.entryUrlWhitelist; return { show(container) { @@ -54,7 +56,8 @@ export default class NotebookViewProvider { provide: { openmct, snapshotContainer, - agent + agent, + entryUrlWhitelist }, data() { return { diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index 932b45a3c4..8d1c3a15b0 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -1,3 +1,4 @@ + /***************************************************************************** * Open MCT, Copyright (c) 2014-2022, United States Government * as represented by the Administrator of the National Aeronautics and Space @@ -75,12 +76,14 @@ class="c-ne__text c-ne__input" aria-label="Notebook Entry Input" tabindex="0" - contenteditable="true" + :contenteditable="canEdit" + @mouseover="checkEditability($event)" + @mouseleave="canEdit = true" @focus="editingEntry()" @blur="updateEntryValue($event)" @keydown.enter.exact.prevent @keyup.enter.exact.prevent="forceBlur($event)" - v-text="entry.text" + v-html="formattedText" >
@@ -91,7 +94,7 @@ class="c-ne__text" contenteditable="false" tabindex="0" - v-text="entry.text" + v-html="formattedText" > @@ -156,10 +159,16 @@ import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue'; import { createNewEmbed } from '../utils/notebook-entries'; import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image'; +import sanitizeHtml from 'sanitize-html'; import _ from 'lodash'; import Moment from 'moment'; +const SANITIZATION_SCHEMA = { + allowedTags: [], + allowedAttributes: {} +}; +const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g; const UNKNOWN_USER = 'Unknown'; export default { @@ -167,7 +176,7 @@ export default { NotebookEmbed, TextHighlight }, - inject: ['openmct', 'snapshotContainer'], + inject: ['openmct', 'snapshotContainer', 'entryUrlWhitelist'], props: { domainObject: { type: Object, @@ -224,6 +233,8 @@ export default { }, data() { return { + editMode: false, + canEdit: true, enableEmbedsWrapperScroll: false }; }, @@ -234,6 +245,31 @@ export default { createdOnTime() { return this.formatTime(this.entry.createdOn, 'HH:mm:ss'); }, + formattedText() { + // remove ANY tags + let text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA); + + if (this.editMode || !this.urlWhitelist) { + return text; + } + + text = text.replace(URL_REGEX, (match) => { + const url = new URL(match); + const domain = url.hostname; + let result = match; + let isMatch = this.urlWhitelist.find((partialDomain) => { + return domain.endsWith(partialDomain); + }); + + if (isMatch) { + result = `${match}`; + } + + return result; + }); + + return text; + }, isSelectedEntry() { return this.selectedEntryId === this.entry.id; }, @@ -271,6 +307,9 @@ export default { this.manageEmbedLayout(); this.dropOnEntry = this.dropOnEntry.bind(this); + if (this.entryUrlWhitelist?.length > 0) { + this.urlWhitelist = this.entryUrlWhitelist; + } }, beforeDestroy() { if (this.embedsWrapperResizeObserver) { @@ -307,6 +346,11 @@ export default { event.dataTransfer.effectAllowed = 'none'; } }, + checkEditability($event) { + if ($event.target.nodeName === 'A') { + this.canEdit = false; + } + }, deleteEntry() { this.$emit('deleteEntry', this.entry.id); }, @@ -405,9 +449,11 @@ export default { this.$emit('updateEntry', this.entry); }, editingEntry() { + this.editMode = true; this.$emit('editingEntry'); }, updateEntryValue($event) { + this.editMode = false; const value = $event.target.innerText; if (value !== this.entry.text && value.match(/\S/)) { this.entry.text = value; diff --git a/src/plugins/notebook/plugin.js b/src/plugins/notebook/plugin.js index 7fcabbe747..f24742644b 100644 --- a/src/plugins/notebook/plugin.js +++ b/src/plugins/notebook/plugin.js @@ -103,7 +103,7 @@ function installBaseNotebookFunctionality(openmct) { monkeyPatchObjectAPIForNotebooks(openmct); } -function NotebookPlugin(name = 'Notebook') { +function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) { return function install(openmct) { if (openmct[NOTEBOOK_INSTALLED_KEY]) { return; @@ -118,8 +118,8 @@ function NotebookPlugin(name = 'Notebook') { const notebookType = new NotebookType(name, description, icon); openmct.types.addType(NOTEBOOK_TYPE, notebookType); - const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer); - openmct.objectViews.addProvider(notebookView); + const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist); + openmct.objectViews.addProvider(notebookView, entryUrlWhitelist); installBaseNotebookFunctionality(openmct); @@ -127,7 +127,7 @@ function NotebookPlugin(name = 'Notebook') { }; } -function RestrictedNotebookPlugin(name = 'Notebook Shift Log') { +function RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist = []) { return function install(openmct) { if (openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) { return; @@ -140,8 +140,8 @@ function RestrictedNotebookPlugin(name = 'Notebook Shift Log') { const notebookType = new NotebookType(name, description, icon); openmct.types.addType(RESTRICTED_NOTEBOOK_TYPE, notebookType); - const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer); - openmct.objectViews.addProvider(notebookView); + const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist); + openmct.objectViews.addProvider(notebookView, entryUrlWhitelist); installBaseNotebookFunctionality(openmct); From 986c596d90da013172232ec5bccb7284d909259e Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Fri, 20 Jan 2023 23:21:57 -0800 Subject: [PATCH 114/594] Imagery compass rose enhancements (#6140) * Fixes 6139 - Markup changes and improvements in CompassRose.vue. - Improved sun and edge gradients. - Related CSS styles updated. - Changed compass key color from cyan to white to avoid conflict with staleness color. * change var def to avoid collision * compass rose should size itself based on image * allow heading or camera pan for fixed cameras * suppress HUD if no camera pan * allow image to display compass rose for other cams * update example imagery to accept transformations * remove comments Co-authored-by: David Tsay Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com> --- example/imagery/plugin.js | 8 ++ .../imagery/components/Compass/Compass.vue | 31 +++-- .../components/Compass/CompassRose.vue | 119 +++++++++++++----- .../imagery/components/Compass/compass.scss | 23 +++- .../imagery/components/ImageryView.vue | 35 +++--- 5 files changed, 152 insertions(+), 64 deletions(-) diff --git a/example/imagery/plugin.js b/example/imagery/plugin.js index 2f323356dd..e193c99afd 100644 --- a/example/imagery/plugin.js +++ b/example/imagery/plugin.js @@ -242,6 +242,13 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) { const url = imageSamples[Math.floor(timestamp / delay) % imageSamples.length]; const urlItems = url.split('/'); const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`; + const navCamTransformations = { + "translateX": 0, + "translateY": 18, + "rotation": 0, + "scale": 0.3, + "cameraAngleOfView": 70 + }; return { name, @@ -251,6 +258,7 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) { sunOrientation: getCompassValues(0, 360), cameraPan: getCompassValues(0, 360), heading: getCompassValues(0, 360), + transformations: navCamTransformations, imageDownloadName }; } diff --git a/src/plugins/imagery/components/Compass/Compass.vue b/src/plugins/imagery/components/Compass/Compass.vue index 850cd3c7d2..15d2f65090 100644 --- a/src/plugins/imagery/components/Compass/Compass.vue +++ b/src/plugins/imagery/components/Compass/Compass.vue @@ -26,19 +26,18 @@ :style="`width: 100%; height: 100%`" > @@ -47,18 +46,12 @@ import CompassHUD from './CompassHUD.vue'; import CompassRose from './CompassRose.vue'; -const CAMERA_ANGLE_OF_VIEW = 70; - export default { components: { CompassHUD, CompassRose }, props: { - compassRoseSizingClasses: { - type: String, - required: true - }, image: { type: Object, required: true @@ -69,13 +62,19 @@ export default { } }, computed: { - hasCameraFieldOfView() { - return this.cameraPan !== undefined && this.cameraAngleOfView > 0; + showCompassHUD() { + return this.hasCameraPan && this.cameraAngleOfView > 0; + }, + showCompassRose() { + return (this.hasCameraPan || this.hasHeading) && this.cameraAngleOfView > 0; }, // horizontal rotation from north in degrees heading() { return this.image.heading; }, + hasHeading() { + return this.heading !== undefined; + }, // horizontal rotation from north in degrees sunHeading() { return this.image.sunOrientation; @@ -84,8 +83,14 @@ export default { cameraPan() { return this.image.cameraPan; }, + hasCameraPan() { + return this.cameraPan !== undefined; + }, cameraAngleOfView() { - return CAMERA_ANGLE_OF_VIEW; + return this.transformations?.cameraAngleOfView; + }, + transformations() { + return this.image.transformations; } }, methods: { diff --git a/src/plugins/imagery/components/Compass/CompassRose.vue b/src/plugins/imagery/components/Compass/CompassRose.vue index d66e382a4a..958bd3779b 100644 --- a/src/plugins/imagery/components/Compass/CompassRose.vue +++ b/src/plugins/imagery/components/Compass/CompassRose.vue @@ -64,14 +64,14 @@ class="c-cr__edge" width="100" height="100" - fill="url(#paint0_radial)" + fill="url(#gradient_edge)" /> @@ -107,9 +107,26 @@ height="100" /> + + + + + + + + - - - + @@ -238,10 +254,6 @@ import { throttle } from 'lodash'; export default { props: { - compassRoseSizingClasses: { - type: String, - required: true - }, heading: { type: Number, required: true, @@ -253,16 +265,13 @@ export default { type: Number, default: undefined }, - cameraAngleOfView: { + cameraPan: { type: Number, default: undefined }, - cameraPan: { - type: Number, - required: true, - default() { - return 0; - } + transformations: { + type: Object, + default: undefined }, sizedImageDimensions: { type: Object, @@ -275,11 +284,38 @@ export default { }; }, computed: { + cameraHeading() { + return this.cameraPan ?? this.heading; + }, + cameraAngleOfView() { + const cameraAngleOfView = this.transformations?.cameraAngleOfView; + + if (!cameraAngleOfView) { + console.warn('No Camera Angle of View provided'); + } + + return cameraAngleOfView; + }, + camAngleAndPositionStyle() { + const translateX = this.transformations?.translateX; + const translateY = this.transformations?.translateY; + const rotation = this.transformations?.rotation; + const scale = this.transformations?.scale; + + return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` }; + }, + camGimbalAngleStyle() { + const rotation = rotate(this.north, this.heading); + + return { + transform: `rotate(${ rotation }deg)` + }; + }, compassRoseStyle() { return { transform: `rotate(${ this.north }deg)` }; }, north() { - return this.lockCompass ? rotate(-this.cameraPan) : 0; + return this.lockCompass ? rotate(-this.cameraHeading) : 0; }, cardinalTextRotateN() { return { transform: `translateY(-27%) rotate(${ -this.north }deg)` }; @@ -297,6 +333,7 @@ export default { return this.heading !== undefined; }, headingStyle() { + /* Replaced with computed camGimbalStyle, but left here just in case. */ const rotation = rotate(this.north, this.heading); return { @@ -313,8 +350,8 @@ export default { transform: `rotate(${ rotation }deg)` }; }, - cameraPanStyle() { - const rotation = rotate(this.north, this.cameraPan); + cameraHeadingStyle() { + const rotation = rotate(this.north, this.cameraHeading); return { transform: `rotate(${ rotation }deg)` @@ -333,6 +370,24 @@ export default { return { transform: `rotate(${ -this.cameraAngleOfView / 2 }deg)` }; + }, + compassRoseSizingClasses() { + let compassRoseSizingClasses = ''; + if (this.sizedImageWidth < 300) { + compassRoseSizingClasses = '--rose-small --rose-min'; + } else if (this.sizedImageWidth < 500) { + compassRoseSizingClasses = '--rose-small'; + } else if (this.sizedImageWidth > 1000) { + compassRoseSizingClasses = '--rose-max'; + } + + return compassRoseSizingClasses; + }, + sizedImageWidth() { + return this.sizedImageDimensions.width; + }, + sizedImageHeight() { + return this.sizedImageDimensions.height; } }, watch: { diff --git a/src/plugins/imagery/components/Compass/compass.scss b/src/plugins/imagery/components/Compass/compass.scss index 5609bf9f60..a143706b2b 100644 --- a/src/plugins/imagery/components/Compass/compass.scss +++ b/src/plugins/imagery/components/Compass/compass.scss @@ -1,5 +1,5 @@ /***************************** THEME/UI CONSTANTS AND MIXINS */ -$interfaceKeyColor: #00B9C5; +$interfaceKeyColor: #fff; $elemBg: rgba(black, 0.7); @mixin sun($position: 'circle closest-side') { @@ -100,13 +100,19 @@ $elemBg: rgba(black, 0.7); } &__edge { - opacity: 0.1; + opacity: 0.2; } &__sun { opacity: 0.7; } + &__cam { + fill: $interfaceKeyColor; + transform-origin: center; + transform: scale(0.15); + } + &__cam-fov-l, &__cam-fov-r { // Cam FOV indication @@ -115,7 +121,6 @@ $elemBg: rgba(black, 0.7); } &__nsew-text, - &__spacecraft-body, &__ticks-major, &__ticks-minor { fill: $color; @@ -166,3 +171,15 @@ $elemBg: rgba(black, 0.7); padding-top: $s; } } + +/************************** ROVER */ +.cr-vrover { + $scale: 0.4; + transform-origin: center; + + &__body { + fill: $interfaceKeyColor; + opacity: 0.3; + transform-origin: center 7% !important; // Places rotation center at mast position + } +} diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index db13f8f3f5..5b18cd29d7 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -93,7 +93,6 @@ > 1000) { - compassRoseSizingClasses = '--rose-max'; - } - - return compassRoseSizingClasses; - }, displayThumbnails() { return ( this.forceShowThumbnails @@ -432,7 +419,6 @@ export default { shouldDisplayCompass() { const imageHeightAndWidth = this.sizedImageHeight !== 0 && this.sizedImageWidth !== 0; - const display = this.focusedImage !== undefined && this.focusedImageNaturalAspectRatio !== undefined && this.imageContainerWidth !== undefined @@ -440,8 +426,9 @@ export default { && imageHeightAndWidth && this.zoomFactor === 1 && this.imagePanned !== true; + const hasCameraConfigurations = this.focusedImage?.transformations !== undefined; - return display; + return display && hasCameraConfigurations; }, isSpacecraftPositionFresh() { let isFresh = undefined; @@ -626,6 +613,7 @@ export default { this.spacecraftOrientationKeys = ['heading']; this.cameraKeys = ['cameraPan', 'cameraTilt']; this.sunKeys = ['sunOrientation']; + this.transformationsKeys = ['transformations']; // related telemetry await this.initializeRelatedTelemetry(); @@ -728,7 +716,13 @@ export default { this.relatedTelemetry = new RelatedTelemetry( this.openmct, this.domainObject, - [...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys] + [ + ...this.spacecraftPositionKeys, + ...this.spacecraftOrientationKeys, + ...this.cameraKeys, + ...this.sunKeys, + ...this.transformationsKeys + ] ); if (this.relatedTelemetry.hasRelatedTelemetry) { @@ -837,6 +831,15 @@ export default { this.$set(this.focusedImageRelatedTelemetry, key, value); } } + + // set configuration for compass + this.transformationsKeys.forEach(key => { + const transformations = this.relatedTelemetry[key]; + + if (transformations !== undefined) { + this.$set(this.imageHistory[this.focusedImageIndex], key, transformations); + } + }); }, trackLatestRelatedTelemetry() { [...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => { From 5e530aa6254c985f253608c41f438725e9ca678a Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Sat, 21 Jan 2023 11:25:35 -0800 Subject: [PATCH 115/594] feat: Support thumbnails in ImageryView and ImageryTimeView (#6132) * fix: get image thumbnail formatter * refactor: ImageryView cleanup * docs: add comment * feat: Support thumbnails in ImageryView - Prefer an image's thumbnail URL if its available * feat: Support thumbnails in ImageryTimeView * refactor: rename variable * test(WIP): add thumbnail unit test, not working yet * test: temp disable test * feat: imagery thumbnail urls for example imagery * test: add unit test for imagery thumbnails * test(e2e): check for thumbnail urls - Update imagery view tests to check for use of thumbnail urls --- .../imagery/exampleImagery.e2e.spec.js | 35 ++++++--- example/imagery/plugin.js | 19 +++++ .../imagery/components/ImageThumbnail.vue | 2 +- .../imagery/components/ImageryTimeView.vue | 24 +++--- .../imagery/components/ImageryView.vue | 78 ++++++++++--------- .../imagery/components/imagery-view.scss | 2 +- src/plugins/imagery/mixins/imageryData.js | 35 +++++++-- src/plugins/imagery/pluginSpec.js | 44 ++++++++++- 8 files changed, 166 insertions(+), 73 deletions(-) diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index 1311597f0a..41aea55c7c 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -25,13 +25,13 @@ This test suite is dedicated to tests which verify the basic operations surround but only assume that example imagery is present. */ /* globals process */ -const { v4: uuid } = require('uuid'); const { waitForAnimations } = require('../../../../baseFixtures'); const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults } = require('../../../../appActions'); const backgroundImageSelector = '.c-imagery__main-image__background-image'; const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; +const thumbnailUrlParamsRegexp = /\?w=100&h=100/; //The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects. test.describe('Example Imagery Object', () => { @@ -397,13 +397,11 @@ test.describe('Example Imagery in Time Strip', () => { test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'networkidle' }); timeStripObject = await createDomainObjectWithDefaults(page, { - type: 'Time Strip', - name: 'Time Strip'.concat(' ', uuid()) + type: 'Time Strip' }); await createDomainObjectWithDefaults(page, { type: 'Example Imagery', - name: 'Example Imagery'.concat(' ', uuid()), parent: timeStripObject.uuid }); // Navigate to timestrip @@ -414,17 +412,28 @@ test.describe('Example Imagery in Time Strip', () => { type: 'issue', description: 'https://github.com/nasa/openmct/issues/5632' }); + + // Hover over the timestrip to reveal a thumbnail image await page.locator('.c-imagery-tsv-container').hover(); - // get url of the hovered image - const hoveredImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img'); - const hoveredImgSrc = await hoveredImg.getAttribute('src'); - expect(hoveredImgSrc).toBeTruthy(); + + // Get the img src of the hovered image thumbnail + const hoveredThumbnailImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img'); + const hoveredThumbnailImgSrc = await hoveredThumbnailImg.getAttribute('src'); + + // Verify that imagery timestrip view uses the thumbnailUrl as img src for thumbnails + expect(hoveredThumbnailImgSrc).toBeTruthy(); + expect(hoveredThumbnailImgSrc).toMatch(thumbnailUrlParamsRegexp); + + // Click on the hovered thumbnail to open "View Large" view await page.locator('.c-imagery-tsv-container').click(); - // get image of view large container + + // Get the img src of the large view image const viewLargeImg = page.locator('img.c-imagery__main-image__image'); const viewLargeImgSrc = await viewLargeImg.getAttribute('src'); expect(viewLargeImgSrc).toBeTruthy(); - expect(viewLargeImgSrc).toEqual(hoveredImgSrc); + + // Verify that the image in the large view is the same as the hovered thumbnail + expect(viewLargeImgSrc).toEqual(hoveredThumbnailImgSrc.split('?')[0]); }); }); @@ -441,6 +450,12 @@ test.describe('Example Imagery in Time Strip', () => { * @param {import('@playwright/test').Page} page */ async function performImageryViewOperationsAndAssert(page) { + // Verify that imagery thumbnails use a thumbnail url + const thumbnailImages = page.locator('.c-thumb__image'); + const mainImage = page.locator('.c-imagery__main-image__image'); + await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp); + await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp); + // Click previous image button const previousImageButton = page.locator('.c-nav--prev'); await previousImageButton.click(); diff --git a/example/imagery/plugin.js b/example/imagery/plugin.js index e193c99afd..2eabb32361 100644 --- a/example/imagery/plugin.js +++ b/example/imagery/plugin.js @@ -107,6 +107,15 @@ export default function () { } ] }, + { + name: 'Image Thumbnail', + key: 'thumbnail-url', + format: 'thumbnail', + hints: { + thumbnail: 1 + }, + source: 'url' + }, { name: 'Image Download Name', key: 'imageDownloadName', @@ -143,6 +152,16 @@ export default function () { ] }); + const formatThumbnail = { + format: function (url) { + return `${url}?w=100&h=100`; + } + }; + + openmct.telemetry.addFormat({ + key: 'thumbnail', + ...formatThumbnail + }); openmct.telemetry.addProvider(getRealtimeProvider()); openmct.telemetry.addProvider(getHistoricalProvider()); openmct.telemetry.addProvider(getLadProvider()); diff --git a/src/plugins/imagery/components/ImageThumbnail.vue b/src/plugins/imagery/components/ImageThumbnail.vue index 99ef0febc7..1748c32bb4 100644 --- a/src/plugins/imagery/components/ImageThumbnail.vue +++ b/src/plugins/imagery/components/ImageThumbnail.vue @@ -39,7 +39,7 @@ diff --git a/src/plugins/imagery/components/ImageryTimeView.vue b/src/plugins/imagery/components/ImageryTimeView.vue index ccaf7b71ba..5adfd2d476 100644 --- a/src/plugins/imagery/components/ImageryTimeView.vue +++ b/src/plugins/imagery/components/ImageryTimeView.vue @@ -186,17 +186,17 @@ export default { item.remove(); }); let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`); - imagery.forEach(item => { + imagery.forEach(imageElm => { if (clearAllImagery) { - item.remove(); + imageElm.remove(); } else { - const id = item.getAttributeNS(null, 'id'); + const id = imageElm.getAttributeNS(null, 'id'); if (id) { const timestamp = id.replace(ID_PREFIX, ''); if (!this.isImageryInBounds({ time: timestamp })) { - item.remove(); + imageElm.remove(); } } } @@ -343,25 +343,25 @@ export default { imageElement.style.display = 'block'; } }, - updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) { + updateExistingImageWrapper(existingImageWrapper, image, showImagePlaceholders) { //Update the x co-ordinates of the image wrapper and the url of image //this is to avoid tearing down all elements completely and re-drawing them this.setNSAttributesForElement(existingImageWrapper, { 'data-show-image-placeholders': showImagePlaceholders }); - existingImageWrapper.style.left = `${this.xScale(item.time)}px`; + existingImageWrapper.style.left = `${this.xScale(image.time)}px`; let imageElement = existingImageWrapper.querySelector('img'); this.setNSAttributesForElement(imageElement, { - src: item.url + src: image.thumbnailUrl || image.url }); this.setImageDisplay(imageElement, showImagePlaceholders); }, - createImageWrapper(index, item, showImagePlaceholders) { - const id = `${ID_PREFIX}${item.time}`; + createImageWrapper(index, image, showImagePlaceholders) { + const id = `${ID_PREFIX}${image.time}`; let imageWrapper = document.createElement('div'); imageWrapper.classList.add(IMAGE_WRAPPER_CLASS); - imageWrapper.style.left = `${this.xScale(item.time)}px`; + imageWrapper.style.left = `${this.xScale(image.time)}px`; this.setNSAttributesForElement(imageWrapper, { id, 'data-show-image-placeholders': showImagePlaceholders @@ -383,7 +383,7 @@ export default { //create image element let imageElement = document.createElement('img'); this.setNSAttributesForElement(imageElement, { - src: item.url + src: image.thumbnailUrl || image.url }); imageElement.style.width = `${IMAGE_SIZE}px`; imageElement.style.height = `${IMAGE_SIZE}px`; @@ -392,7 +392,7 @@ export default { //handle mousedown event to show the image in a large view imageWrapper.addEventListener('mousedown', (e) => { if (e.button === 0) { - this.expand(item.time); + this.expand(image.time); } }); diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index 5b18cd29d7..20446109aa 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -171,7 +171,7 @@ > { - 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; - }); - } + const layersMetadata = this.imageMetadataValue.layers; + if (!layersMetadata) { + return; + } + + this.layers = layersMetadata; + if (this.domainObject.configuration) { + const 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() { diff --git a/src/plugins/imagery/components/imagery-view.scss b/src/plugins/imagery/components/imagery-view.scss index ee0713244b..22e38d9040 100644 --- a/src/plugins/imagery/components/imagery-view.scss +++ b/src/plugins/imagery/components/imagery-view.scss @@ -29,7 +29,7 @@ flex-direction: column; flex: 1 1 auto; - &.unnsynced{ + &.unsynced{ @include sUnsynced(); } diff --git a/src/plugins/imagery/mixins/imageryData.js b/src/plugins/imagery/mixins/imageryData.js index 94118e6813..0e51b6fd35 100644 --- a/src/plugins/imagery/mixins/imageryData.js +++ b/src/plugins/imagery/mixins/imageryData.js @@ -21,6 +21,9 @@ *****************************************************************************/ const DEFAULT_DURATION_FORMATTER = 'duration'; +const IMAGE_HINT_KEY = 'image'; +const IMAGE_THUMBNAIL_HINT_KEY = 'thumbnail'; +const IMAGE_DOWNLOAD_NAME_HINT_KEY = 'imageDownloadName'; export default { inject: ['openmct', 'domainObject', 'objectPath'], @@ -32,13 +35,20 @@ export default { this.setDataTimeContext(); this.openmct.objectViews.on('clearData', this.dataCleared); - // set + // Get metadata and formatters this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); - this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] }; + + this.imageMetadataValue = { ...this.metadata.valuesForHints([IMAGE_HINT_KEY])[0] }; + this.imageFormatter = this.getFormatter(this.imageMetadataValue.key); + + this.imageThumbnailMetadataValue = { ...this.metadata.valuesForHints([IMAGE_THUMBNAIL_HINT_KEY])[0] }; + this.imageThumbnailFormatter = this.imageThumbnailMetadataValue.key + ? this.getFormatter(this.imageThumbnailMetadataValue.key) + : null; + this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); - this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints); - this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]}; + this.imageDownloadNameMetadataValue = { ...this.metadata.valuesForHints([IMAGE_DOWNLOAD_NAME_HINT_KEY])[0]}; // initialize this.timeKey = this.timeSystem.key; @@ -105,12 +115,19 @@ export default { return this.imageFormatter.format(datum); }, + formatImageThumbnailUrl(datum) { + if (!datum || !this.imageThumbnailFormatter) { + return; + } + + return this.imageThumbnailFormatter.format(datum); + }, formatTime(datum) { if (!datum) { return; } - let dateTimeStr = this.timeFormatter.format(datum); + const dateTimeStr = this.timeFormatter.format(datum); // Replace ISO "T" with a space to allow wrapping return dateTimeStr.replace("T", " "); @@ -118,7 +135,7 @@ export default { getImageDownloadName(datum) { let imageDownloadName = ''; if (datum) { - const key = this.imageDownloadNameHints.key; + const key = this.imageDownloadNameMetadataValue.key; imageDownloadName = datum[key]; } @@ -150,6 +167,7 @@ export default { normalizeDatum(datum) { const formattedTime = this.formatTime(datum); const url = this.formatImageUrl(datum); + const thumbnailUrl = this.formatImageThumbnailUrl(datum); const time = this.parseTime(formattedTime); const imageDownloadName = this.getImageDownloadName(datum); @@ -157,13 +175,14 @@ export default { ...datum, formattedTime, url, + thumbnailUrl, time, imageDownloadName }; }, getFormatter(key) { - let metadataValue = this.metadata.value(key) || { format: key }; - let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); + const metadataValue = this.metadata.value(key) || { format: key }; + const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); return valueFormatter; } diff --git a/src/plugins/imagery/pluginSpec.js b/src/plugins/imagery/pluginSpec.js index 15a81b09e3..226de27136 100644 --- a/src/plugins/imagery/pluginSpec.js +++ b/src/plugins/imagery/pluginSpec.js @@ -35,6 +35,10 @@ const MAIN_IMAGE_CLASS = '.js-imageryView-image'; const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new'; const REFRESH_CSS_MS = 500; +function formatThumbnail(url) { + return url.replace('logo-openmct.svg', 'logo-nasa.svg'); +} + function getImageInfo(doc) { let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; let timestamp = imageElement.dataset.openmctImageTimestamp; @@ -124,6 +128,16 @@ describe("The Imagery View Layouts", () => { }, "source": "url" }, + { + "name": "Image Thumbnail", + "key": "thumbnail-url", + "format": "thumbnail", + "hints": { + "thumbnail": 1, + "priority": 3 + }, + "source": "url" + }, { "name": "Name", "key": "name", @@ -200,6 +214,11 @@ describe("The Imagery View Layouts", () => { originalRouterPath = openmct.router.path; + openmct.telemetry.addFormat({ + key: 'thumbnail', + format: formatThumbnail + }); + openmct.on('start', done); openmct.startHeadless(); }); @@ -384,15 +403,32 @@ describe("The Imagery View Layouts", () => { //Looks like we need Vue.nextTick here so that computed properties settle down await Vue.nextTick(); const layerEls = parent.querySelectorAll('.js-layer-image'); - console.log(layerEls); expect(layerEls.length).toEqual(1); }); + it("should use the image thumbnailUrl for thumbnails", async () => { + await Vue.nextTick(); + const fullSizeImageUrl = imageTelemetry[5].url; + const thumbnailUrl = formatThumbnail(imageTelemetry[5].url); + + // Ensure thumbnails are shown w/ thumbnail Urls + const thumbnails = parent.querySelectorAll(`img[src='${thumbnailUrl}']`); + expect(thumbnails.length).toBeGreaterThan(0); + + // Click a thumbnail + parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click(); + await Vue.nextTick(); + + // Ensure full size image is shown w/ full size url + const fullSizeImages = parent.querySelectorAll(`img[src='${fullSizeImageUrl}']`); + expect(fullSizeImages.length).toBeGreaterThan(0); + }); + it("should show the clicked thumbnail as the main image", async () => { //Looks like we need Vue.nextTick here so that computed properties settle down await Vue.nextTick(); - const target = imageTelemetry[5].url; - parent.querySelectorAll(`img[src='${target}']`)[0].click(); + const thumbnailUrl = formatThumbnail(imageTelemetry[5].url); + parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click(); await Vue.nextTick(); const imageInfo = getImageInfo(parent); @@ -417,7 +453,7 @@ describe("The Imagery View Layouts", () => { it("should show that an image is not new", async () => { await Vue.nextTick(); - const target = imageTelemetry[4].url; + const target = formatThumbnail(imageTelemetry[4].url); parent.querySelectorAll(`img[src='${target}']`)[0].click(); await Vue.nextTick(); From 9980aab18f622ca9410e9c3aa61d3bb9b4a18e0f Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Sun, 22 Jan 2023 19:38:05 +0100 Subject: [PATCH 116/594] 5834 stacked plot removing objects from a stacked plot will not remove them from the legend (#6022) * Add listeners to remove stacked plot series and make keys unique Co-authored-by: Shefali Joshi --- src/plugins/plot/MctPlot.vue | 6 ++-- src/plugins/plot/inspector/PlotOptions.vue | 2 +- src/plugins/plot/legend/PlotLegend.vue | 4 +-- src/plugins/plot/stackedPlot/StackedPlot.vue | 31 ++++++++++++++----- .../plot/stackedPlot/StackedPlotItem.vue | 3 +- 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 8704069dd4..4bf5183453 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -353,10 +353,8 @@ export default { this.config = this.getConfig(); this.legend = this.config.legend; - if (this.isNestedWithinAStackedPlot) { - const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); - this.$emit('configLoaded', configId); - } + const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); + this.$emit('configLoaded', configId); this.listenTo(this.config.series, 'add', this.addSeries, this); this.listenTo(this.config.series, 'remove', this.removeSeries, this); diff --git a/src/plugins/plot/inspector/PlotOptions.vue b/src/plugins/plot/inspector/PlotOptions.vue index a72fcb8c9a..5bcbc5f09b 100644 --- a/src/plugins/plot/inspector/PlotOptions.vue +++ b/src/plugins/plot/inspector/PlotOptions.vue @@ -38,7 +38,7 @@ export default { PlotOptionsBrowse, PlotOptionsEdit }, - inject: ['openmct', 'domainObject'], + inject: ['openmct', 'domainObject', 'path'], data() { return { isEditing: this.openmct.editor.isEditing() diff --git a/src/plugins/plot/legend/PlotLegend.vue b/src/plugins/plot/legend/PlotLegend.vue index 1bdf4b3bb9..01054b1958 100644 --- a/src/plugins/plot/legend/PlotLegend.vue +++ b/src/plugins/plot/legend/PlotLegend.vue @@ -50,7 +50,7 @@ > { this.data = data; } @@ -183,6 +185,10 @@ export default { this.domainObject.configuration.series.splice(configIndex, 1); } + this.removeSeries({ + keyString: id + }); + const childObj = this.compositionObjects.filter((c) => { const identifier = this.openmct.objects.makeKeyString(c.identifier); @@ -244,18 +250,29 @@ export default { this.highlights = data; }, registerSeriesListeners(configId) { - this.seriesConfig[configId] = this.getConfig(configId); - this.listenTo(this.seriesConfig[configId].series, 'add', this.addSeries, this); - this.listenTo(this.seriesConfig[configId].series, 'remove', this.removeSeries, this); + const config = this.getConfig(configId); + this.seriesConfig[configId] = config; + const childObject = config.get('domainObject'); - this.seriesConfig[configId].series.models.forEach(this.addSeries, this); + //TODO differentiate between objects with composition and those without + if (childObject.type === 'telemetry.plot.overlay') { + this.listenTo(config.series, 'add', this.addSeries, this); + this.listenTo(config.series, 'remove', this.removeSeries, this); + } + + config.series.models.forEach(this.addSeries, this); }, addSeries(series) { - const index = this.seriesModels.length; - this.$set(this.seriesModels, index, series); + const childObject = series.domainObject; + //don't add the series if it can have child series this will happen in registerSeriesListeners + if (childObject.type !== 'telemetry.plot.overlay') { + const index = this.seriesModels.length; + this.$set(this.seriesModels, index, series); + } + }, removeSeries(plotSeries) { - const index = this.seriesModels.findIndex(seriesModel => this.openmct.objects.areIdsEqual(seriesModel.identifier, plotSeries.identifier)); + const index = this.seriesModels.findIndex(seriesModel => seriesModel.keyString === plotSeries.keyString); if (index > -1) { this.$delete(this.seriesModels, index); } diff --git a/src/plugins/plot/stackedPlot/StackedPlotItem.vue b/src/plugins/plot/stackedPlot/StackedPlotItem.vue index b0049a3613..e7ec5b8f46 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotItem.vue +++ b/src/plugins/plot/stackedPlot/StackedPlotItem.vue @@ -133,6 +133,7 @@ export default { //If this object is not persistable, then package it with it's parent const object = this.getPlotObject(); + const getProps = this.getProps; const isMissing = openmct.objects.isMissing(object); let viewContainer = document.createElement('div'); @@ -160,7 +161,7 @@ export default { onGridLinesChange, setStatus, isMissing, - loading: true + loading: false }; }, methods: { From 1b71a3bf338a946eb743af1eadc9a8a60e5ec69b Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Mon, 23 Jan 2023 07:34:26 -0800 Subject: [PATCH 117/594] Multiple Y-Axes for Overlay Plots (#6153) Support multiple y-axes in overlay plots Co-authored-by: Khalid Adil Co-authored-by: Shefali Joshi Co-authored-by: Rukmini Bose --- .../plugins/plot/autoscale.e2e.spec.js | 2 +- .../plugins/plot/logPlot.e2e.spec.js | 5 +- .../plugins/plot/overlayPlot.e2e.spec.js | 124 +++++ src/plugins/plot/MctPlot.vue | 377 +++++++++---- src/plugins/plot/MctTicks.vue | 21 +- src/plugins/plot/axis/YAxis.vue | 186 +++++-- src/plugins/plot/chart/MctChart.vue | 350 ++++++++---- .../configuration/PlotConfigurationModel.js | 37 +- src/plugins/plot/configuration/PlotSeries.js | 7 +- .../plot/configuration/SeriesCollection.js | 3 + src/plugins/plot/configuration/YAxisModel.js | 111 ++-- .../plot/inspector/PlotOptionsBrowse.vue | 121 +++-- .../plot/inspector/PlotOptionsEdit.vue | 66 ++- .../plot/inspector/forms/YAxisForm.vue | 116 +++- src/plugins/plot/overlayPlot/pluginSpec.js | 504 ++++++++++++++++++ src/plugins/plot/pluginSpec.js | 16 + src/plugins/plot/stackedPlot/StackedPlot.vue | 3 +- src/plugins/plot/stackedPlot/pluginSpec.js | 18 +- src/styles/_glyphs.scss | 1 + src/styles/_legacy-plots.scss | 44 +- src/ui/inspector/ElementItem.vue | 7 +- src/ui/inspector/ElementItemGroup.vue | 101 ++++ src/ui/inspector/ElementsPool.vue | 4 +- src/ui/inspector/Inspector.vue | 23 +- src/ui/inspector/PlotElementsPool.vue | 330 ++++++++++++ src/ui/inspector/elements.scss | 29 +- 26 files changed, 2234 insertions(+), 372 deletions(-) create mode 100644 e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js create mode 100644 src/plugins/plot/overlayPlot/pluginSpec.js create mode 100644 src/ui/inspector/ElementItemGroup.vue create mode 100644 src/ui/inspector/PlotElementsPool.vue diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js index a615254194..fb9a2e2a73 100644 --- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js @@ -156,7 +156,7 @@ async function turnOffAutoscale(page) { await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click(); // uncheck autoscale - await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck(); + await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck(); // save await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); diff --git a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js index fa6b43eec1..e923b15bb4 100644 --- a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js @@ -205,7 +205,8 @@ async function enableEditMode(page) { */ async function enableLogMode(page) { // turn on log mode - await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check(); + await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').check(); + // await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check(); } /** @@ -213,7 +214,7 @@ async function enableLogMode(page) { */ async function disableLogMode(page) { // turn off log mode - await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck(); + await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck(); } /** diff --git a/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js new file mode 100644 index 0000000000..3486b62a4d --- /dev/null +++ b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js @@ -0,0 +1,124 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +Tests to verify log plot functionality. Note this test suite if very much under active development and should not +necessarily be used for reference when writing new tests in this area. +*/ + +const { test, expect } = require('../../../../pluginFixtures'); +const { createDomainObjectWithDefaults } = require('../../../../appActions'); + +test.describe('Overlay Plot', () => { + test('Plot legend color is in sync with plot series color', async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' }); + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: "Overlay Plot" + }); + + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + parent: overlayPlot.uuid + }); + + await page.goto(overlayPlot.url); + + // navigate to plot series color palette + await page.click('.l-browse-bar__actions__edit'); + await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click(); + await page.locator('.c-click-swatch--menu').click(); + await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click(); + + // gets color for swatch located in legend + const element = await page.waitForSelector('.plot-series-color-swatch'); + const color = await element.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('background-color'); + }); + + expect(color).toBe('rgb(255, 166, 61)'); + }); + test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' }); + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: "Overlay Plot" + }); + + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg a', + parent: overlayPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg b', + parent: overlayPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg c', + parent: overlayPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg d', + parent: overlayPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg e', + parent: overlayPlot.uuid + }); + + await page.goto(overlayPlot.url); + await page.click('button[title="Edit"]'); + + // Expand the elements pool vertically + await page.locator('.l-pane__handle').nth(2).hover({ trial: true }); + await page.mouse.down(); + await page.mouse.move(0, 100); + await page.mouse.up(); + + // Drag swg a, c, e into Y Axis 2 + await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + + // Drag swg b into Y Axis 3 + await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]')); + + const yAxis1Group = page.getByLabel("Y Axis 1"); + const yAxis2Group = page.getByLabel("Y Axis 2"); + const yAxis3Group = page.getByLabel("Y Axis 3"); + + // Verify that the elements are in the correct buckets and in the correct order + expect(yAxis1Group.getByRole('listitem', { name: 'swg d' })).toBeTruthy(); + expect(yAxis1Group.getByRole('listitem').nth(0).getByText('swg d')).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem', { name: 'swg e' })).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem').nth(0).getByText('swg e')).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem', { name: 'swg c' })).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem').nth(1).getByText('swg c')).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem', { name: 'swg a' })).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem').nth(2).getByText('swg a')).toBeTruthy(); + expect(yAxis3Group.getByRole('listitem', { name: 'swg b' })).toBeTruthy(); + expect(yAxis3Group.getByRole('listitem').nth(0).getByText('swg b')).toBeTruthy(); + }); +}); diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 4bf5183453..d970992e1c 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -34,23 +34,27 @@ @legendHoverChanged="legendHoverChanged" />
- +
+ +
@@ -69,9 +73,12 @@ /> @@ -88,6 +95,7 @@ :annotated-points="annotatedPoints" :annotation-selections="annotationSelections" :show-limit-line-labels="showLimitLineLabels" + :hidden-y-axis-ids="hiddenYAxisIds" :annotation-viewing-and-editing-allowed="annotationViewingAndEditingAllowed" @plotReinitializeCanvas="initCanvas" @chartLoaded="initialize" @@ -218,6 +226,7 @@ import KDBush from 'kdbush'; import _ from "lodash"; const OFFSET_THRESHOLD = 10; +const AXES_PADDING = 20; export default { components: { @@ -275,7 +284,6 @@ export default { annotatedPoints: [], annotationSelections: [], lockHighlightPoint: false, - tickWidth: 0, yKeyOptions: [], yAxisLabel: '', rectangles: [], @@ -290,12 +298,33 @@ export default { isTimeOutOfSync: false, showLimitLineLabels: this.limitLineLabels, isFrozenOnMouseDown: false, - hasSameRangeValue: true, cursorGuide: this.initCursorGuide, - gridLines: this.initGridLines + gridLines: this.initGridLines, + yAxes: [], + hiddenYAxisIds: [], + yAxisListWithRange: [] }; }, computed: { + xAxisStyle() { + const rightAxis = this.yAxesIds.find(yAxis => yAxis.id > 2); + const leftOffset = this.multipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING; + let style = { + left: `${this.plotLeftTickWidth + leftOffset}px` + }; + + if (rightAxis) { + style.right = `${rightAxis.tickWidth + AXES_PADDING}px`; + } + + return style; + }, + yAxesIds() { + return this.yAxes.filter(yAxis => yAxis.seriesCount > 0); + }, + multipleLeftAxes() { + return this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1; + }, isNestedWithinAStackedPlot() { const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path)); @@ -322,8 +351,17 @@ export default { return 'plot-legend-collapsed'; } }, - plotWidth() { - return this.plotTickWidth || this.tickWidth; + plotLeftTickWidth() { + let leftTickWidth = 0; + this.yAxes.forEach((yAxis) => { + if (yAxis.id > 2) { + return; + } + + leftTickWidth = leftTickWidth + yAxis.tickWidth; + }); + + return this.plotTickWidth || leftTickWidth; } }, watch: { @@ -341,6 +379,7 @@ export default { } }, mounted() { + this.yAxisIdVisibility = {}; this.offsetWidth = 0; document.addEventListener('keydown', this.handleKeyDown); @@ -352,6 +391,20 @@ export default { this.config = this.getConfig(); this.legend = this.config.legend; + this.yAxes = [{ + id: this.config.yAxis.id, + seriesCount: 0, + tickWidth: 0 + }]; + if (this.config.additionalYAxes) { + this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => { + return { + id: yAxis.id, + seriesCount: 0, + tickWidth: 0 + }; + })); + } const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.$emit('configLoaded', configId); @@ -373,6 +426,8 @@ export default { this.openmct.selection.on('change', this.updateSelection); this.setTimeContext(); + this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes]; + this.loaded = true; }, beforeDestroy() { @@ -456,8 +511,10 @@ export default { }, setTimeContext() { this.stopFollowingTimeContext(); + this.timeContext = this.openmct.time.getContextForView(this.path); this.followTimeContext(); + }, followTimeContext() { this.updateDisplayBounds(this.timeContext.bounds()); @@ -490,33 +547,41 @@ export default { return config; }, addSeries(series, index) { + const yAxisId = series.get('yAxisId'); + this.updateAxisUsageCount(yAxisId, 1); this.$set(this.seriesModels, index, series); this.listenTo(series, 'change:xKey', (xKey) => { this.setDisplayRange(series, xKey); }, this); this.listenTo(series, 'change:yKey', () => { - this.checkSameRangeValue(); this.loadSeriesData(series); }, this); this.listenTo(series, 'change:interpolate', () => { this.loadSeriesData(series); }, this); + this.listenTo(series, 'change:yAxisId', this.updateTicksAndSeriesForYAxis, this); - this.checkSameRangeValue(); this.loadSeriesData(series); }, - checkSameRangeValue() { - this.hasSameRangeValue = this.seriesModels.every((model) => { - return model.get('yKey') === this.seriesModels[0].get('yKey'); - }); + removeSeries(plotSeries, index) { + const yAxisId = plotSeries.get('yAxisId'); + this.updateAxisUsageCount(yAxisId, -1); + this.seriesModels.splice(index, 1); + this.stopListening(plotSeries); }, - removeSeries(plotSeries, index) { - this.seriesModels.splice(index, 1); - this.checkSameRangeValue(); - this.stopListening(plotSeries); + updateTicksAndSeriesForYAxis(newAxisId, oldAxisId) { + this.updateAxisUsageCount(oldAxisId, -1); + this.updateAxisUsageCount(newAxisId, 1); + }, + + updateAxisUsageCount(yAxisId, updateCountBy) { + const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId); + if (foundYAxis) { + foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy; + } }, async loadAnnotations() { if (!this.openmct.annotation.getAvailableTags().length) { @@ -832,7 +897,13 @@ export default { // Setup canvas etc. this.xScale = new LinearScale(this.config.xAxis.get('displayRange')); - this.yScale = new LinearScale(this.config.yAxis.get('displayRange')); + this.yScale = []; + this.yAxisListWithRange.forEach((yAxis) => { + this.yScale.push({ + id: yAxis.id, + scale: new LinearScale(yAxis.get('displayRange')) + }); + }); this.pan = undefined; this.marquee = undefined; @@ -848,7 +919,9 @@ export default { this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal; this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this); - this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this); + this.yAxisListWithRange.forEach((yAxis) => { + this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange.bind(this, yAxis.id), this); + }); }, onXAxisChange(displayBounds) { @@ -857,26 +930,45 @@ export default { } }, - onYAxisChange(displayBounds) { + onYAxisChange(yAxisId, displayBounds) { if (displayBounds) { - this.yScale.domain(displayBounds); + this.yScale.filter((yAxis) => yAxis.id === yAxisId).forEach((yAxis) => { + yAxis.scale.domain(displayBounds); + }); } }, - onTickWidthChange(width, fromDifferentObject) { - if (fromDifferentObject) { + onTickWidthChange(data, fromDifferentObject) { + const {width, yAxisId} = data; + if (yAxisId) { + const index = this.yAxes.findIndex(yAxis => yAxis.id === yAxisId); + if (fromDifferentObject) { // Always accept tick width if it comes from a different object. - this.tickWidth = width; - } else { + this.yAxes[index].tickWidth = width; + } else { // Otherwise, only accept tick with if it's larger. - const newWidth = Math.max(width, this.tickWidth); - if (newWidth !== this.tickWidth) { - this.tickWidth = newWidth; + const newWidth = Math.max(width, this.yAxes[index].tickWidth); + if (newWidth !== this.yAxes[index].tickWidth) { + this.yAxes[index].tickWidth = newWidth; + } } + + const id = this.openmct.objects.makeKeyString(this.domainObject.identifier); + this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id); + } + }, + + toggleSeriesForYAxis({ id, visible}) { + //if toggling to visible, re-fetch the data for the series that are part of this y Axis + if (visible === true) { + this.config.series.models.filter(model => model.get('yAxisId') === id) + .forEach(this.loadSeriesData, this); } - const id = this.openmct.objects.makeKeyString(this.domainObject.identifier); - this.$emit('plotTickWidth', this.tickWidth, id); + this.yAxisIdVisibility[id] = visible; + this.hiddenYAxisIds = Object.keys(this.yAxisIdVisibility).map(Number).filter(key => { + return this.yAxisIdVisibility[key] === false; + }); }, trackMousePosition(event) { @@ -885,9 +977,11 @@ export default { min: 0, max: this.chartElementBounds.width }); - this.yScale.range({ - min: 0, - max: this.chartElementBounds.height + this.yScale.forEach((yAxis) => { + yAxis.scale.range({ + min: 0, + max: this.chartElementBounds.height + }); }); this.positionOverElement = { @@ -896,9 +990,13 @@ export default { - (event.clientY - this.chartElementBounds.top) }; + const yLocationForPositionOverPlot = this.yScale.map((yAxis) => yAxis.scale.invert(this.positionOverElement.y)); + const yAxisIds = this.yScale.map((yAxis) => yAxis.id); + // Also store the order of yAxisIds so that we can associate the y location to the yAxis this.positionOverPlot = { x: this.xScale.invert(this.positionOverElement.x), - y: this.yScale.invert(this.positionOverElement.y) + y: yLocationForPositionOverPlot, + yAxisIds }; if (this.cursorGuide) { @@ -911,6 +1009,12 @@ export default { event.preventDefault(); }, + getYPositionForYAxis(object, yAxis) { + const index = object.yAxisIds.findIndex(yAxisId => yAxisId === yAxis.get('id')); + + return object.y[index]; + }, + updateCrosshairs(event) { this.cursorGuideVertical.style.left = (event.clientX - this.chartElementBounds.x) + 'px'; this.cursorGuideHorizontal.style.top = (event.clientY - this.chartElementBounds.y) + 'px'; @@ -1017,8 +1121,9 @@ export default { } const { start, end } = this.marquee; + const someYPositionOverPlot = start.y.some(y => y); - return start.x === end.x && start.y === end.y; + return start.x === end.x && someYPositionOverPlot; }, updateMarquee() { @@ -1179,9 +1284,15 @@ export default { }, endAnnotationMarquee(event) { const minX = Math.min(this.marquee.start.x, this.marquee.end.x); - const minY = Math.min(this.marquee.start.y, this.marquee.end.y); + const startMinY = this.marquee.start.y.reduce((previousY, currentY) => { + return Math.min(previousY, currentY); + }, this.marquee.start.y[0]); + const endMinY = this.marquee.end.y.reduce((previousY, currentY) => { + return Math.min(previousY, currentY); + }, this.marquee.end.y[0]); + const minY = Math.min(startMinY, endMinY); const maxX = Math.max(this.marquee.start.x, this.marquee.end.x); - const maxY = Math.max(this.marquee.start.y, this.marquee.end.y); + const maxY = Math.max(startMinY, endMinY); const boundingBox = { minX, minY, @@ -1205,9 +1316,13 @@ export default { min: Math.min(this.marquee.start.x, this.marquee.end.x), max: Math.max(this.marquee.start.x, this.marquee.end.x) }); - this.config.yAxis.set('displayRange', { - min: Math.min(this.marquee.start.y, this.marquee.end.y), - max: Math.max(this.marquee.start.y, this.marquee.end.y) + this.yAxisListWithRange.forEach((yAxis) => { + const yStartPosition = this.getYPositionForYAxis(this.marquee.start, yAxis); + const yEndPosition = this.getYPositionForYAxis(this.marquee.end, yAxis); + yAxis.set('displayRange', { + min: Math.min(yStartPosition, yEndPosition), + max: Math.max(yStartPosition, yEndPosition) + }); }); this.userViewportChangeEnd(); } else { @@ -1238,11 +1353,17 @@ export default { zoom(zoomDirection, zoomFactor) { const currentXaxis = this.config.xAxis.get('displayRange'); - const currentYaxis = this.config.yAxis.get('displayRange'); + + let doesYAxisHaveRange = false; + this.yAxisListWithRange.forEach((yAxisModel) => { + if (yAxisModel.get('displayRange')) { + doesYAxisHaveRange = true; + } + }); // when there is no plot data, the ranges can be undefined // in which case we should not perform zoom - if (!currentXaxis || !currentYaxis) { + if (!currentXaxis || !doesYAxisHaveRange) { return; } @@ -1250,7 +1371,6 @@ export default { this.trackHistory(); const xAxisDist = (currentXaxis.max - currentXaxis.min) * zoomFactor; - const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor; if (zoomDirection === 'in') { this.config.xAxis.set('displayRange', { @@ -1258,9 +1378,17 @@ export default { max: currentXaxis.max - xAxisDist }); - this.config.yAxis.set('displayRange', { - min: currentYaxis.min + yAxisDist, - max: currentYaxis.max - yAxisDist + this.yAxisListWithRange.forEach((yAxisModel) => { + const currentYaxis = yAxisModel.get('displayRange'); + if (!currentYaxis) { + return; + } + + const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor; + yAxisModel.set('displayRange', { + min: currentYaxis.min + yAxisDist, + max: currentYaxis.max - yAxisDist + }); }); } else if (zoomDirection === 'out') { this.config.xAxis.set('displayRange', { @@ -1268,9 +1396,17 @@ export default { max: currentXaxis.max + xAxisDist }); - this.config.yAxis.set('displayRange', { - min: currentYaxis.min - yAxisDist, - max: currentYaxis.max + yAxisDist + this.yAxisListWithRange.forEach((yAxisModel) => { + const currentYaxis = yAxisModel.get('displayRange'); + if (!currentYaxis) { + return; + } + + const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor; + yAxisModel.set('displayRange', { + min: currentYaxis.min - yAxisDist, + max: currentYaxis.max + yAxisDist + }); }); } @@ -1287,11 +1423,17 @@ export default { } let xDisplayRange = this.config.xAxis.get('displayRange'); - let yDisplayRange = this.config.yAxis.get('displayRange'); + + let doesYAxisHaveRange = false; + this.yAxisListWithRange.forEach((yAxisModel) => { + if (yAxisModel.get('displayRange')) { + doesYAxisHaveRange = true; + } + }); // when there is no plot data, the ranges can be undefined // in which case we should not perform zoom - if (!xDisplayRange || !yDisplayRange) { + if (!xDisplayRange || !doesYAxisHaveRange) { return; } @@ -1299,22 +1441,19 @@ export default { window.clearTimeout(this.stillZooming); let xAxisDist = (xDisplayRange.max - xDisplayRange.min); - let yAxisDist = (yDisplayRange.max - yDisplayRange.min); let xDistMouseToMax = xDisplayRange.max - this.positionOverPlot.x; let xDistMouseToMin = this.positionOverPlot.x - xDisplayRange.min; - let yDistMouseToMax = yDisplayRange.max - this.positionOverPlot.y; - let yDistMouseToMin = this.positionOverPlot.y - yDisplayRange.min; let xAxisMaxDist = xDistMouseToMax / xAxisDist; let xAxisMinDist = xDistMouseToMin / xAxisDist; - let yAxisMaxDist = yDistMouseToMax / yAxisDist; - let yAxisMinDist = yDistMouseToMin / yAxisDist; let plotHistoryStep; if (!plotHistoryStep) { + const yRangeList = []; + this.yAxisListWithRange.map((yAxis) => yRangeList.push(yAxis.get('displayRange'))); plotHistoryStep = { - x: xDisplayRange, - y: yDisplayRange + x: this.config.xAxis.get('displayRange'), + y: yRangeList }; } @@ -1325,20 +1464,47 @@ export default { max: xDisplayRange.max - ((xAxisDist * ZOOM_AMT) * xAxisMaxDist) }); - this.config.yAxis.set('displayRange', { - min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist), - max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist) + this.yAxisListWithRange.forEach((yAxisModel) => { + const yDisplayRange = yAxisModel.get('displayRange'); + if (!yDisplayRange) { + return; + } + + const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel); + let yAxisDist = (yDisplayRange.max - yDisplayRange.min); + let yDistMouseToMax = yDisplayRange.max - yPosition; + let yDistMouseToMin = yPosition - yDisplayRange.min; + let yAxisMaxDist = yDistMouseToMax / yAxisDist; + let yAxisMinDist = yDistMouseToMin / yAxisDist; + + yAxisModel.set('displayRange', { + min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist), + max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist) + }); }); } else if (event.wheelDelta >= 0) { - this.config.xAxis.set('displayRange', { min: xDisplayRange.min - ((xAxisDist * ZOOM_AMT) * xAxisMinDist), max: xDisplayRange.max + ((xAxisDist * ZOOM_AMT) * xAxisMaxDist) }); - this.config.yAxis.set('displayRange', { - min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist), - max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist) + this.yAxisListWithRange.forEach((yAxisModel) => { + const yDisplayRange = yAxisModel.get('displayRange'); + if (!yDisplayRange) { + return; + } + + const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel); + let yAxisDist = (yDisplayRange.max - yDisplayRange.min); + let yDistMouseToMax = yDisplayRange.max - yPosition; + let yDistMouseToMin = yPosition - yDisplayRange.min; + let yAxisMaxDist = yDistMouseToMax / yAxisDist; + let yAxisMinDist = yDistMouseToMin / yAxisDist; + + yAxisModel.set('displayRange', { + min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist), + max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist) + }); }); } @@ -1371,24 +1537,48 @@ export default { } const dX = this.pan.start.x - this.positionOverPlot.x; - const dY = this.pan.start.y - this.positionOverPlot.y; const xRange = this.config.xAxis.get('displayRange'); - const yRange = this.config.yAxis.get('displayRange'); this.config.xAxis.set('displayRange', { min: xRange.min + dX, max: xRange.max + dX }); - this.config.yAxis.set('displayRange', { - min: yRange.min + dY, - max: yRange.max + dY + + const dY = []; + this.positionOverPlot.y.forEach((yAxisPosition, index) => { + const yAxisId = this.positionOverPlot.yAxisIds[index]; + dY.push({ + yAxisId: yAxisId, + y: this.pan.start.y[index] - yAxisPosition + }); + }); + + this.yAxisListWithRange.forEach((yAxis) => { + const yRange = yAxis.get('displayRange'); + if (!yRange) { + return; + } + + const yIndex = dY.findIndex(y => y.yAxisId === yAxis.get('id')); + + yAxis.set('displayRange', { + min: yRange.min + dY[yIndex].y, + max: yRange.max + dY[yIndex].y + }); }); }, trackHistory() { + const yRangeList = []; + const yAxisIds = []; + this.yAxisListWithRange.forEach((yAxis) => { + yRangeList.push(yAxis.get('displayRange')); + yAxisIds.push(yAxis.get('id')); + }); this.plotHistory.push({ x: this.config.xAxis.get('displayRange'), - y: this.config.yAxis.get('displayRange') + y: yRangeList, + yAxisIds }); }, @@ -1398,7 +1588,9 @@ export default { }, freeze() { - this.config.yAxis.set('frozen', true); + this.yAxisListWithRange.forEach((yAxis) => { + yAxis.set('frozen', true); + }); this.config.xAxis.set('frozen', true); this.setStatus(); }, @@ -1409,7 +1601,9 @@ export default { }, clearPanZoomHistory() { - this.config.yAxis.set('frozen', false); + this.yAxisListWithRange.forEach((yAxis) => { + yAxis.set('frozen', false); + }); this.config.xAxis.set('frozen', false); this.setStatus(); this.plotHistory = []; @@ -1424,12 +1618,17 @@ export default { } this.config.xAxis.set('displayRange', previousAxisRanges.x); - this.config.yAxis.set('displayRange', previousAxisRanges.y); + this.yAxisListWithRange.forEach((yAxis) => { + const yPosition = this.getYPositionForYAxis(previousAxisRanges, yAxis); + yAxis.set('displayRange', yPosition); + }); + this.userViewportChangeEnd(); }, - setYAxisKey(yKey) { - this.config.series.models[0].set('yKey', yKey); + setYAxisKey(yKey, yAxisId) { + const seriesForYAxis = this.config.series.models.filter((model => model.get('yAxisId') === yAxisId)); + seriesForYAxis.forEach(model => model.set('yKey', yKey)); }, pause() { diff --git a/src/plugins/plot/MctTicks.vue b/src/plugins/plot/MctTicks.vue index ab09cb6d1c..755678fc70 100644 --- a/src/plugins/plot/MctTicks.vue +++ b/src/plugins/plot/MctTicks.vue @@ -103,6 +103,12 @@ export default { return 6; } }, + axisId: { + type: Number, + default() { + return null; + } + }, position: { required: true, type: String, @@ -145,7 +151,15 @@ export default { throw new Error('config is missing'); } - return config[this.axisType]; + if (this.axisType === 'yAxis') { + if (this.axisId && this.axisId !== config.yAxis.id) { + return config.additionalYAxes.find(axis => axis.id === this.axisId); + } else { + return config.yAxis; + } + } else { + return config[this.axisType]; + } }, /** * Determine whether ticks should be regenerated for a given range. @@ -258,7 +272,10 @@ export default { }, 0)); this.tickWidth = tickWidth; - this.$emit('plotTickWidth', tickWidth); + this.$emit('plotTickWidth', { + width: tickWidth, + yAxisId: this.axisType === 'yAxis' ? this.axisId : '' + }); this.shouldCheckWidth = false; } } diff --git a/src/plugins/plot/axis/YAxis.vue b/src/plugins/plot/axis/YAxis.vue index 6e170fbd6a..0073a048b8 100644 --- a/src/plugins/plot/axis/YAxis.vue +++ b/src/plugins/plot/axis/YAxis.vue @@ -22,19 +22,28 @@ -
+
-
+
-
-
+
+ +
diff --git a/src/ui/components/tags/tags.scss b/src/ui/components/tags/tags.scss index 964b2361ab..23157937e3 100644 --- a/src/ui/components/tags/tags.scss +++ b/src/ui/components/tags/tags.scss @@ -1,19 +1,30 @@ +@mixin tagHolder() { + align-items: center; + display: flex; + flex-wrap: wrap; + + > * { + $m: $interiorMarginSm; + + margin: 0 $m $m 0; + } +} + + /******************************* TAGS */ .c-tag { - border-radius: 10px; //TODO: convert to theme constant + border-radius: $tagBorderRadius; display: inline-flex; - padding: 1px 10px; //TODO: convert to theme constant - - > * + * { - margin-left: $interiorMargin; - } + overflow: hidden; + padding: 1px 6px; //TODO: convert to theme constant + transition: $transIn; &__remove-btn { color: inherit !important; - display: none; opacity: 0; - overflow: hidden; - padding: 1px !important; + padding: 0; // Overrides default

    Legend

    @@ -97,20 +96,23 @@ export default { mounted() { eventHelpers.extend(this); this.config = this.getConfig(); - this.yAxes = [{ - id: this.config.yAxis.id, - seriesCount: 0 - }]; - if (this.config.additionalYAxes) { - this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => { - return { - id: yAxis.id, - seriesCount: 0 - }; - })); + if (!this.isStackedPlotObject) { + this.yAxes = [{ + id: this.config.yAxis.id, + seriesCount: 0 + }]; + if (this.config.additionalYAxes) { + this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => { + return { + id: yAxis.id, + seriesCount: 0 + }; + })); + } + + this.registerListeners(); } - this.registerListeners(); this.loaded = true; }, beforeDestroy() { diff --git a/src/plugins/plot/inspector/PlotsInspectorViewProvider.js b/src/plugins/plot/inspector/PlotsInspectorViewProvider.js index aebacfa7eb..36703c0263 100644 --- a/src/plugins/plot/inspector/PlotsInspectorViewProvider.js +++ b/src/plugins/plot/inspector/PlotsInspectorViewProvider.js @@ -12,11 +12,12 @@ export default function PlotsInspectorViewProvider(openmct) { } let object = selection[0][0].context.item; + let parent = selection[0].length > 1 && selection[0][1].context.item; const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay'; - const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked'; + const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked'; - return isStackedPlotObject || isOverlayPlotObject; + return isOverlayPlotObject || isParentStackedPlotObject; }, view: function (selection) { let component; diff --git a/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js b/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js index 8cd6bc78d2..2e64675040 100644 --- a/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js +++ b/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js @@ -12,12 +12,10 @@ export default function StackedPlotsInspectorViewProvider(openmct) { } const object = selection[0][0].context.item; - const parent = selection[0].length > 1 && selection[0][1].context.item; - const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay'; - const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked'; + const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked'; - return !isOverlayPlotObject && isParentStackedPlotObject; + return isStackedPlotObject; }, view: function (selection) { let component; diff --git a/src/plugins/plot/legend/PlotLegend.vue b/src/plugins/plot/legend/PlotLegend.vue index 01054b1958..9b954e32bd 100644 --- a/src/plugins/plot/legend/PlotLegend.vue +++ b/src/plugins/plot/legend/PlotLegend.vue @@ -49,10 +49,10 @@ title="Cursor is point locked. Click anywhere in the plot to unlock." >
@@ -95,11 +95,10 @@ @@ -111,6 +110,9 @@ diff --git a/src/ui/inspector/annotations/AnnotationsInspectorView.vue b/src/ui/inspector/annotations/AnnotationsInspectorView.vue index a1b20b2970..db9dc17148 100644 --- a/src/ui/inspector/annotations/AnnotationsInspectorView.vue +++ b/src/ui/inspector/annotations/AnnotationsInspectorView.vue @@ -111,25 +111,31 @@ export default { return this?.selection?.[0]?.[0]?.context?.item; }, targetDetails() { - return this?.selection?.[0]?.[1]?.context?.targetDetails ?? {}; + return this?.selection?.[0]?.[0]?.context?.targetDetails ?? {}; }, shouldShowTagsEditor() { - return Object.keys(this.targetDetails).length > 0; + const showingTagsEditor = Object.keys(this.targetDetails).length > 0; + + if (showingTagsEditor) { + return true; + } + + return false; }, targetDomainObjects() { - return this?.selection?.[0]?.[1]?.context?.targetDomainObjects ?? {}; + return this?.selection?.[0]?.[0]?.context?.targetDomainObjects ?? {}; }, selectedAnnotations() { - return this?.selection?.[0]?.[1]?.context?.annotations; + return this?.selection?.[0]?.[0]?.context?.annotations; }, annotationType() { - return this?.selection?.[0]?.[1]?.context?.annotationType; + return this?.selection?.[0]?.[0]?.context?.annotationType; }, annotationFilter() { - return this?.selection?.[0]?.[1]?.context?.annotationFilter; + return this?.selection?.[0]?.[0]?.context?.annotationFilter; }, onAnnotationChange() { - return this?.selection?.[0]?.[1]?.context?.onAnnotationChange; + return this?.selection?.[0]?.[0]?.context?.onAnnotationChange; } }, async mounted() { @@ -195,6 +201,7 @@ export default { } }, async loadAnnotationForTargetObject(target) { + console.debug(`📝 Loading annotations for target`, target); const targetID = this.openmct.objects.makeKeyString(target.identifier); const allAnnotationsForTarget = await this.openmct.annotation.getAnnotations(target.identifier); const filteredAnnotationsForSelection = allAnnotationsForTarget.filter(annotation => { diff --git a/src/ui/inspector/annotations/annotation-inspector.scss b/src/ui/inspector/annotations/annotation-inspector.scss deleted file mode 100644 index af58979e4b..0000000000 --- a/src/ui/inspector/annotations/annotation-inspector.scss +++ /dev/null @@ -1,18 +0,0 @@ -.c-inspect-annotations { - > * + * { - margin-top: $interiorMargin; - } - - &__content{ - > * + * { - margin-top: $interiorMargin; - } - } - - &__content { - display: flex; - flex-direction: column; - } -} - - diff --git a/src/ui/layout/search/AnnotationSearchResult.vue b/src/ui/layout/search/AnnotationSearchResult.vue index f2eeff5075..d97f656339 100644 --- a/src/ui/layout/search/AnnotationSearchResult.vue +++ b/src/ui/layout/search/AnnotationSearchResult.vue @@ -150,16 +150,11 @@ export default { }); const selection = [ - { - element: this.openmct.layout.$refs.browseObject.$el, - context: { - item: this.result - } - }, { element: this.$el, context: { - type: 'plot-points-selection', + item: this.result.targetModels[0], + type: 'plot-annotation-search-result', targetDetails, targetDomainObjects, annotations: [this.result], From 0382d22f7f7d075c3250883b509dafb902682d1e Mon Sep 17 00:00:00 2001 From: Jamie V Date: Wed, 1 Feb 2023 11:55:08 -0800 Subject: [PATCH 136/594] [Notebook] Entry links tests (#6190) * removing dupe nb install, adding whitelist nb init script, testing whitelist urls * updating from copy * addressing PR comments for cleaner tests * removing .only * added a secure url test and a subdomain url test and simplified some code * not messin with protocols atm * update variable name --- e2e/helper/addInitNotebookWithUrls.js | 32 ++++ .../plugins/notebook/notebook.e2e.spec.js | 155 +++++++++++------- src/plugins/notebook/plugin.js | 6 - 3 files changed, 130 insertions(+), 63 deletions(-) create mode 100644 e2e/helper/addInitNotebookWithUrls.js diff --git a/e2e/helper/addInitNotebookWithUrls.js b/e2e/helper/addInitNotebookWithUrls.js new file mode 100644 index 0000000000..0af7d7b60b --- /dev/null +++ b/e2e/helper/addInitNotebookWithUrls.js @@ -0,0 +1,32 @@ +/***************************************************************************** + * 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 should be used to install the re-instal default Notebook plugin with a simple url whitelist. +// e.g. +// await page.addInitScript({ path: path.join(__dirname, 'addInitNotebookWithUrls.js') }); +const NOTEBOOK_NAME = 'Notebook'; +const URL_WHITELIST = ['google.com']; + +document.addEventListener('DOMContentLoaded', () => { + const openmct = window.openmct; + openmct.install(openmct.plugins.Notebook(NOTEBOOK_NAME, URL_WHITELIST)); +}); diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index 8d867e7517..cd73405757 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -25,8 +25,11 @@ This test suite is dedicated to tests which verify the basic operations surround */ const { test, expect } = require('../../../../pluginFixtures'); -const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions'); +const { createDomainObjectWithDefaults } = require('../../../../appActions'); const nbUtils = require('../../../../helper/notebookUtils'); +const path = require('path'); + +const NOTEBOOK_NAME = 'Notebook'; test.describe('Notebook CRUD Operations', () => { test.fixme('Can create a Notebook Object', async ({ page }) => { @@ -73,8 +76,7 @@ test.describe('Notebook section tests', () => { // Create Notebook await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Test Notebook" + type: NOTEBOOK_NAME }); }); test('Default and new sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => { @@ -135,8 +137,7 @@ test.describe('Notebook page tests', () => { // Create Notebook await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Test Notebook" + type: NOTEBOOK_NAME }); }); //Test will need to be implemented after a refactor in #5713 @@ -207,24 +208,30 @@ test.describe('Notebook search tests', () => { }); test.describe('Notebook entry tests', () => { + // Create Notebook with URL Whitelist + let notebookObject; + test.beforeEach(async ({ page }) => { + // eslint-disable-next-line no-undef + await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitNotebookWithUrls.js') }); + await page.goto('./', { waitUntil: 'networkidle' }); + + notebookObject = await createDomainObjectWithDefaults(page, { + type: NOTEBOOK_NAME + }); + }); test.fixme('When a new entry is created, it should be focused', async ({ page }) => {}); test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => { - await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); - - // Create Notebook - const notebook = await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Embed Test Notebook" - }); // Create Overlay Plot await createDomainObjectWithDefaults(page, { - type: 'Overlay Plot', - name: "Dropped Overlay Plot" + type: 'Overlay Plot' }); - await expandTreePaneItemByName(page, 'My Items'); + // Navigate to the notebook object + await page.goto(notebookObject.url); + + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); - await page.goto(notebook.url); await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area'); const embed = page.locator('.c-ne__embed__link'); @@ -234,22 +241,16 @@ test.describe('Notebook entry tests', () => { expect(embedName).toBe('Dropped Overlay Plot'); }); test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => { - await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); - - // Create Notebook - const notebook = await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Embed Test Notebook" - }); // Create Overlay Plot await createDomainObjectWithDefaults(page, { - type: 'Overlay Plot', - name: "Dropped Overlay Plot" + type: 'Overlay Plot' }); - await expandTreePaneItemByName(page, 'My Items'); + // Navigate to the notebook object + await page.goto(notebookObject.url); - await page.goto(notebook.url); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); await nbUtils.enterTextEntry(page, 'Entry to drop into'); await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', 'text=Entry to drop into'); @@ -263,19 +264,14 @@ test.describe('Notebook entry tests', () => { }); test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); test.fixme('previous and new entries can be deleted', async ({ page }) => {}); - test.fixme('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { + test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { const TEST_LINK = 'http://www.google.com'; - await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); - // Create Notebook - const notebook = await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Entry Link Test" - }); + // Navigate to the notebook object + await page.goto(notebookObject.url); - await expandTreePaneItemByName(page, 'My Items'); - - await page.goto(notebook.url); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); @@ -293,19 +289,14 @@ test.describe('Notebook entry tests', () => { expect(await validLink.count()).toBe(1); }); - test.fixme('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => { + test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => { const TEST_LINK = 'www.google.com'; - await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); - // Create Notebook - const notebook = await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Entry Link Test" - }); + // Navigate to the notebook object + await page.goto(notebookObject.url); - await expandTreePaneItemByName(page, 'My Items'); - - await page.goto(notebook.url); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); @@ -313,20 +304,70 @@ test.describe('Notebook entry tests', () => { expect(await invalidLink.count()).toBe(0); }); - test.fixme('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ page }) => { + test('when a link is entered, but it is not in the whitelisted urls, it does not become clickable when viewing', async ({ page }) => { + const TEST_LINK = 'http://www.bing.com'; + + // Navigate to the notebook object + await page.goto(notebookObject.url); + + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); + + await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); + + const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); + + expect(await invalidLink.count()).toBe(0); + }); + test('when a valid link with a subdomain and a valid domain in the whitelisted urls is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { + const INVALID_TEST_LINK = 'http://bing.google.com'; + + // Navigate to the notebook object + await page.goto(notebookObject.url); + + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); + + await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`); + + const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`); + + expect(await validLink.count()).toBe(1); + }); + test('when a valid secure link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { + const TEST_LINK = 'https://www.google.com'; + + // Navigate to the notebook object + await page.goto(notebookObject.url); + + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); + + await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); + + const validLink = page.locator(`a[href="${TEST_LINK}"]`); + + // Start waiting for popup before clicking. Note no await. + const popupPromise = page.waitForEvent('popup'); + + await validLink.click(); + const popup = await popupPromise; + + // Wait for the popup to load. + await popup.waitForLoadState(); + expect.soft(popup.url()).toContain('www.google.com'); + + expect(await validLink.count()).toBe(1); + }); + test('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ page }) => { const TEST_LINK = 'http://www.google.com?bad='; const TEST_LINK_BAD = `http://www.google.com?bad=`; - await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); - // Create Notebook - const notebook = await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Entry Link Test" - }); + // Navigate to the notebook object + await page.goto(notebookObject.url); - await expandTreePaneItemByName(page, 'My Items'); - - await page.goto(notebook.url); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); await nbUtils.enterTextEntry(page, `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`); diff --git a/src/plugins/notebook/plugin.js b/src/plugins/notebook/plugin.js index f24742644b..41b379fc98 100644 --- a/src/plugins/notebook/plugin.js +++ b/src/plugins/notebook/plugin.js @@ -105,10 +105,6 @@ function installBaseNotebookFunctionality(openmct) { function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) { return function install(openmct) { - if (openmct[NOTEBOOK_INSTALLED_KEY]) { - return; - } - const icon = 'icon-notebook'; const description = 'Create and save timestamped notes with embedded object snapshots.'; const snapshotContainer = getSnapshotContainer(openmct); @@ -122,8 +118,6 @@ function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) { openmct.objectViews.addProvider(notebookView, entryUrlWhitelist); installBaseNotebookFunctionality(openmct); - - openmct[NOTEBOOK_INSTALLED_KEY] = true; }; } From c1c1d879536483e875366f6dd2841aa00b4f72a2 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Wed, 1 Feb 2023 13:46:15 -0800 Subject: [PATCH 137/594] Fix multiple y axis issues (#6204) * Ensure enabling log mode does not reset series that don't belong to that yaxis. propagate both left and right y axes widths so that plots can adjust accordingly * Revert code Handle second axis resizing * Fixes issue where logMode was getting initialized incorrectly for multiple y axes * Get the yAxisId of the series from the model. * Address review comments - rename params for readability * Fix number of log ticks expected and the tick values since we reduced the number of secondary ticks * Fix log plot test * Add guard code during destroy * Add missing remove callback --- .../plugins/plot/logPlot.e2e.spec.js | 37 +++------- src/plugins/plot/MctPlot.vue | 72 ++++++++++++++----- src/plugins/plot/MctTicks.vue | 4 +- src/plugins/plot/axis/XAxis.vue | 2 +- src/plugins/plot/axis/YAxis.vue | 16 +++-- src/plugins/plot/configuration/PlotSeries.js | 13 +++- src/plugins/plot/configuration/YAxisModel.js | 3 +- src/plugins/plot/legend/PlotLegend.vue | 7 ++ src/plugins/plot/stackedPlot/StackedPlot.vue | 39 ++++++++-- .../plot/stackedPlot/StackedPlotItem.vue | 49 ++++++++++--- 10 files changed, 169 insertions(+), 73 deletions(-) diff --git a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js index e923b15bb4..ca0689ce0e 100644 --- a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js @@ -160,35 +160,16 @@ async function testRegularTicks(page) { */ async function testLogTicks(page) { const yTicks = await page.locator('.gl-plot-y-tick-label'); - expect(await yTicks.count()).toBe(28); + expect(await yTicks.count()).toBe(9); await expect(yTicks.nth(0)).toHaveText('-2.98'); - await expect(yTicks.nth(1)).toHaveText('-2.50'); - await expect(yTicks.nth(2)).toHaveText('-2.00'); - await expect(yTicks.nth(3)).toHaveText('-1.51'); - await expect(yTicks.nth(4)).toHaveText('-1.20'); - await expect(yTicks.nth(5)).toHaveText('-1.00'); - await expect(yTicks.nth(6)).toHaveText('-0.80'); - await expect(yTicks.nth(7)).toHaveText('-0.58'); - await expect(yTicks.nth(8)).toHaveText('-0.40'); - await expect(yTicks.nth(9)).toHaveText('-0.20'); - await expect(yTicks.nth(10)).toHaveText('-0.00'); - await expect(yTicks.nth(11)).toHaveText('0.20'); - await expect(yTicks.nth(12)).toHaveText('0.40'); - await expect(yTicks.nth(13)).toHaveText('0.58'); - await expect(yTicks.nth(14)).toHaveText('0.80'); - await expect(yTicks.nth(15)).toHaveText('1.00'); - await expect(yTicks.nth(16)).toHaveText('1.20'); - await expect(yTicks.nth(17)).toHaveText('1.51'); - await expect(yTicks.nth(18)).toHaveText('2.00'); - await expect(yTicks.nth(19)).toHaveText('2.50'); - await expect(yTicks.nth(20)).toHaveText('2.98'); - await expect(yTicks.nth(21)).toHaveText('3.50'); - await expect(yTicks.nth(22)).toHaveText('4.00'); - await expect(yTicks.nth(23)).toHaveText('4.50'); - await expect(yTicks.nth(24)).toHaveText('5.31'); - await expect(yTicks.nth(25)).toHaveText('7.00'); - await expect(yTicks.nth(26)).toHaveText('8.00'); - await expect(yTicks.nth(27)).toHaveText('9.00'); + await expect(yTicks.nth(1)).toHaveText('-1.51'); + await expect(yTicks.nth(2)).toHaveText('-0.58'); + await expect(yTicks.nth(3)).toHaveText('-0.00'); + await expect(yTicks.nth(4)).toHaveText('0.58'); + await expect(yTicks.nth(5)).toHaveText('1.51'); + await expect(yTicks.nth(6)).toHaveText('2.98'); + await expect(yTicks.nth(7)).toHaveText('5.31'); + await expect(yTicks.nth(8)).toHaveText('9.00'); } /** diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 2466e0798a..49fc537c88 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -34,13 +34,14 @@ v-for="(yAxis, index) in yAxesIds" :id="yAxis.id" :key="`yAxis-${yAxis.id}-${index}`" - :multiple-left-axes="multipleLeftAxes" + :has-multiple-left-axes="hasMultipleLeftAxes" :position="yAxis.id > 2 ? 'right' : 'left'" :class="{'plot-yaxis-right': yAxis.id > 2}" :tick-width="yAxis.tickWidth" + :used-tick-width="plotFirstLeftTickWidth" :plot-left-tick-width="yAxis.id > 2 ? yAxis.tickWidth: plotLeftTickWidth" @yKeyChanged="setYAxisKey" - @tickWidthChanged="onTickWidthChange" + @plotYTickWidth="onYTickWidthChange" @toggleAxisVisibility="toggleSeriesForYAxis" />
@@ -61,7 +62,6 @@ v-show="gridLines && !options.compact" :axis-type="'xAxis'" :position="'right'" - @plotTickWidth="onTickWidthChange" />
yAxis.id > 2); - const leftOffset = this.multipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING; + const leftOffset = this.hasMultipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING; let style = { left: `${this.plotLeftTickWidth + leftOffset}px` }; + const parentRightAxisWidth = this.parentYTickWidth.rightTickWidth; - if (rightAxis) { - style.right = `${rightAxis.tickWidth + AXES_PADDING}px`; + if (parentRightAxisWidth || rightAxis) { + style.right = `${(parentRightAxisWidth || rightAxis.tickWidth) + AXES_PADDING}px`; } return style; @@ -310,8 +315,8 @@ export default { yAxesIds() { return this.yAxes.filter(yAxis => yAxis.seriesCount > 0); }, - multipleLeftAxes() { - return this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1; + hasMultipleLeftAxes() { + return this.parentYTickWidth.hasMultipleLeftAxes || this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1; }, isNestedWithinAStackedPlot() { const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path)); @@ -325,6 +330,11 @@ export default { // only allow annotations viewing/editing if plot is paused or in fixed time mode return this.isFrozen || !this.isRealTime; }, + plotFirstLeftTickWidth() { + const firstYAxis = this.yAxes.find(yAxis => yAxis.id === 1); + + return firstYAxis ? firstYAxis.tickWidth : 0; + }, plotLeftTickWidth() { let leftTickWidth = 0; this.yAxes.forEach((yAxis) => { @@ -334,8 +344,9 @@ export default { leftTickWidth = leftTickWidth + yAxis.tickWidth; }); + const parentLeftTickWidth = this.parentYTickWidth.leftTickWidth; - return this.plotTickWidth || leftTickWidth; + return parentLeftTickWidth || leftTickWidth; } }, watch: { @@ -557,6 +568,14 @@ export default { updateTicksAndSeriesForYAxis(newAxisId, oldAxisId) { this.updateAxisUsageCount(oldAxisId, -1); this.updateAxisUsageCount(newAxisId, 1); + + const foundYAxis = this.yAxes.find(yAxis => yAxis.id === oldAxisId); + if (foundYAxis.seriesCount === 0) { + this.onYTickWidthChange({ + width: foundYAxis.tickWidth, + yAxisId: foundYAxis.id + }); + } }, updateAxisUsageCount(yAxisId, updateCountBy) { @@ -934,8 +953,13 @@ export default { } }, - onTickWidthChange(data, fromDifferentObject) { - const {width, yAxisId} = data; + /** + * Aggregate widths of all left and right y axes and send them up to any parent plots + * @param {Object} tickWidthWithYAxisId - the width and yAxisId of the tick bar + * @param fromDifferentObject + */ + onYTickWidthChange(tickWidthWithYAxisId, fromDifferentObject) { + const {width, yAxisId} = tickWidthWithYAxisId; if (yAxisId) { const index = this.yAxes.findIndex(yAxis => yAxis.id === yAxisId); if (fromDifferentObject) { @@ -944,13 +968,23 @@ export default { } else { // Otherwise, only accept tick with if it's larger. const newWidth = Math.max(width, this.yAxes[index].tickWidth); - if (newWidth !== this.yAxes[index].tickWidth) { + if (width !== this.yAxes[index].tickWidth) { this.yAxes[index].tickWidth = newWidth; } } const id = this.openmct.objects.makeKeyString(this.domainObject.identifier); - this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id); + const leftTickWidth = this.yAxes.filter(yAxis => yAxis.id < 3).reduce((previous, current) => { + return previous + current.tickWidth; + }, 0); + const rightTickWidth = this.yAxes.filter(yAxis => yAxis.id > 2).reduce((previous, current) => { + return previous + current.tickWidth; + }, 0); + this.$emit('plotYTickWidth', { + hasMultipleLeftAxes: this.hasMultipleLeftAxes, + leftTickWidth, + rightTickWidth + }, id); } }, @@ -1722,7 +1756,9 @@ export default { }, destroy() { - configStore.deleteStore(this.config.id); + if (this.config) { + configStore.deleteStore(this.config.id); + } this.stopListening(); diff --git a/src/plugins/plot/MctTicks.vue b/src/plugins/plot/MctTicks.vue index 755678fc70..3ceecdeb70 100644 --- a/src/plugins/plot/MctTicks.vue +++ b/src/plugins/plot/MctTicks.vue @@ -86,6 +86,8 @@ import eventHelpers from "./lib/eventHelpers"; import { ticks, getLogTicks, getFormattedTicks } from "./tickUtils"; import configStore from "./configuration/ConfigStore"; +const SECONDARY_TICK_NUMBER = 2; + export default { inject: ['openmct', 'domainObject'], props: { @@ -205,7 +207,7 @@ export default { } if (this.axisType === 'yAxis' && this.axis.get('logMode')) { - return getLogTicks(range.min, range.max, number, 4); + return getLogTicks(range.min, range.max, number, SECONDARY_TICK_NUMBER); } else { return ticks(range.min, range.max, number); } diff --git a/src/plugins/plot/axis/XAxis.vue b/src/plugins/plot/axis/XAxis.vue index a6bc176aa1..1b92c339ca 100644 --- a/src/plugins/plot/axis/XAxis.vue +++ b/src/plugins/plot/axis/XAxis.vue @@ -152,7 +152,7 @@ export default { this.selectedXKeyOptionKey = this.xKeyOptions.length > 0 ? this.getXKeyOption(xAxisKey).key : xAxisKey; }, onTickWidthChange(width) { - this.$emit('tickWidthChanged', width); + this.$emit('plotXTickWidth', width); } } }; diff --git a/src/plugins/plot/axis/YAxis.vue b/src/plugins/plot/axis/YAxis.vue index 215465fc73..2f55ef5a64 100644 --- a/src/plugins/plot/axis/YAxis.vue +++ b/src/plugins/plot/axis/YAxis.vue @@ -101,7 +101,13 @@ export default { return 0; } }, - multipleLeftAxes: { + usedTickWidth: { + type: Number, + default() { + return 0; + } + }, + hasMultipleLeftAxes: { type: Boolean, default() { return false; @@ -138,14 +144,14 @@ export default { let style = { width: `${this.tickWidth + AXIS_PADDING}px` }; - const multipleAxesPadding = this.multipleLeftAxes ? AXIS_PADDING : 0; + const multipleAxesPadding = this.hasMultipleLeftAxes ? AXIS_PADDING : 0; if (this.position === 'right') { style.left = `-${this.tickWidth + AXIS_PADDING}px`; } else { const thisIsTheSecondLeftAxis = (this.id - 1) > 0; - if (this.multipleLeftAxes && thisIsTheSecondLeftAxis) { - style.left = 0; + if (this.hasMultipleLeftAxes && thisIsTheSecondLeftAxis) { + style.left = `${this.plotLeftTickWidth - this.usedTickWidth - this.tickWidth}px`; style['border-right'] = `1px solid`; } else { style.left = `${ this.plotLeftTickWidth - this.tickWidth + multipleAxesPadding}px`; @@ -256,7 +262,7 @@ export default { } }, onTickWidthChange(data) { - this.$emit('tickWidthChanged', { + this.$emit('plotYTickWidth', { width: data.width, yAxisId: this.id }); diff --git a/src/plugins/plot/configuration/PlotSeries.js b/src/plugins/plot/configuration/PlotSeries.js index a0f32be54f..ac9a2ee724 100644 --- a/src/plugins/plot/configuration/PlotSeries.js +++ b/src/plugins/plot/configuration/PlotSeries.js @@ -73,7 +73,7 @@ export default class PlotSeries extends Model { super(options); - this.logMode = options.collection.plot.model.yAxis.logMode; + this.logMode = this.getLogMode(options); this.listenTo(this, 'change:xKey', this.onXKeyChange, this); this.listenTo(this, 'change:yKey', this.onYKeyChange, this); @@ -87,6 +87,17 @@ export default class PlotSeries extends Model { this.unPlottableValues = [undefined, Infinity, -Infinity]; } + getLogMode(options) { + const yAxisId = this.get('yAxisId'); + if (yAxisId === 1) { + return options.collection.plot.model.yAxis.logMode; + } else { + const foundYAxis = options.collection.plot.model.additionalYAxes.find(yAxis => yAxis.id === yAxisId); + + return foundYAxis ? foundYAxis.logMode : false; + } + } + /** * Set defaults for telemetry series. * @param {import('./Model').ModelOptions} options diff --git a/src/plugins/plot/configuration/YAxisModel.js b/src/plugins/plot/configuration/YAxisModel.js index 80c0bf3f44..1107b8e803 100644 --- a/src/plugins/plot/configuration/YAxisModel.js +++ b/src/plugins/plot/configuration/YAxisModel.js @@ -287,7 +287,8 @@ export default class YAxisModel extends Model { this.resetSeries(); } resetSeries() { - this.plot.series.forEach((plotSeries) => { + const series = this.getSeriesForYAxis(this.seriesCollection); + series.forEach((plotSeries) => { plotSeries.logMode = this.get('logMode'); plotSeries.reset(plotSeries.getSeriesData()); }); diff --git a/src/plugins/plot/legend/PlotLegend.vue b/src/plugins/plot/legend/PlotLegend.vue index 9b954e32bd..2f5b003ad0 100644 --- a/src/plugins/plot/legend/PlotLegend.vue +++ b/src/plugins/plot/legend/PlotLegend.vue @@ -207,6 +207,13 @@ export default { this.registerListeners(config); } }, + removeTelemetryObject(identifier) { + const configId = this.openmct.objects.makeKeyString(identifier); + const config = configStore.get(configId); + if (config) { + config.series.forEach(this.removeSeries, this); + } + }, registerListeners(config) { //listen to any changes to the telemetry endpoints that are associated with the child this.listenTo(config.series, 'add', this.addSeries, this); diff --git a/src/plugins/plot/stackedPlot/StackedPlot.vue b/src/plugins/plot/stackedPlot/StackedPlot.vue index 5636ccd691..961cb32245 100644 --- a/src/plugins/plot/stackedPlot/StackedPlot.vue +++ b/src/plugins/plot/stackedPlot/StackedPlot.vue @@ -47,8 +47,8 @@ :color-palette="colorPalette" :cursor-guide="cursorGuide" :show-limit-line-labels="showLimitLineLabels" - :plot-tick-width="maxTickWidth" - @plotTickWidth="onTickWidthChange" + :parent-y-tick-width="maxTickWidth" + @plotYTickWidth="onYTickWidthChange" @loadingUpdated="loadingUpdated" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @@ -113,8 +113,21 @@ export default { return 'plot-legend-collapsed'; } }, + /** + * Returns the maximum width of the left and right y axes ticks of this stacked plots children + * @returns {{rightTickWidth: number, leftTickWidth: number, hasMultipleLeftAxes: boolean}} + */ maxTickWidth() { - return Math.max(...Object.values(this.tickWidthMap)); + const tickWidthValues = Object.values(this.tickWidthMap); + const maxLeftTickWidth = Math.max(...tickWidthValues.map(tickWidthItem => tickWidthItem.leftTickWidth)); + const maxRightTickWidth = Math.max(...tickWidthValues.map(tickWidthItem => tickWidthItem.rightTickWidth)); + const hasMultipleLeftAxes = tickWidthValues.some(tickWidthItem => tickWidthItem.hasMultipleLeftAxes === true); + + return { + leftTickWidth: maxLeftTickWidth, + rightTickWidth: maxRightTickWidth, + hasMultipleLeftAxes + }; } }, beforeDestroy() { @@ -175,7 +188,10 @@ export default { addChild(child) { const id = this.openmct.objects.makeKeyString(child.identifier); - this.$set(this.tickWidthMap, id, 0); + this.$set(this.tickWidthMap, id, { + leftTickWidth: 0, + rightTickWidth: 0 + }); this.compositionObjects.push({ object: child, @@ -231,7 +247,10 @@ export default { resetTelemetryAndTicks(domainObject) { this.compositionObjects = []; - this.tickWidthMap = {}; + this.tickWidthMap = { + leftTickWidth: 0, + rightTickWidth: 0 + }; }, exportJPG() { @@ -254,12 +273,18 @@ export default { this.hideExportButtons = false; }.bind(this)); }, - onTickWidthChange(width, plotId) { + /** + * @typedef {Object} PlotYTickData + * @property {Number} leftTickWidth the width of the ticks for all the y axes on the left of the plot. + * @property {Number} rightTickWidth the width of the ticks for all the y axes on the right of the plot. + * @property {Boolean} hasMultipleLeftAxes whether or not there is more than one left y axis. + */ + onYTickWidthChange(data, plotId) { if (!Object.prototype.hasOwnProperty.call(this.tickWidthMap, plotId)) { return; } - this.$set(this.tickWidthMap, plotId, width); + this.$set(this.tickWidthMap, plotId, data); }, legendHoverChanged(data) { this.showLimitLineLabels = data; diff --git a/src/plugins/plot/stackedPlot/StackedPlotItem.vue b/src/plugins/plot/stackedPlot/StackedPlotItem.vue index 64409db55a..0213a465af 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotItem.vue +++ b/src/plugins/plot/stackedPlot/StackedPlotItem.vue @@ -72,10 +72,14 @@ export default { return undefined; } }, - plotTickWidth: { - type: Number, + parentYTickWidth: { + type: Object, default() { - return 0; + return { + leftTickWidth: 0, + rightTickWidth: 0, + hasMultipleLeftAxes: false + }; } } }, @@ -86,8 +90,8 @@ export default { cursorGuide(newCursorGuide) { this.updateComponentProp('cursorGuide', newCursorGuide); }, - plotTickWidth(width) { - this.updateComponentProp('plotTickWidth', width); + parentYTickWidth(width) { + this.updateComponentProp('parentYTickWidth', width); }, showLimitLineLabels: { handler(data) { @@ -121,7 +125,7 @@ export default { this.$el.innerHTML = ''; } - const onTickWidthChange = this.onTickWidthChange; + const onYTickWidthChange = this.onYTickWidthChange; const onLockHighlightPointUpdated = this.onLockHighlightPointUpdated; const onHighlightsUpdated = this.onHighlightsUpdated; const onConfigLoaded = this.onConfigLoaded; @@ -158,7 +162,7 @@ export default { data() { return { ...getProps(), - onTickWidthChange, + onYTickWidthChange, onLockHighlightPointUpdated, onHighlightsUpdated, onConfigLoaded, @@ -174,7 +178,30 @@ export default { this.loading = loaded; } }, - template: '
' + template: ` +
+ + +
` }); }, onLockHighlightPointUpdated() { @@ -186,8 +213,8 @@ export default { onConfigLoaded() { this.$emit('configLoaded', ...arguments); }, - onTickWidthChange() { - this.$emit('plotTickWidth', ...arguments); + onYTickWidthChange() { + this.$emit('plotYTickWidth', ...arguments); }, onCursorGuideChange() { this.$emit('cursorGuide', ...arguments); @@ -204,7 +231,7 @@ export default { limitLineLabels: this.showLimitLineLabels, gridLines: this.gridLines, cursorGuide: this.cursorGuide, - plotTickWidth: this.plotTickWidth, + parentYTickWidth: this.parentYTickWidth, options: this.options, status: this.status, colorPalette: this.colorPalette, From c1e8c7915c4daaa6946bd5dbfe3cca49df9d2ce1 Mon Sep 17 00:00:00 2001 From: Jamie V Date: Wed, 1 Feb 2023 14:06:54 -0800 Subject: [PATCH 138/594] [Staleness] Fix removed object error and clean up (#6241) * fixing error from plots when removing swg and making methods and props private for swg staleness provider * removing unsubscribes from destroy hooks if the item has been removed already and reverting an unneccesary change * checking for undefined staleness response * removed un-neccesary code --- .../generator/SinewaveStalenessProvider.js | 114 +++++++++--------- .../LADTable/components/LadTableSet.vue | 1 + .../components/ConditionCollection.vue | 5 +- .../criterion/AllTelemetryCriterion.js | 5 + src/plugins/plot/Plot.vue | 1 + src/plugins/telemetryTable/TelemetryTable.js | 1 + 6 files changed, 72 insertions(+), 55 deletions(-) diff --git a/example/generator/SinewaveStalenessProvider.js b/example/generator/SinewaveStalenessProvider.js index 3ebf4570e1..0fafcc14c2 100644 --- a/example/generator/SinewaveStalenessProvider.js +++ b/example/generator/SinewaveStalenessProvider.js @@ -23,14 +23,18 @@ import EventEmitter from 'EventEmitter'; export default class SinewaveLimitProvider extends EventEmitter { + #openmct; + #observingStaleness; + #watchingTheClock; + #isRealTime; constructor(openmct) { super(); - this.openmct = openmct; - this.observingStaleness = {}; - this.watchingTheClock = false; - this.isRealTime = undefined; + this.#openmct = openmct; + this.#observingStaleness = {}; + this.#watchingTheClock = false; + this.#isRealTime = undefined; } supportsStaleness(domainObject) { @@ -38,114 +42,116 @@ export default class SinewaveLimitProvider extends EventEmitter { } isStale(domainObject, options) { - if (!this.providingStaleness(domainObject)) { + if (!this.#providingStaleness(domainObject)) { return Promise.resolve({ isStale: false, utc: 0 }); } - const id = this.getObjectKeyString(domainObject); + const id = this.#getObjectKeyString(domainObject); - if (!this.observerExists(id)) { - this.createObserver(id); + if (!this.#observerExists(id)) { + this.#createObserver(id); } - return Promise.resolve(this.observingStaleness[id].isStale); + return Promise.resolve(this.#observingStaleness[id].isStale); } subscribeToStaleness(domainObject, callback) { - const id = this.getObjectKeyString(domainObject); + const id = this.#getObjectKeyString(domainObject); - if (this.isRealTime === undefined) { - this.updateRealTime(this.openmct.time.clock()); + if (this.#isRealTime === undefined) { + this.#updateRealTime(this.#openmct.time.clock()); } - this.handleClockUpdate(); + this.#handleClockUpdate(); - if (this.observerExists(id)) { - this.addCallbackToObserver(id, callback); + if (this.#observerExists(id)) { + this.#addCallbackToObserver(id, callback); } else { - this.createObserver(id, callback); + this.#createObserver(id, callback); } const intervalId = setInterval(() => { - if (this.providingStaleness(domainObject)) { - this.updateStaleness(id, !this.observingStaleness[id].isStale); + if (this.#providingStaleness(domainObject)) { + this.#updateStaleness(id, !this.#observingStaleness[id].isStale); } }, 10000); return () => { clearInterval(intervalId); - this.updateStaleness(id, false); - this.handleClockUpdate(); - this.destroyObserver(id); + this.#updateStaleness(id, false); + this.#handleClockUpdate(); + this.#destroyObserver(id); }; } - handleClockUpdate() { - let observers = Object.values(this.observingStaleness).length > 0; + #handleClockUpdate() { + let observers = Object.values(this.#observingStaleness).length > 0; - if (observers && !this.watchingTheClock) { - this.watchingTheClock = true; - this.openmct.time.on('clock', this.updateRealTime, this); - } else if (!observers && this.watchingTheClock) { - this.watchingTheClock = false; - this.openmct.time.off('clock', this.updateRealTime, this); + if (observers && !this.#watchingTheClock) { + this.#watchingTheClock = true; + this.#openmct.time.on('clock', this.#updateRealTime, this); + } else if (!observers && this.#watchingTheClock) { + this.#watchingTheClock = false; + this.#openmct.time.off('clock', this.#updateRealTime, this); } } - updateRealTime(clock) { - this.isRealTime = clock !== undefined; + #updateRealTime(clock) { + this.#isRealTime = clock !== undefined; - if (!this.isRealTime) { - Object.keys(this.observingStaleness).forEach((id) => { - this.updateStaleness(id, false); + if (!this.#isRealTime) { + Object.keys(this.#observingStaleness).forEach((id) => { + this.#updateStaleness(id, false); }); } } - updateStaleness(id, isStale) { - this.observingStaleness[id].isStale = isStale; - this.observingStaleness[id].utc = Date.now(); - this.observingStaleness[id].callback({ - isStale: this.observingStaleness[id].isStale, - utc: this.observingStaleness[id].utc + #updateStaleness(id, isStale) { + this.#observingStaleness[id].isStale = isStale; + this.#observingStaleness[id].utc = Date.now(); + this.#observingStaleness[id].callback({ + isStale: this.#observingStaleness[id].isStale, + utc: this.#observingStaleness[id].utc }); this.emit('stalenessEvent', { id, - isStale: this.observingStaleness[id].isStale + isStale: this.#observingStaleness[id].isStale }); } - createObserver(id, callback) { - this.observingStaleness[id] = { + #createObserver(id, callback) { + this.#observingStaleness[id] = { isStale: false, utc: Date.now() }; if (typeof callback === 'function') { - this.addCallbackToObserver(id, callback); + this.#addCallbackToObserver(id, callback); } } - destroyObserver(id) { - delete this.observingStaleness[id]; + #destroyObserver(id) { + if (this.#observingStaleness[id]) { + delete this.#observingStaleness[id]; + } } - providingStaleness(domainObject) { - return domainObject.telemetry?.staleness === true && this.isRealTime; + #providingStaleness(domainObject) { + return domainObject.telemetry?.staleness === true && this.#isRealTime; } - getObjectKeyString(object) { - return this.openmct.objects.makeKeyString(object.identifier); + #getObjectKeyString(object) { + return this.#openmct.objects.makeKeyString(object.identifier); } - addCallbackToObserver(id, callback) { - this.observingStaleness[id].callback = callback; + #addCallbackToObserver(id, callback) { + this.#observingStaleness[id].callback = callback; } - observerExists(id) { - return this.observingStaleness?.[id]; + #observerExists(id) { + return this.#observingStaleness?.[id]; } } diff --git a/src/plugins/LADTable/components/LadTableSet.vue b/src/plugins/LADTable/components/LadTableSet.vue index 9bba9e8caa..3a3b000553 100644 --- a/src/plugins/LADTable/components/LadTableSet.vue +++ b/src/plugins/LADTable/components/LadTableSet.vue @@ -218,6 +218,7 @@ export default { this.stalenessSubscription[keystring].unsubscribe(); this.stalenessSubscription[keystring].stalenessUtils.destroy(); this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK); + delete this.stalenessSubscription[keystring]; }; }, handleStaleness(id, stalenessResponse, skipCheck = false) { diff --git a/src/plugins/condition/components/ConditionCollection.vue b/src/plugins/condition/components/ConditionCollection.vue index c05bfc0939..f5336952c7 100644 --- a/src/plugins/condition/components/ConditionCollection.vue +++ b/src/plugins/condition/components/ConditionCollection.vue @@ -232,7 +232,9 @@ export default { this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils(this.openmct, domainObject); this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => { - this.hanldeStaleness(keyString, stalenessResponse); + if (stalenessResponse !== undefined) { + this.hanldeStaleness(keyString, stalenessResponse); + } }); const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => { this.hanldeStaleness(keyString, stalenessResponse); @@ -259,6 +261,7 @@ export default { keyString, isStale: false }); + delete this.stalenessSubscription[keyString]; } }, hanldeStaleness(keyString, stalenessResponse) { diff --git a/src/plugins/condition/criterion/AllTelemetryCriterion.js b/src/plugins/condition/criterion/AllTelemetryCriterion.js index 316451dae3..c9ee3db8b9 100644 --- a/src/plugins/condition/criterion/AllTelemetryCriterion.js +++ b/src/plugins/condition/criterion/AllTelemetryCriterion.js @@ -83,6 +83,11 @@ export default class AllTelemetryCriterion extends TelemetryCriterion { if (!this.stalenessSubscription[id]) { this.stalenessSubscription[id] = {}; this.stalenessSubscription[id].stalenessUtils = new StalenessUtils(this.openmct, telemetryObject); + this.openmct.telemetry.isStale(telemetryObject).then((stalenessResponse) => { + if (stalenessResponse !== undefined) { + this.handleStaleTelemetry(id, stalenessResponse); + } + }); this.stalenessSubscription[id].unsubscribe = this.openmct.telemetry.subscribeToStaleness( telemetryObject, (stalenessResponse) => { diff --git a/src/plugins/plot/Plot.vue b/src/plugins/plot/Plot.vue index 5140e7fa4c..6bada906e1 100644 --- a/src/plugins/plot/Plot.vue +++ b/src/plugins/plot/Plot.vue @@ -166,6 +166,7 @@ export default { this.stalenessSubscription[keystring].unsubscribe(); this.stalenessSubscription[keystring].stalenessUtils.destroy(); this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK); + delete this.stalenessSubscription[keystring]; }, handleStaleness(id, stalenessResponse, skipCheck = false) { if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse, id)) { diff --git a/src/plugins/telemetryTable/TelemetryTable.js b/src/plugins/telemetryTable/TelemetryTable.js index 2db58858ec..67178a6d11 100644 --- a/src/plugins/telemetryTable/TelemetryTable.js +++ b/src/plugins/telemetryTable/TelemetryTable.js @@ -293,6 +293,7 @@ define([ this.stalenessSubscription[keyString].unsubscribe(); this.stalenessSubscription[keyString].stalenessUtils.destroy(); this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK); + delete this.stalenessSubscription[keyString]; } clearData() { From 800062d37e937a3f1e840e95750042bd48ad5238 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 2 Feb 2023 15:50:37 -0800 Subject: [PATCH 139/594] fix: remove 1px padding and re-enable autoscale snapshot test (#6271) * style: remove 1px padding from plot legend item * test: re-enable autoscale snapshot test --- e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js | 2 +- src/styles/_legacy-plots.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js index 3a1e4e20d6..fb9a2e2a73 100644 --- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js @@ -32,7 +32,7 @@ test.use({ } }); -test.fixme('ExportAsJSON', () => { +test.describe('ExportAsJSON', () => { test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => { const { myItemsFolderName } = openmctConfig; diff --git a/src/styles/_legacy-plots.scss b/src/styles/_legacy-plots.scss index 3a67c6e253..a13339036f 100644 --- a/src/styles/_legacy-plots.scss +++ b/src/styles/_legacy-plots.scss @@ -664,7 +664,6 @@ mct-plot { border-radius: $smallCr; display: flex; justify-content: stretch; - padding: 1px; .plot-series-swatch-and-name, .plot-series-value { From 422b7f3e0993f4324f39b0ed6479e476f0768840 Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Thu, 2 Feb 2023 17:18:41 -0800 Subject: [PATCH 140/594] Compass rose rotation fixes (#6260) --- .../components/Compass/CompassRose.vue | 85 +++++++++---------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/src/plugins/imagery/components/Compass/CompassRose.vue b/src/plugins/imagery/components/Compass/CompassRose.vue index 958bd3779b..2b4a17f480 100644 --- a/src/plugins/imagery/components/Compass/CompassRose.vue +++ b/src/plugins/imagery/components/Compass/CompassRose.vue @@ -107,48 +107,53 @@ height="100" /> - - - - - - - - - + + + - - + + + + + + + - + @@ -305,7 +310,7 @@ export default { return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` }; }, camGimbalAngleStyle() { - const rotation = rotate(this.north, this.heading); + const rotation = rotate(this.heading); return { transform: `rotate(${ rotation }deg)` @@ -332,14 +337,6 @@ export default { hasHeading() { return this.heading !== undefined; }, - headingStyle() { - /* Replaced with computed camGimbalStyle, but left here just in case. */ - const rotation = rotate(this.north, this.heading); - - return { - transform: `rotate(${ rotation }deg)` - }; - }, hasSunHeading() { return this.sunHeading !== undefined; }, From 0f312a88bb8ddfd839eb6286a20ceefe493350d2 Mon Sep 17 00:00:00 2001 From: Jamie V Date: Thu, 2 Feb 2023 18:16:45 -0800 Subject: [PATCH 141/594] [Notebook] Sanitize entries before save for extra protection (#6255) * Sanitizing before save as well to be be doubly safe --------- Co-authored-by: Andrew Henry --- src/plugins/notebook/components/NotebookEntry.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index 11015c0054..20e4f8784e 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -77,13 +77,13 @@ aria-label="Notebook Entry Input" tabindex="0" :contenteditable="canEdit" + v-bind.prop="formattedText" @mouseover="checkEditability($event)" @mouseleave="canEdit = true" @focus="editingEntry()" @blur="updateEntryValue($event)" @keydown.enter.exact.prevent @keyup.enter.exact.prevent="forceBlur($event)" - v-html="formattedText" >
@@ -250,7 +250,7 @@ export default { let text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA); if (this.editMode || !this.urlWhitelist) { - return text; + return { innerText: text }; } text = text.replace(URL_REGEX, (match) => { @@ -268,7 +268,7 @@ export default { return result; }); - return text; + return { innerHTML: text }; }, isSelectedEntry() { return this.selectedEntryId === this.entry.id; @@ -456,7 +456,7 @@ export default { this.editMode = false; const value = $event.target.innerText; if (value !== this.entry.text && value.match(/\S/)) { - this.entry.text = value; + this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA); this.timestampAndUpdate(); } else { this.$emit('cancelEdit'); From be38c3e6546e9d2217b1062cc12a419780e28a3c Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Fri, 3 Feb 2023 15:56:50 -0800 Subject: [PATCH 142/594] Fix stacked plot child selection (#6275) * Fix selections for different scenarios * Ensure plot selection in stacked plots works when there are no selected or found annotations * Adds e2e test for stacked plot selection and fixes the old e2e test which was testing overlay plots instead. * Fix selection of plots while in Edit mode * Improve tests for stacked plots * refactor: remove unnecessary `await`s * a11y: move aria-label to StackedPlotItem * refactor(e2e): combine like tests, unique object names - Use unique object names in `text=` selectors - Combine like tests to reduce execution time - Use `getByRole` selectors where able * docs(e2e): add comments to test * fix: add class back for unit test selector --------- Co-authored-by: Scott Bell Co-authored-by: Jesse Mazzella --- .../plugins/plot/stackedPlot.e2e.spec.js | 124 +++++++++++++----- src/plugins/plot/MctPlot.vue | 40 ++++-- .../plot/inspector/PlotOptionsBrowse.vue | 2 + .../plot/inspector/PlotOptionsEdit.vue | 1 + .../plot/inspector/forms/YAxisForm.vue | 5 +- .../plot/stackedPlot/StackedPlotItem.vue | 39 +++++- src/ui/inspector/InspectorViews.vue | 2 +- 7 files changed, 165 insertions(+), 48 deletions(-) diff --git a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js index 5d6c47f65c..509fe267eb 100644 --- a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js @@ -29,29 +29,39 @@ const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults } = require('../../../../appActions'); test.describe('Stacked Plot', () => { + let stackedPlot; + let swgA; + let swgB; + let swgC; + + test.beforeEach(async ({ page }) => { + //Open a browser, navigate to the main page, and wait until all networkevents to resolve + await page.goto('/', { waitUntil: 'networkidle' }); + stackedPlot = await createDomainObjectWithDefaults(page, { + type: "Stacked Plot" + }); + + swgA = await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + parent: stackedPlot.uuid + }); + swgB = await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + parent: stackedPlot.uuid + }); + swgC = await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + parent: stackedPlot.uuid + }); + }); test('Using the remove action removes the correct plot', async ({ page }) => { - await page.goto('/', { waitUntil: 'networkidle' }); - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: "Overlay Plot" - }); + const swgAElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgA.name }); + const swgBElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgB.name }); + const swgCElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgC.name }); + + await page.goto(stackedPlot.url); - await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - name: 'swg a', - parent: overlayPlot.uuid - }); - await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - name: 'swg b', - parent: overlayPlot.uuid - }); - await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - name: 'swg c', - parent: overlayPlot.uuid - }); - await page.goto(overlayPlot.url); await page.click('button[title="Edit"]'); // Expand the elements pool vertically @@ -60,20 +70,70 @@ test.describe('Stacked Plot', () => { await page.mouse.move(0, 100); await page.mouse.up(); - await page.locator('.js-elements-pool__tree >> text=swg b').click({ button: 'right' }); - await page.locator('li[role="menuitem"]:has-text("Remove")').click(); - await page.locator('.js-overlay .js-overlay__button >> text=OK').click(); + await swgBElementsPoolItem.click({ button: 'right' }); + await page.getByRole('menuitem').filter({ hasText: /Remove/ }).click(); + await page.getByRole('button').filter({ hasText: "OK" }).click(); - // Wait until the number of elements in the elements pool has changed, and then confirm that the correct children were retained - // await page.waitForFunction(() => { - // return Array.from(document.querySelectorAll('.js-elements-pool__tree .js-elements-pool__item')).length === 2; - // }); - // Wait until there are only two items in the elements pool (ie the remove action has completed) - await expect(page.locator('.js-elements-pool__tree .js-elements-pool__item')).toHaveCount(2); + await expect(page.locator('#inspector-elements-tree .js-elements-pool__item')).toHaveCount(2); // Confirm that the elements pool contains the items we expect - await expect(page.locator('.js-elements-pool__tree >> text=swg a')).toHaveCount(1); - await expect(page.locator('.js-elements-pool__tree >> text=swg b')).toHaveCount(0); - await expect(page.locator('.js-elements-pool__tree >> text=swg c')).toHaveCount(1); + await expect(swgAElementsPoolItem).toHaveCount(1); + await expect(swgBElementsPoolItem).toHaveCount(0); + await expect(swgCElementsPoolItem).toHaveCount(1); + }); + + test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({ page }) => { + await page.goto(stackedPlot.url); + + // Click on the 1st plot + await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click(); + + // Assert that the inspector shows the Y Axis properties for swgA + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); + await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name); + + // Click on the 2nd plot + await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"] canvas`).nth(1).click(); + + // Assert that the inspector shows the Y Axis properties for swgB + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); + await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name); + + // Click on the 3rd plot + await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"] canvas`).nth(1).click(); + + // Assert that the inspector shows the Y Axis properties for swgC + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); + await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name); + + // Go into edit mode + await page.click('button[title="Edit"]'); + + // Click on canvas for the 1st plot + await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click(); + + // Assert that the inspector shows the Y Axis properties for swgA + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); + await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name); + + //Click on canvas for the 2nd plot + await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"]`).click(); + + // Assert that the inspector shows the Y Axis properties for swgB + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); + await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name); + + //Click on canvas for the 3rd plot + await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"]`).click(); + + // Assert that the inspector shows the Y Axis properties for swgC + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); + await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name); }); }); diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 49fc537c88..ecdee217ff 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -1190,28 +1190,42 @@ export default { selectNearbyAnnotations(event) { // need to stop propagation right away to prevent selecting the plot itself event.stopPropagation(); - if (!this.annotationViewingAndEditingAllowed || this.annotationSelections.length) { - return; - } const nearbyAnnotations = this.gatherNearbyAnnotations(); - if (!nearbyAnnotations.length) { - const emptySelection = this.createPathSelection(); - this.openmct.selection.select(emptySelection, true); - // should show plot itself if we didn't find any annotations + + if (this.annotationViewingAndEditingAllowed && this.annotationSelections.length) { + //no annotations were found, but we are adding some now + return; + } + + if (this.annotationViewingAndEditingAllowed && nearbyAnnotations.length) { + //show annotations if some were found + const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations); + this.selectPlotAnnotations({ + targetDetails, + targetDomainObjects, + annotations: nearbyAnnotations + }); return; } - const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations); - this.selectPlotAnnotations({ - targetDetails, - targetDomainObjects, - annotations: nearbyAnnotations - }); + //Fall through to here if either there is no new selection add tags or no existing annotations were retrieved + this.selectPlot(); + }, + selectPlot() { + // should show plot itself if we didn't find any annotations + const selection = this.createPathSelection(); + this.openmct.selection.select(selection, true); }, createPathSelection() { let selection = []; + selection.unshift({ + element: this.$el, + context: { + item: this.domainObject + } + }); this.path.forEach((pathObject, index) => { selection.push({ element: this.openmct.layout.$refs.browseObject.$el, diff --git a/src/plugins/plot/inspector/PlotOptionsBrowse.vue b/src/plugins/plot/inspector/PlotOptionsBrowse.vue index 9707f8a001..c99004d74a 100644 --- a/src/plugins/plot/inspector/PlotOptionsBrowse.vue +++ b/src/plugins/plot/inspector/PlotOptionsBrowse.vue @@ -27,6 +27,7 @@

    Plot Series

    Y Axis {{ yAxesWithSeries.length > 1 ? yAxis.id : '' }}

  • diff --git a/src/plugins/plot/inspector/PlotOptionsEdit.vue b/src/plugins/plot/inspector/PlotOptionsEdit.vue index ee83dd3081..a67baeec22 100644 --- a/src/plugins/plot/inspector/PlotOptionsEdit.vue +++ b/src/plugins/plot/inspector/PlotOptionsEdit.vue @@ -27,6 +27,7 @@

      Plot Series

    • -
        +

          Y Axis {{ id > 1 ? id : '' }}

        • diff --git a/src/plugins/plan/PlanViewConfiguration.js b/src/plugins/plan/PlanViewConfiguration.js new file mode 100644 index 0000000000..249d97be27 --- /dev/null +++ b/src/plugins/plan/PlanViewConfiguration.js @@ -0,0 +1,110 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, 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 const DEFAULT_CONFIGURATION = { + clipActivityNames: false, + swimlaneVisibility: {} +}; + +export default class PlanViewConfiguration extends EventEmitter { + constructor(domainObject, openmct) { + super(); + + this.domainObject = domainObject; + this.openmct = openmct; + + this.configurationChanged = this.configurationChanged.bind(this); + this.unlistenFromMutation = openmct.objects.observe(domainObject, 'configuration', this.configurationChanged); + } + + /** + * @returns {Object.} + */ + getConfiguration() { + const configuration = this.domainObject.configuration ?? {}; + for (const configKey of Object.keys(DEFAULT_CONFIGURATION)) { + configuration[configKey] = configuration[configKey] ?? DEFAULT_CONFIGURATION[configKey]; + } + + return configuration; + } + + #updateConfiguration(configuration) { + this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); + } + + /** + * @param {string} swimlaneName + * @param {boolean} isVisible + */ + setSwimlaneVisibility(swimlaneName, isVisible) { + const configuration = this.getConfiguration(); + const { swimlaneVisibility } = configuration; + swimlaneVisibility[swimlaneName] = isVisible; + this.#updateConfiguration(configuration); + } + + resetSwimlaneVisibility() { + const configuration = this.getConfiguration(); + const swimlaneVisibility = {}; + configuration.swimlaneVisibility = swimlaneVisibility; + this.#updateConfiguration(configuration); + } + + initializeSwimlaneVisibility(swimlaneNames) { + const configuration = this.getConfiguration(); + const { swimlaneVisibility } = configuration; + let shouldMutate = false; + for (const swimlaneName of swimlaneNames) { + if (swimlaneVisibility[swimlaneName] === undefined) { + swimlaneVisibility[swimlaneName] = true; + shouldMutate = true; + } + } + + if (shouldMutate) { + configuration.swimlaneVisibility = swimlaneVisibility; + this.#updateConfiguration(configuration); + } + } + + /** + * @param {boolean} isEnabled + */ + setClipActivityNames(isEnabled) { + const configuration = this.getConfiguration(); + configuration.clipActivityNames = isEnabled; + this.#updateConfiguration(configuration); + } + + configurationChanged(configuration) { + if (configuration !== undefined) { + this.emit('change', configuration); + } + } + + destroy() { + this.unlistenFromMutation(); + } +} diff --git a/src/plugins/plan/PlanViewProvider.js b/src/plugins/plan/PlanViewProvider.js index 7d8b237e65..dcf3ac1056 100644 --- a/src/plugins/plan/PlanViewProvider.js +++ b/src/plugins/plan/PlanViewProvider.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import Plan from './Plan.vue'; +import Plan from './components/Plan.vue'; import Vue from 'vue'; export default function PlanViewProvider(openmct) { @@ -35,11 +35,11 @@ export default function PlanViewProvider(openmct) { name: 'Plan', cssClass: 'icon-plan', canView(domainObject) { - return domainObject.type === 'plan'; + return domainObject.type === 'plan' || domainObject.type === 'gantt-chart'; }, canEdit(domainObject) { - return false; + return domainObject.type === 'gantt-chart'; }, view: function (domainObject, objectPath) { diff --git a/src/plugins/plan/components/ActivityTimeline.vue b/src/plugins/plan/components/ActivityTimeline.vue new file mode 100644 index 0000000000..ec0120d392 --- /dev/null +++ b/src/plugins/plan/components/ActivityTimeline.vue @@ -0,0 +1,187 @@ + + + + diff --git a/src/plugins/plan/components/Plan.vue b/src/plugins/plan/components/Plan.vue new file mode 100644 index 0000000000..fc7005b2e6 --- /dev/null +++ b/src/plugins/plan/components/Plan.vue @@ -0,0 +1,558 @@ +* Open MCT, Copyright (c) 2014-2023, 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. +*****************************************************************************/ + + + + diff --git a/src/plugins/plan/inspector/PlanInspectorViewProvider.js b/src/plugins/plan/inspector/ActivityInspectorViewProvider.js similarity index 90% rename from src/plugins/plan/inspector/PlanInspectorViewProvider.js rename to src/plugins/plan/inspector/ActivityInspectorViewProvider.js index 019125fb69..2dfb756911 100644 --- a/src/plugins/plan/inspector/PlanInspectorViewProvider.js +++ b/src/plugins/plan/inspector/ActivityInspectorViewProvider.js @@ -20,13 +20,13 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import PlanActivitiesView from "./PlanActivitiesView.vue"; +import PlanActivitiesView from "./components/PlanActivitiesView.vue"; import Vue from 'vue'; -export default function PlanInspectorViewProvider(openmct) { +export default function ActivityInspectorViewProvider(openmct) { return { - key: 'plan-inspector', - name: 'Plan Inspector View', + key: 'activity-inspector', + name: 'Activity', canView: function (selection) { if (selection.length === 0 || selection[0].length === 0) { return false; @@ -44,6 +44,7 @@ export default function PlanInspectorViewProvider(openmct) { show: function (element) { component = new Vue({ el: element, + name: "PlanActivitiesView", components: { PlanActivitiesView: PlanActivitiesView }, diff --git a/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js b/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js new file mode 100644 index 0000000000..56f66b4a5b --- /dev/null +++ b/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js @@ -0,0 +1,68 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, 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 PlanViewConfiguration from './components/PlanViewConfiguration.vue'; +import Vue from 'vue'; + +export default function GanttChartInspectorViewProvider(openmct) { + return { + key: 'plan-inspector', + name: 'Config', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } + + const domainObject = selection[0][0].context.item; + + return domainObject?.type === 'gantt-chart'; + }, + view: function (selection) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + PlanViewConfiguration + }, + provide: { + openmct, + selection: selection + }, + template: '' + }); + }, + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } + } + }; + } + }; +} diff --git a/src/plugins/plan/inspector/ActivityProperty.vue b/src/plugins/plan/inspector/components/ActivityProperty.vue similarity index 100% rename from src/plugins/plan/inspector/ActivityProperty.vue rename to src/plugins/plan/inspector/components/ActivityProperty.vue diff --git a/src/plugins/plan/inspector/PlanActivitiesView.vue b/src/plugins/plan/inspector/components/PlanActivitiesView.vue similarity index 96% rename from src/plugins/plan/inspector/PlanActivitiesView.vue rename to src/plugins/plan/inspector/components/PlanActivitiesView.vue index 29ae522c35..8e7b5c1641 100644 --- a/src/plugins/plan/inspector/PlanActivitiesView.vue +++ b/src/plugins/plan/inspector/components/PlanActivitiesView.vue @@ -36,16 +36,15 @@ import { getPreciseDuration } from "utils/duration"; import { v4 as uuid } from 'uuid'; const propertyLabels = { - 'start': 'Start DateTime', - 'end': 'End DateTime', - 'duration': 'Duration', - 'earliestStart': 'Earliest Start', - 'latestEnd': 'Latest End', - 'gap': 'Gap', - 'overlap': 'Overlap', - 'totalTime': 'Total Time' + start: 'Start DateTime', + end: 'End DateTime', + duration: 'Duration', + earliestStart: 'Earliest Start', + latestEnd: 'Latest End', + gap: 'Gap', + overlap: 'Overlap', + totalTime: 'Total Time' }; - export default { components: { PlanActivityView diff --git a/src/plugins/plan/inspector/PlanActivityView.vue b/src/plugins/plan/inspector/components/PlanActivityView.vue similarity index 100% rename from src/plugins/plan/inspector/PlanActivityView.vue rename to src/plugins/plan/inspector/components/PlanActivityView.vue diff --git a/src/plugins/plan/inspector/components/PlanViewConfiguration.vue b/src/plugins/plan/inspector/components/PlanViewConfiguration.vue new file mode 100644 index 0000000000..786e97ed4b --- /dev/null +++ b/src/plugins/plan/inspector/components/PlanViewConfiguration.vue @@ -0,0 +1,144 @@ + + + diff --git a/src/plugins/plan/plan.scss b/src/plugins/plan/plan.scss index 45bd9b2938..a582260fa9 100644 --- a/src/plugins/plan/plan.scss +++ b/src/plugins/plan/plan.scss @@ -21,21 +21,34 @@ *****************************************************************************/ .c-plan { - svg { - text-rendering: geometricPrecision; + svg { + text-rendering: geometricPrecision; - text { - stroke: none; + text { + stroke: none; + } } - .activity-label { - &--outside-rect { - fill: $colorBodyFg !important; - } - } - } + &__activity { + cursor: pointer; - canvas { - display: none; - } + &[s-selected] { + rect, use { + outline-style: dotted; + outline-width: 2px; + stroke: $colorGanttSelectedBorder; + stroke-width: 2px; + } + } + } + + &__activity-label { + &--outside-rect { + fill: $colorBodyFg !important; + } + } + + canvas { + display: none; + } } diff --git a/src/plugins/plan/plugin.js b/src/plugins/plan/plugin.js index d44a267da8..85022fd61b 100644 --- a/src/plugins/plan/plugin.js +++ b/src/plugins/plan/plugin.js @@ -21,15 +21,18 @@ *****************************************************************************/ import PlanViewProvider from './PlanViewProvider'; -import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider"; +import ActivityInspectorViewProvider from "./inspector/ActivityInspectorViewProvider"; +import GanttChartInspectorViewProvider from "./inspector/GanttChartInspectorViewProvider"; +import ganttChartCompositionPolicy from './GanttChartCompositionPolicy'; +import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration'; -export default function (configuration) { +export default function (options = {}) { return function install(openmct) { openmct.types.addType('plan', { name: 'Plan', key: 'plan', - description: 'A configurable timeline-like view for a compatible mission plan file.', - creatable: true, + description: 'A non-configurable timeline-like view for a compatible plan file.', + creatable: options.creatable ?? false, cssClass: 'icon-plan', form: [ { @@ -45,10 +48,30 @@ export default function (configuration) { } ], initialize: function (domainObject) { + domainObject.configuration = { + clipActivityNames: DEFAULT_CONFIGURATION.clipActivityNames + }; + } + }); + // Name TBD and subject to change + openmct.types.addType('gantt-chart', { + name: 'Gantt Chart', + key: 'gantt-chart', + description: 'A configurable timeline-like view for a compatible plan file.', + creatable: true, + cssClass: 'icon-plan', + form: [], + initialize(domainObject) { + domainObject.configuration = { + clipActivityNames: true + }; + domainObject.composition = []; } }); openmct.objectViews.addProvider(new PlanViewProvider(openmct)); - openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct)); + openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct)); + openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct)); + openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct)); }; } diff --git a/src/plugins/plan/pluginSpec.js b/src/plugins/plan/pluginSpec.js index e7603ee24a..25db86289f 100644 --- a/src/plugins/plan/pluginSpec.js +++ b/src/plugins/plan/pluginSpec.js @@ -27,6 +27,7 @@ import Properties from "../inspectorViews/properties/Properties.vue"; describe('the plugin', function () { let planDefinition; + let ganttDefinition; let element; let child; let openmct; @@ -50,6 +51,7 @@ describe('the plugin', function () { openmct.install(new PlanPlugin()); planDefinition = openmct.types.get('plan').definition; + ganttDefinition = openmct.types.get('gantt-chart').definition; element = document.createElement('div'); element.style.width = '640px'; @@ -74,15 +76,30 @@ describe('the plugin', function () { let mockPlanObject = { name: 'Plan', key: 'plan', + creatable: false + }; + + let mockGanttObject = { + name: 'Gantt', + key: 'gantt-chart', creatable: true }; - it('defines a plan object type with the correct key', () => { - expect(planDefinition.key).toEqual(mockPlanObject.key); + describe('the plan type', () => { + it('defines a plan object type with the correct key', () => { + expect(planDefinition.key).toEqual(mockPlanObject.key); + }); + it('is not creatable', () => { + expect(planDefinition.creatable).toEqual(mockPlanObject.creatable); + }); }); - - it('is creatable', () => { - expect(planDefinition.creatable).toEqual(mockPlanObject.creatable); + describe('the gantt-chart type', () => { + it('defines a gantt-chart object type with the correct key', () => { + expect(ganttDefinition.key).toEqual(mockGanttObject.key); + }); + it('is creatable', () => { + expect(ganttDefinition.creatable).toEqual(mockGanttObject.creatable); + }); }); describe('the plan view', () => { @@ -107,7 +124,7 @@ describe('the plugin', function () { const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); - expect(planView.canEdit()).toBeFalse(); + expect(planView.canEdit(testViewObject)).toBeFalse(); }); }); @@ -179,10 +196,10 @@ describe('the plugin', function () { it('displays the group label', () => { const labelEl = element.querySelector('.c-plan__contents .c-object-label .c-object-label__name'); - expect(labelEl.innerHTML).toEqual('TEST-GROUP'); + expect(labelEl.innerHTML).toMatch(/TEST-GROUP/); }); - it('displays the activities and their labels', (done) => { + it('displays the activities and their labels', async () => { const bounds = { start: 1597160002854, end: 1597181232854 @@ -190,27 +207,22 @@ describe('the plugin', function () { openmct.time.bounds(bounds); - Vue.nextTick(() => { - const rectEls = element.querySelectorAll('.c-plan__contents rect'); - expect(rectEls.length).toEqual(2); - const textEls = element.querySelectorAll('.c-plan__contents text'); - expect(textEls.length).toEqual(3); - - done(); - }); + await Vue.nextTick(); + const rectEls = element.querySelectorAll('.c-plan__contents use'); + expect(rectEls.length).toEqual(2); + const textEls = element.querySelectorAll('.c-plan__contents text'); + expect(textEls.length).toEqual(3); }); - it ('shows the status indicator when available', (done) => { + it ('shows the status indicator when available', async () => { openmct.status.set({ key: "test-object", namespace: '' }, 'draft'); - Vue.nextTick(() => { - const statusEl = element.querySelector('.c-plan__contents .is-status--draft'); - expect(statusEl).toBeDefined(); - done(); - }); + await Vue.nextTick(); + const statusEl = element.querySelector('.c-plan__contents .is-status--draft'); + expect(statusEl).toBeDefined(); }); }); @@ -224,10 +236,12 @@ describe('the plugin', function () { key: 'test-plan', namespace: '' }, + created: 123456789, + modified: 123456790, version: 'v1' }; - beforeEach(() => { + beforeEach(async () => { openmct.selection.select([{ element: element, context: { @@ -241,19 +255,18 @@ describe('the plugin', function () { } }], false); - return Vue.nextTick().then(() => { - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - Properties - }, - provide: { - openmct: openmct - }, - template: '' - }); + await Vue.nextTick(); + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + Properties + }, + provide: { + openmct: openmct + }, + template: '' }); }); @@ -264,7 +277,6 @@ describe('the plugin', function () { it('provides an inspector view with the version information if available', () => { componentObject = component.$root.$children[0]; const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row'); - expect(propertiesEls.length).toEqual(7); const found = Array.from(propertiesEls).some((propertyEl) => { return (propertyEl.children[0].innerHTML.trim() === 'Version' && propertyEl.children[1].innerHTML.trim() === 'v1'); diff --git a/src/plugins/plan/util.js b/src/plugins/plan/util.js index 5b3934bd79..27cbcb5491 100644 --- a/src/plugins/plan/util.js +++ b/src/plugins/plan/util.js @@ -21,8 +21,8 @@ *****************************************************************************/ export function getValidatedData(domainObject) { - let sourceMap = domainObject.sourceMap; - let body = domainObject.selectFile?.body; + const sourceMap = domainObject.sourceMap; + const body = domainObject.selectFile?.body; let json = {}; if (typeof body === 'string') { try { @@ -64,3 +64,27 @@ export function getValidatedData(domainObject) { return json; } } + +export function getContrastingColor(hexColor) { + function cutHex(h, start, end) { + const hStr = (h.charAt(0) === '#') ? h.substring(1, 7) : h; + + return parseInt(hStr.substring(start, end), 16); + } + + // https://codepen.io/davidhalford/pen/ywEva/ + const cThreshold = 130; + + if (hexColor.indexOf('#') === -1) { + // We weren't given a hex color + return "#ff0000"; + } + + const hR = cutHex(hexColor, 0, 2); + const hG = cutHex(hexColor, 2, 4); + const hB = cutHex(hexColor, 4, 6); + + const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000; + + return cBrightness > cThreshold ? "#000000" : "#ffffff"; +} diff --git a/src/plugins/timeline/TimelineCompositionPolicy.js b/src/plugins/timeline/TimelineCompositionPolicy.js index 49f7d5ed8a..4d2dc675e9 100644 --- a/src/plugins/timeline/TimelineCompositionPolicy.js +++ b/src/plugins/timeline/TimelineCompositionPolicy.js @@ -19,10 +19,12 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ + const ALLOWED_TYPES = [ 'telemetry.plot.overlay', 'telemetry.plot.stacked', - 'plan' + 'plan', + 'gantt-chart' ]; const DISALLOWED_TYPES = [ 'telemetry.plot.bar-graph', diff --git a/src/plugins/timeline/TimelineObjectView.vue b/src/plugins/timeline/TimelineObjectView.vue index 039ac3e4eb..6855ac22c1 100644 --- a/src/plugins/timeline/TimelineObjectView.vue +++ b/src/plugins/timeline/TimelineObjectView.vue @@ -19,12 +19,13 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ + - + diff --git a/src/plugins/plan/components/Plan.vue b/src/plugins/plan/components/Plan.vue index 9890d6040b..040211f053 100644 --- a/src/plugins/plan/components/Plan.vue +++ b/src/plugins/plan/components/Plan.vue @@ -21,48 +21,42 @@ --> +
          +
          -
          +
      diff --git a/src/plugins/plan/inspector/ActivityInspectorViewProvider.js b/src/plugins/plan/inspector/ActivityInspectorViewProvider.js index 2dfb756911..911fdfbaef 100644 --- a/src/plugins/plan/inspector/ActivityInspectorViewProvider.js +++ b/src/plugins/plan/inspector/ActivityInspectorViewProvider.js @@ -20,51 +20,50 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import PlanActivitiesView from "./components/PlanActivitiesView.vue"; +import PlanActivitiesView from './components/PlanActivitiesView.vue'; import Vue from 'vue'; export default function ActivityInspectorViewProvider(openmct) { - return { - key: 'activity-inspector', - name: 'Activity', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: 'activity-inspector', + name: 'Activity', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - let context = selection[0][0].context; + let context = selection[0][0].context; - return context - && context.type === 'activity'; + return context && context.type === 'activity'; + }, + view: function (selection) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + name: 'PlanActivitiesView', + components: { + PlanActivitiesView: PlanActivitiesView + }, + provide: { + openmct, + selection: selection + }, + template: '' + }); }, - view: function (selection) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - name: "PlanActivitiesView", - components: { - PlanActivitiesView: PlanActivitiesView - }, - provide: { - openmct, - selection: selection - }, - template: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js b/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js index 56f66b4a5b..dc19cc813c 100644 --- a/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js +++ b/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js @@ -24,45 +24,45 @@ import PlanViewConfiguration from './components/PlanViewConfiguration.vue'; import Vue from 'vue'; export default function GanttChartInspectorViewProvider(openmct) { - return { - key: 'plan-inspector', - name: 'Config', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: 'plan-inspector', + name: 'Config', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - const domainObject = selection[0][0].context.item; + const domainObject = selection[0][0].context.item; - return domainObject?.type === 'gantt-chart'; + return domainObject?.type === 'gantt-chart'; + }, + view: function (selection) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + PlanViewConfiguration + }, + provide: { + openmct, + selection: selection + }, + template: '' + }); }, - view: function (selection) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - PlanViewConfiguration - }, - provide: { - openmct, - selection: selection - }, - template: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/plan/inspector/components/ActivityProperty.vue b/src/plugins/plan/inspector/components/ActivityProperty.vue index 88c1f4d424..1b6560dfb5 100644 --- a/src/plugins/plan/inspector/components/ActivityProperty.vue +++ b/src/plugins/plan/inspector/components/ActivityProperty.vue @@ -21,32 +21,31 @@ --> diff --git a/src/plugins/plan/inspector/components/PlanActivitiesView.vue b/src/plugins/plan/inspector/components/PlanActivitiesView.vue index 8e7b5c1641..6e31acaa62 100644 --- a/src/plugins/plan/inspector/components/PlanActivitiesView.vue +++ b/src/plugins/plan/inspector/components/PlanActivitiesView.vue @@ -20,187 +20,187 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plan/inspector/components/PlanActivityView.vue b/src/plugins/plan/inspector/components/PlanActivityView.vue index 605e09cc22..d514eb472a 100644 --- a/src/plugins/plan/inspector/components/PlanActivityView.vue +++ b/src/plugins/plan/inspector/components/PlanActivityView.vue @@ -21,24 +21,18 @@ --> diff --git a/src/plugins/plan/inspector/components/PlanViewConfiguration.vue b/src/plugins/plan/inspector/components/PlanViewConfiguration.vue index 786e97ed4b..c8747ffad3 100644 --- a/src/plugins/plan/inspector/components/PlanViewConfiguration.vue +++ b/src/plugins/plan/inspector/components/PlanViewConfiguration.vue @@ -20,125 +20,109 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plan/plan.scss b/src/plugins/plan/plan.scss index a582260fa9..4353079658 100644 --- a/src/plugins/plan/plan.scss +++ b/src/plugins/plan/plan.scss @@ -21,34 +21,35 @@ *****************************************************************************/ .c-plan { - svg { - text-rendering: geometricPrecision; + svg { + text-rendering: geometricPrecision; - text { - stroke: none; - } + text { + stroke: none; } + } - &__activity { - cursor: pointer; + &__activity { + cursor: pointer; - &[s-selected] { - rect, use { - outline-style: dotted; - outline-width: 2px; - stroke: $colorGanttSelectedBorder; - stroke-width: 2px; - } - } + &[s-selected] { + rect, + use { + outline-style: dotted; + outline-width: 2px; + stroke: $colorGanttSelectedBorder; + stroke-width: 2px; + } } + } - &__activity-label { - &--outside-rect { - fill: $colorBodyFg !important; - } + &__activity-label { + &--outside-rect { + fill: $colorBodyFg !important; } + } - canvas { - display: none; - } + canvas { + display: none; + } } diff --git a/src/plugins/plan/plugin.js b/src/plugins/plan/plugin.js index 85022fd61b..ae999d3c3f 100644 --- a/src/plugins/plan/plugin.js +++ b/src/plugins/plan/plugin.js @@ -21,57 +21,54 @@ *****************************************************************************/ import PlanViewProvider from './PlanViewProvider'; -import ActivityInspectorViewProvider from "./inspector/ActivityInspectorViewProvider"; -import GanttChartInspectorViewProvider from "./inspector/GanttChartInspectorViewProvider"; +import ActivityInspectorViewProvider from './inspector/ActivityInspectorViewProvider'; +import GanttChartInspectorViewProvider from './inspector/GanttChartInspectorViewProvider'; import ganttChartCompositionPolicy from './GanttChartCompositionPolicy'; import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration'; export default function (options = {}) { - return function install(openmct) { - openmct.types.addType('plan', { - name: 'Plan', - key: 'plan', - description: 'A non-configurable timeline-like view for a compatible plan file.', - creatable: options.creatable ?? false, - cssClass: 'icon-plan', - form: [ - { - name: 'Upload Plan (JSON File)', - key: 'selectFile', - control: 'file-input', - required: true, - text: 'Select File...', - type: 'application/json', - property: [ - "selectFile" - ] - } - ], - initialize: function (domainObject) { - domainObject.configuration = { - clipActivityNames: DEFAULT_CONFIGURATION.clipActivityNames - }; - } - }); - // Name TBD and subject to change - openmct.types.addType('gantt-chart', { - name: 'Gantt Chart', - key: 'gantt-chart', - description: 'A configurable timeline-like view for a compatible plan file.', - creatable: true, - cssClass: 'icon-plan', - form: [], - initialize(domainObject) { - domainObject.configuration = { - clipActivityNames: true - }; - domainObject.composition = []; - } - }); - openmct.objectViews.addProvider(new PlanViewProvider(openmct)); - openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct)); - openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct)); - openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct)); - }; + return function install(openmct) { + openmct.types.addType('plan', { + name: 'Plan', + key: 'plan', + description: 'A non-configurable timeline-like view for a compatible plan file.', + creatable: options.creatable ?? false, + cssClass: 'icon-plan', + form: [ + { + name: 'Upload Plan (JSON File)', + key: 'selectFile', + control: 'file-input', + required: true, + text: 'Select File...', + type: 'application/json', + property: ['selectFile'] + } + ], + initialize: function (domainObject) { + domainObject.configuration = { + clipActivityNames: DEFAULT_CONFIGURATION.clipActivityNames + }; + } + }); + // Name TBD and subject to change + openmct.types.addType('gantt-chart', { + name: 'Gantt Chart', + key: 'gantt-chart', + description: 'A configurable timeline-like view for a compatible plan file.', + creatable: true, + cssClass: 'icon-plan', + form: [], + initialize(domainObject) { + domainObject.configuration = { + clipActivityNames: true + }; + domainObject.composition = []; + } + }); + openmct.objectViews.addProvider(new PlanViewProvider(openmct)); + openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct)); + openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct)); + openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct)); + }; } - diff --git a/src/plugins/plan/pluginSpec.js b/src/plugins/plan/pluginSpec.js index 25db86289f..ca4d35651b 100644 --- a/src/plugins/plan/pluginSpec.js +++ b/src/plugins/plan/pluginSpec.js @@ -20,268 +20,281 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createOpenMct, resetApplicationState} from "utils/testing"; -import PlanPlugin from "../plan/plugin"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import PlanPlugin from '../plan/plugin'; import Vue from 'vue'; -import Properties from "../inspectorViews/properties/Properties.vue"; +import Properties from '../inspectorViews/properties/Properties.vue'; describe('the plugin', function () { - let planDefinition; - let ganttDefinition; - let element; - let child; - let openmct; - let appHolder; - let originalRouterPath; + let planDefinition; + let ganttDefinition; + let element; + let child; + let openmct; + let appHolder; + let originalRouterPath; - beforeEach((done) => { - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; + beforeEach((done) => { + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; - const timeSystemOptions = { - timeSystemKey: 'utc', - bounds: { - start: 1597160002854, - end: 1597181232854 + const timeSystemOptions = { + timeSystemKey: 'utc', + bounds: { + start: 1597160002854, + end: 1597181232854 + } + }; + + openmct = createOpenMct(timeSystemOptions); + openmct.install(new PlanPlugin()); + + planDefinition = openmct.types.get('plan').definition; + ganttDefinition = openmct.types.get('gantt-chart').definition; + + element = document.createElement('div'); + element.style.width = '640px'; + element.style.height = '480px'; + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + element.appendChild(child); + + originalRouterPath = openmct.router.path; + + openmct.on('start', done); + openmct.start(appHolder); + }); + + afterEach(() => { + openmct.router.path = originalRouterPath; + + return resetApplicationState(openmct); + }); + + let mockPlanObject = { + name: 'Plan', + key: 'plan', + creatable: false + }; + + let mockGanttObject = { + name: 'Gantt', + key: 'gantt-chart', + creatable: true + }; + + describe('the plan type', () => { + it('defines a plan object type with the correct key', () => { + expect(planDefinition.key).toEqual(mockPlanObject.key); + }); + it('is not creatable', () => { + expect(planDefinition.creatable).toEqual(mockPlanObject.creatable); + }); + }); + describe('the gantt-chart type', () => { + it('defines a gantt-chart object type with the correct key', () => { + expect(ganttDefinition.key).toEqual(mockGanttObject.key); + }); + it('is creatable', () => { + expect(ganttDefinition.creatable).toEqual(mockGanttObject.creatable); + }); + }); + + describe('the plan view', () => { + it('provides a plan view', () => { + const testViewObject = { + id: 'test-object', + type: 'plan' + }; + openmct.router.path = [testViewObject]; + + const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); + let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); + expect(planView).toBeDefined(); + }); + + it('is not an editable view', () => { + const testViewObject = { + id: 'test-object', + type: 'plan' + }; + openmct.router.path = [testViewObject]; + + const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); + let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); + expect(planView.canEdit(testViewObject)).toBeFalse(); + }); + }); + + describe('the plan view displays activities', () => { + let planDomainObject; + let mockObjectPath = [ + { + identifier: { + key: 'test', + namespace: '' + }, + type: 'time-strip', + name: 'Test Parent Object' + } + ]; + let planView; + + beforeEach(() => { + openmct.time.timeSystem('utc', { + start: 1597160002854, + end: 1597181232854 + }); + + planDomainObject = { + identifier: { + key: 'test-object', + namespace: '' + }, + type: 'plan', + id: 'test-object', + selectFile: { + body: JSON.stringify({ + 'TEST-GROUP': [ + { + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', + start: 1597170002854, + end: 1597171032854, + type: 'TEST-GROUP', + color: 'fuchsia', + textColor: 'black' + }, + { + name: 'Sed ut perspiciatis', + start: 1597171132854, + end: 1597171232854, + type: 'TEST-GROUP', + color: 'fuchsia', + textColor: 'black' + } + ] + }) + } + }; + + openmct.router.path = [planDomainObject]; + + const applicableViews = openmct.objectViews.get(planDomainObject, [planDomainObject]); + planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); + let view = planView.view(planDomainObject, mockObjectPath); + view.show(child, true); + + return Vue.nextTick(); + }); + + it('loads activities into the view', () => { + const svgEls = element.querySelectorAll('.c-plan__contents svg'); + expect(svgEls.length).toEqual(1); + }); + + it('displays the group label', () => { + const labelEl = element.querySelector( + '.c-plan__contents .c-object-label .c-object-label__name' + ); + expect(labelEl.innerHTML).toMatch(/TEST-GROUP/); + }); + + it('displays the activities and their labels', async () => { + const bounds = { + start: 1597160002854, + end: 1597181232854 + }; + + openmct.time.bounds(bounds); + + await Vue.nextTick(); + const rectEls = element.querySelectorAll('.c-plan__contents use'); + expect(rectEls.length).toEqual(2); + const textEls = element.querySelectorAll('.c-plan__contents text'); + expect(textEls.length).toEqual(3); + }); + + it('shows the status indicator when available', async () => { + openmct.status.set( + { + key: 'test-object', + namespace: '' + }, + 'draft' + ); + + await Vue.nextTick(); + const statusEl = element.querySelector('.c-plan__contents .is-status--draft'); + expect(statusEl).toBeDefined(); + }); + }); + + describe('the plan version', () => { + let component; + let componentObject; + let testPlanObject = { + name: 'Plan', + type: 'plan', + identifier: { + key: 'test-plan', + namespace: '' + }, + created: 123456789, + modified: 123456790, + version: 'v1' + }; + + beforeEach(async () => { + openmct.selection.select( + [ + { + element: element, + context: { + item: testPlanObject } - }; + }, + { + element: openmct.layout.$refs.browseObject.$el, + context: { + item: testPlanObject, + supportsMultiSelect: false + } + } + ], + false + ); - openmct = createOpenMct(timeSystemOptions); - openmct.install(new PlanPlugin()); - - planDefinition = openmct.types.get('plan').definition; - ganttDefinition = openmct.types.get('gantt-chart').definition; - - element = document.createElement('div'); - element.style.width = '640px'; - element.style.height = '480px'; - child = document.createElement('div'); - child.style.width = '640px'; - child.style.height = '480px'; - element.appendChild(child); - - originalRouterPath = openmct.router.path; - - openmct.on('start', done); - openmct.start(appHolder); + await Vue.nextTick(); + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + Properties + }, + provide: { + openmct: openmct + }, + template: '' + }); }); afterEach(() => { - openmct.router.path = originalRouterPath; - - return resetApplicationState(openmct); + component.$destroy(); }); - let mockPlanObject = { - name: 'Plan', - key: 'plan', - creatable: false - }; - - let mockGanttObject = { - name: 'Gantt', - key: 'gantt-chart', - creatable: true - }; - - describe('the plan type', () => { - it('defines a plan object type with the correct key', () => { - expect(planDefinition.key).toEqual(mockPlanObject.key); - }); - it('is not creatable', () => { - expect(planDefinition.creatable).toEqual(mockPlanObject.creatable); - }); - }); - describe('the gantt-chart type', () => { - it('defines a gantt-chart object type with the correct key', () => { - expect(ganttDefinition.key).toEqual(mockGanttObject.key); - }); - it('is creatable', () => { - expect(ganttDefinition.creatable).toEqual(mockGanttObject.creatable); - }); - }); - - describe('the plan view', () => { - it('provides a plan view', () => { - const testViewObject = { - id: "test-object", - type: "plan" - }; - openmct.router.path = [testViewObject]; - - const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); - let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); - expect(planView).toBeDefined(); - }); - - it('is not an editable view', () => { - const testViewObject = { - id: "test-object", - type: "plan" - }; - openmct.router.path = [testViewObject]; - - const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); - let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); - expect(planView.canEdit(testViewObject)).toBeFalse(); - }); - }); - - describe('the plan view displays activities', () => { - let planDomainObject; - let mockObjectPath = [ - { - identifier: { - key: 'test', - namespace: '' - }, - type: 'time-strip', - name: 'Test Parent Object' - } - ]; - let planView; - - beforeEach(() => { - openmct.time.timeSystem('utc', { - start: 1597160002854, - end: 1597181232854 - }); - - planDomainObject = { - identifier: { - key: 'test-object', - namespace: '' - }, - type: 'plan', - id: "test-object", - selectFile: { - body: JSON.stringify({ - "TEST-GROUP": [ - { - "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", - "start": 1597170002854, - "end": 1597171032854, - "type": "TEST-GROUP", - "color": "fuchsia", - "textColor": "black" - }, - { - "name": "Sed ut perspiciatis", - "start": 1597171132854, - "end": 1597171232854, - "type": "TEST-GROUP", - "color": "fuchsia", - "textColor": "black" - } - ] - }) - } - }; - - openmct.router.path = [planDomainObject]; - - const applicableViews = openmct.objectViews.get(planDomainObject, [planDomainObject]); - planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); - let view = planView.view(planDomainObject, mockObjectPath); - view.show(child, true); - - return Vue.nextTick(); - }); - - it('loads activities into the view', () => { - const svgEls = element.querySelectorAll('.c-plan__contents svg'); - expect(svgEls.length).toEqual(1); - }); - - it('displays the group label', () => { - const labelEl = element.querySelector('.c-plan__contents .c-object-label .c-object-label__name'); - expect(labelEl.innerHTML).toMatch(/TEST-GROUP/); - }); - - it('displays the activities and their labels', async () => { - const bounds = { - start: 1597160002854, - end: 1597181232854 - }; - - openmct.time.bounds(bounds); - - await Vue.nextTick(); - const rectEls = element.querySelectorAll('.c-plan__contents use'); - expect(rectEls.length).toEqual(2); - const textEls = element.querySelectorAll('.c-plan__contents text'); - expect(textEls.length).toEqual(3); - }); - - it ('shows the status indicator when available', async () => { - openmct.status.set({ - key: "test-object", - namespace: '' - }, 'draft'); - - await Vue.nextTick(); - const statusEl = element.querySelector('.c-plan__contents .is-status--draft'); - expect(statusEl).toBeDefined(); - }); - }); - - describe('the plan version', () => { - let component; - let componentObject; - let testPlanObject = { - name: 'Plan', - type: 'plan', - identifier: { - key: 'test-plan', - namespace: '' - }, - created: 123456789, - modified: 123456790, - version: 'v1' - }; - - beforeEach(async () => { - openmct.selection.select([{ - element: element, - context: { - item: testPlanObject - } - }, { - element: openmct.layout.$refs.browseObject.$el, - context: { - item: testPlanObject, - supportsMultiSelect: false - } - }], false); - - await Vue.nextTick(); - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - Properties - }, - provide: { - openmct: openmct - }, - template: '' - }); - }); - - afterEach(() => { - component.$destroy(); - }); - - it('provides an inspector view with the version information if available', () => { - componentObject = component.$root.$children[0]; - const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row'); - const found = Array.from(propertiesEls).some((propertyEl) => { - return (propertyEl.children[0].innerHTML.trim() === 'Version' - && propertyEl.children[1].innerHTML.trim() === 'v1'); - }); - expect(found).toBeTrue(); - }); + it('provides an inspector view with the version information if available', () => { + componentObject = component.$root.$children[0]; + const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row'); + const found = Array.from(propertiesEls).some((propertyEl) => { + return ( + propertyEl.children[0].innerHTML.trim() === 'Version' && + propertyEl.children[1].innerHTML.trim() === 'v1' + ); + }); + expect(found).toBeTrue(); }); + }); }); diff --git a/src/plugins/plan/util.js b/src/plugins/plan/util.js index 27cbcb5491..3914f40b8b 100644 --- a/src/plugins/plan/util.js +++ b/src/plugins/plan/util.js @@ -21,70 +21,74 @@ *****************************************************************************/ export function getValidatedData(domainObject) { - const sourceMap = domainObject.sourceMap; - const body = domainObject.selectFile?.body; - let json = {}; - if (typeof body === 'string') { - try { - json = JSON.parse(body); - } catch (e) { - return json; + const sourceMap = domainObject.sourceMap; + const body = domainObject.selectFile?.body; + let json = {}; + if (typeof body === 'string') { + try { + json = JSON.parse(body); + } catch (e) { + return json; + } + } else if (body !== undefined) { + json = body; + } + + if ( + sourceMap !== undefined && + sourceMap.activities !== undefined && + sourceMap.groupId !== undefined + ) { + let mappedJson = {}; + json[sourceMap.activities].forEach((activity) => { + if (activity[sourceMap.groupId]) { + const groupIdKey = activity[sourceMap.groupId]; + let groupActivity = { + ...activity + }; + + if (sourceMap.start) { + groupActivity.start = activity[sourceMap.start]; } - } else if (body !== undefined) { - json = body; - } - if (sourceMap !== undefined && sourceMap.activities !== undefined && sourceMap.groupId !== undefined) { - let mappedJson = {}; - json[sourceMap.activities].forEach((activity) => { - if (activity[sourceMap.groupId]) { - const groupIdKey = activity[sourceMap.groupId]; - let groupActivity = { - ...activity - }; + if (sourceMap.end) { + groupActivity.end = activity[sourceMap.end]; + } - if (sourceMap.start) { - groupActivity.start = activity[sourceMap.start]; - } + if (!mappedJson[groupIdKey]) { + mappedJson[groupIdKey] = []; + } - if (sourceMap.end) { - groupActivity.end = activity[sourceMap.end]; - } + mappedJson[groupIdKey].push(groupActivity); + } + }); - if (!mappedJson[groupIdKey]) { - mappedJson[groupIdKey] = []; - } - - mappedJson[groupIdKey].push(groupActivity); - } - }); - - return mappedJson; - } else { - return json; - } + return mappedJson; + } else { + return json; + } } export function getContrastingColor(hexColor) { - function cutHex(h, start, end) { - const hStr = (h.charAt(0) === '#') ? h.substring(1, 7) : h; + function cutHex(h, start, end) { + const hStr = h.charAt(0) === '#' ? h.substring(1, 7) : h; - return parseInt(hStr.substring(start, end), 16); - } + return parseInt(hStr.substring(start, end), 16); + } - // https://codepen.io/davidhalford/pen/ywEva/ - const cThreshold = 130; + // https://codepen.io/davidhalford/pen/ywEva/ + const cThreshold = 130; - if (hexColor.indexOf('#') === -1) { - // We weren't given a hex color - return "#ff0000"; - } + if (hexColor.indexOf('#') === -1) { + // We weren't given a hex color + return '#ff0000'; + } - const hR = cutHex(hexColor, 0, 2); - const hG = cutHex(hexColor, 2, 4); - const hB = cutHex(hexColor, 4, 6); + const hR = cutHex(hexColor, 0, 2); + const hG = cutHex(hexColor, 2, 4); + const hB = cutHex(hexColor, 4, 6); - const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000; + const cBrightness = (hR * 299 + hG * 587 + hB * 114) / 1000; - return cBrightness > cThreshold ? "#000000" : "#ffffff"; + return cBrightness > cThreshold ? '#000000' : '#ffffff'; } diff --git a/src/plugins/plot/LinearScale.js b/src/plugins/plot/LinearScale.js index 6d24f3db83..90c1b90722 100644 --- a/src/plugins/plot/LinearScale.js +++ b/src/plugins/plot/LinearScale.js @@ -27,53 +27,53 @@ */ class LinearScale { - constructor(domain) { - this.domain(domain); + constructor(domain) { + this.domain(domain); + } + + domain(newDomain) { + if (newDomain) { + this._domain = newDomain; + this._domainDenominator = newDomain.max - newDomain.min; } - domain(newDomain) { - if (newDomain) { - this._domain = newDomain; - this._domainDenominator = newDomain.max - newDomain.min; - } + return this._domain; + } - return this._domain; + range(newRange) { + if (newRange) { + this._range = newRange; + this._rangeDenominator = newRange.max - newRange.min; } - range(newRange) { - if (newRange) { - this._range = newRange; - this._rangeDenominator = newRange.max - newRange.min; - } + return this._range; + } - return this._range; + scale(domainValue) { + if (!this._domain || !this._range) { + return; } - scale(domainValue) { - if (!this._domain || !this._range) { - return; - } + const domainOffset = domainValue - this._domain.min; + const rangeFraction = domainOffset - this._domainDenominator; + const rangeOffset = rangeFraction * this._rangeDenominator; + const rangeValue = rangeOffset + this._range.min; - const domainOffset = domainValue - this._domain.min; - const rangeFraction = domainOffset - this._domainDenominator; - const rangeOffset = rangeFraction * this._rangeDenominator; - const rangeValue = rangeOffset + this._range.min; + return rangeValue; + } - return rangeValue; + invert(rangeValue) { + if (!this._domain || !this._range) { + return; } - invert(rangeValue) { - if (!this._domain || !this._range) { - return; - } + const rangeOffset = rangeValue - this._range.min; + const domainFraction = rangeOffset / this._rangeDenominator; + const domainOffset = domainFraction * this._domainDenominator; + const domainValue = domainOffset + this._domain.min; - const rangeOffset = rangeValue - this._range.min; - const domainFraction = rangeOffset / this._rangeDenominator; - const domainOffset = domainFraction * this._domainDenominator; - const domainValue = domainOffset + this._domain.min; - - return domainValue; - } + return domainValue; + } } export default LinearScale; diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 8227ba044e..c0d0e18e24 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -20,1845 +20,1900 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/MctTicks.vue b/src/plugins/plot/MctTicks.vue index 88d7185011..bfcaf866b2 100644 --- a/src/plugins/plot/MctTicks.vue +++ b/src/plugins/plot/MctTicks.vue @@ -21,270 +21,266 @@ --> diff --git a/src/plugins/plot/Plot.vue b/src/plugins/plot/Plot.vue index f31b5c5496..49605adec8 100644 --- a/src/plugins/plot/Plot.vue +++ b/src/plugins/plot/Plot.vue @@ -20,222 +20,225 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/PlotViewProvider.js b/src/plugins/plot/PlotViewProvider.js index 461696150c..78c3bea730 100644 --- a/src/plugins/plot/PlotViewProvider.js +++ b/src/plugins/plot/PlotViewProvider.js @@ -24,80 +24,82 @@ import Plot from './Plot.vue'; import Vue from 'vue'; export default function PlotViewProvider(openmct) { - function hasNumericTelemetry(domainObject) { - if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { - return false; - } - - let metadata = openmct.telemetry.getMetadata(domainObject); - - return metadata.values().length > 0 && hasDomainAndNumericRange(metadata); + function hasNumericTelemetry(domainObject) { + if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { + return false; } - function hasDomainAndNumericRange(metadata) { - const rangeValues = metadata.valuesForHints(['range']); - const domains = metadata.valuesForHints(['domain']); + let metadata = openmct.telemetry.getMetadata(domainObject); - return domains.length > 0 - && rangeValues.length > 0 - && !rangeValues.every(value => value.format === 'string'); - } + return metadata.values().length > 0 && hasDomainAndNumericRange(metadata); + } - function isCompactView(objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); + function hasDomainAndNumericRange(metadata) { + const rangeValues = metadata.valuesForHints(['range']); + const domains = metadata.valuesForHints(['domain']); - return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); - } + return ( + domains.length > 0 && + rangeValues.length > 0 && + !rangeValues.every((value) => value.format === 'string') + ); + } - return { - key: 'plot-single', - name: 'Plot', - cssClass: 'icon-telemetry', - canView(domainObject, objectPath) { - return hasNumericTelemetry(domainObject); - }, + function isCompactView(objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip'); - view: function (domainObject, objectPath) { - let component; + return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); + } - return { - show: function (element) { - let isCompact = isCompactView(objectPath); - component = new Vue({ - el: element, - components: { - Plot - }, - provide: { - openmct, - domainObject, - path: objectPath - }, - data() { - return { - options: { - compact: isCompact - } - }; - }, - template: '' - }); - }, - getViewContext() { - if (!component) { - return {}; - } + return { + key: 'plot-single', + name: 'Plot', + cssClass: 'icon-telemetry', + canView(domainObject, objectPath) { + return hasNumericTelemetry(domainObject); + }, - return component.$refs.plotComponent.getViewContext(); - }, - destroy: function () { - component.$destroy(); - component = undefined; - }, - getComponent() { - return component; + view: function (domainObject, objectPath) { + let component; + + return { + show: function (element) { + let isCompact = isCompactView(objectPath); + component = new Vue({ + el: element, + components: { + Plot + }, + provide: { + openmct, + domainObject, + path: objectPath + }, + data() { + return { + options: { + compact: isCompact } - }; + }; + }, + template: '' + }); + }, + getViewContext() { + if (!component) { + return {}; + } + + return component.$refs.plotComponent.getViewContext(); + }, + destroy: function () { + component.$destroy(); + component = undefined; + }, + getComponent() { + return component; } - }; + }; + } + }; } diff --git a/src/plugins/plot/actions/ViewActions.js b/src/plugins/plot/actions/ViewActions.js index b038041af0..c036753015 100644 --- a/src/plugins/plot/actions/ViewActions.js +++ b/src/plugins/plot/actions/ViewActions.js @@ -19,39 +19,36 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import {isPlotView} from "@/plugins/plot/actions/utils"; +import { isPlotView } from '@/plugins/plot/actions/utils'; const exportPNG = { - name: 'Export as PNG', - key: 'export-as-png', - description: 'Export This View\'s Data as PNG', - cssClass: 'icon-download', - group: 'view', - invoke(objectPath, view) { - view.getViewContext().exportPNG(); - } + name: 'Export as PNG', + key: 'export-as-png', + description: "Export This View's Data as PNG", + cssClass: 'icon-download', + group: 'view', + invoke(objectPath, view) { + view.getViewContext().exportPNG(); + } }; const exportJPG = { - name: 'Export as JPG', - key: 'export-as-jpg', - description: 'Export This View\'s Data as JPG', - cssClass: 'icon-download', - group: 'view', - invoke(objectPath, view) { - view.getViewContext().exportJPG(); - } + name: 'Export as JPG', + key: 'export-as-jpg', + description: "Export This View's Data as JPG", + cssClass: 'icon-download', + group: 'view', + invoke(objectPath, view) { + view.getViewContext().exportJPG(); + } }; -const viewActions = [ - exportPNG, - exportJPG -]; +const viewActions = [exportPNG, exportJPG]; -viewActions.forEach(action => { - action.appliesTo = (objectPath, view = {}) => { - return isPlotView(view); - }; +viewActions.forEach((action) => { + action.appliesTo = (objectPath, view = {}) => { + return isPlotView(view); + }; }); export default viewActions; diff --git a/src/plugins/plot/actions/utils.js b/src/plugins/plot/actions/utils.js index 2bebbecf4d..8df92f9979 100644 --- a/src/plugins/plot/actions/utils.js +++ b/src/plugins/plot/actions/utils.js @@ -1,3 +1,3 @@ export function isPlotView(view) { - return view.key === 'plot-single' || view.key === 'plot-overlay' || view.key === 'plot-stacked'; + return view.key === 'plot-single' || view.key === 'plot-overlay' || view.key === 'plot-stacked'; } diff --git a/src/plugins/plot/axis/XAxis.vue b/src/plugins/plot/axis/XAxis.vue index 267b2e3b05..2328494230 100644 --- a/src/plugins/plot/axis/XAxis.vue +++ b/src/plugins/plot/axis/XAxis.vue @@ -21,139 +21,125 @@ --> diff --git a/src/plugins/plot/axis/YAxis.vue b/src/plugins/plot/axis/YAxis.vue index bb1304e57a..b756db3c9f 100644 --- a/src/plugins/plot/axis/YAxis.vue +++ b/src/plugins/plot/axis/YAxis.vue @@ -20,262 +20,262 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/chart/LimitLabel.vue b/src/plugins/plot/chart/LimitLabel.vue index 2d00906324..5e67d891f7 100644 --- a/src/plugins/plot/chart/LimitLabel.vue +++ b/src/plugins/plot/chart/LimitLabel.vue @@ -20,55 +20,51 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/chart/LimitLine.vue b/src/plugins/plot/chart/LimitLine.vue index 777efaf082..4026a38fab 100644 --- a/src/plugins/plot/chart/LimitLine.vue +++ b/src/plugins/plot/chart/LimitLine.vue @@ -20,43 +20,39 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/chart/MCTChartAlarmLineSet.js b/src/plugins/plot/chart/MCTChartAlarmLineSet.js index 45e9f105e8..df87a6c0d8 100644 --- a/src/plugins/plot/chart/MCTChartAlarmLineSet.js +++ b/src/plugins/plot/chart/MCTChartAlarmLineSet.js @@ -23,97 +23,102 @@ import eventHelpers from '../lib/eventHelpers'; export default class MCTChartAlarmLineSet { - /** - * @param {Bounds} bounds - */ - constructor(series, chart, offset, bounds) { - this.series = series; - this.chart = chart; - this.offset = offset; - this.bounds = bounds; - this.limits = []; + /** + * @param {Bounds} bounds + */ + constructor(series, chart, offset, bounds) { + this.series = series; + this.chart = chart; + this.offset = offset; + this.bounds = bounds; + this.limits = []; - eventHelpers.extend(this); - this.listenTo(series, 'limitBounds', this.updateBounds, this); - this.listenTo(series, 'limits', this.getLimitPoints, this); - this.listenTo(series, 'change:xKey', this.getLimitPoints, this); + eventHelpers.extend(this); + this.listenTo(series, 'limitBounds', this.updateBounds, this); + this.listenTo(series, 'limits', this.getLimitPoints, this); + this.listenTo(series, 'change:xKey', this.getLimitPoints, this); - if (series.limits) { - this.getLimitPoints(series); - } + if (series.limits) { + this.getLimitPoints(series); + } + } + + /** + * @param {Bounds} bounds + */ + updateBounds(bounds) { + this.bounds = bounds; + this.getLimitPoints(this.series); + } + + color() { + return this.series.get('color'); + } + + name() { + return this.series.get('name'); + } + + makePoint(point, series) { + if (!this.offset.xVal) { + this.chart.setOffset(point, undefined, series); } - /** - * @param {Bounds} bounds - */ - updateBounds(bounds) { - this.bounds = bounds; - this.getLimitPoints(this.series); - } - - color() { - return this.series.get('color'); - } - - name() { - return this.series.get('name'); - } - - makePoint(point, series) { - if (!this.offset.xVal) { - this.chart.setOffset(point, undefined, series); - } - - return { - x: this.offset.xVal(point, series), - y: this.offset.yVal(point, series) - }; - } - - getLimitPoints(series) { - this.limits = []; - let xKey = series.get('xKey'); - Object.keys(series.limits).forEach((key) => { - const limitForLevel = series.limits[key]; - if (limitForLevel.high) { - this.limits.push({ - seriesKey: series.keyString, - level: key.toLowerCase(), - name: this.name(), - seriesColor: series.get('color').asHexString(), - point: this.makePoint(Object.assign({ [xKey]: this.bounds.start }, limitForLevel.high), series), - value: series.getYVal(limitForLevel.high), - color: limitForLevel.high.color, - isUpper: true - }); - } - - if (limitForLevel.low) { - this.limits.push({ - seriesKey: series.keyString, - level: key.toLowerCase(), - name: this.name(), - seriesColor: series.get('color').asHexString(), - point: this.makePoint(Object.assign({ [xKey]: this.bounds.start }, limitForLevel.low), series), - value: series.getYVal(limitForLevel.low), - color: limitForLevel.low.color, - isUpper: false - }); - } - }, this); - } - - reset() { - this.limits = []; - if (this.series.limits) { - this.getLimitPoints(this.series); - } - } - - destroy() { - this.stopListening(); + return { + x: this.offset.xVal(point, series), + y: this.offset.yVal(point, series) + }; + } + + getLimitPoints(series) { + this.limits = []; + let xKey = series.get('xKey'); + Object.keys(series.limits).forEach((key) => { + const limitForLevel = series.limits[key]; + if (limitForLevel.high) { + this.limits.push({ + seriesKey: series.keyString, + level: key.toLowerCase(), + name: this.name(), + seriesColor: series.get('color').asHexString(), + point: this.makePoint( + Object.assign({ [xKey]: this.bounds.start }, limitForLevel.high), + series + ), + value: series.getYVal(limitForLevel.high), + color: limitForLevel.high.color, + isUpper: true + }); + } + + if (limitForLevel.low) { + this.limits.push({ + seriesKey: series.keyString, + level: key.toLowerCase(), + name: this.name(), + seriesColor: series.get('color').asHexString(), + point: this.makePoint( + Object.assign({ [xKey]: this.bounds.start }, limitForLevel.low), + series + ), + value: series.getYVal(limitForLevel.low), + color: limitForLevel.low.color, + isUpper: false + }); + } + }, this); + } + + reset() { + this.limits = []; + if (this.series.limits) { + this.getLimitPoints(this.series); } + } + destroy() { + this.stopListening(); + } } /** diff --git a/src/plugins/plot/chart/MCTChartAlarmPointSet.js b/src/plugins/plot/chart/MCTChartAlarmPointSet.js index dd2f9cef09..8d26b29e51 100644 --- a/src/plugins/plot/chart/MCTChartAlarmPointSet.js +++ b/src/plugins/plot/chart/MCTChartAlarmPointSet.js @@ -23,45 +23,45 @@ import eventHelpers from '../lib/eventHelpers'; export default class MCTChartAlarmPointSet { - constructor(series, chart, offset) { - this.series = series; - this.chart = chart; - this.offset = offset; - this.points = []; + constructor(series, chart, offset) { + this.series = series; + this.chart = chart; + this.offset = offset; + this.points = []; - eventHelpers.extend(this); + eventHelpers.extend(this); - this.listenTo(series, 'add', this.append, this); - this.listenTo(series, 'remove', this.remove, this); - this.listenTo(series, 'reset', this.reset, this); - this.listenTo(series, 'destroy', this.destroy, this); + this.listenTo(series, 'add', this.append, this); + this.listenTo(series, 'remove', this.remove, this); + this.listenTo(series, 'reset', this.reset, this); + this.listenTo(series, 'destroy', this.destroy, this); - this.series.getSeriesData().forEach(function (point, index) { - this.append(point, index, series); - }, this); + this.series.getSeriesData().forEach(function (point, index) { + this.append(point, index, series); + }, this); + } + + append(datum) { + if (datum.mctLimitState) { + this.points.push({ + x: this.offset.xVal(datum, this.series), + y: this.offset.yVal(datum, this.series), + datum: datum + }); } + } - append(datum) { - if (datum.mctLimitState) { - this.points.push({ - x: this.offset.xVal(datum, this.series), - y: this.offset.yVal(datum, this.series), - datum: datum - }); - } - } + remove(datum) { + this.points = this.points.filter(function (p) { + return p.datum !== datum; + }); + } - remove(datum) { - this.points = this.points.filter(function (p) { - return p.datum !== datum; - }); - } + reset() { + this.points = []; + } - reset() { - this.points = []; - } - - destroy() { - this.stopListening(); - } + destroy() { + this.stopListening(); + } } diff --git a/src/plugins/plot/chart/MCTChartLineLinear.js b/src/plugins/plot/chart/MCTChartLineLinear.js index 1df5af7c92..32c8546077 100644 --- a/src/plugins/plot/chart/MCTChartLineLinear.js +++ b/src/plugins/plot/chart/MCTChartLineLinear.js @@ -23,9 +23,8 @@ import MCTChartSeriesElement from './MCTChartSeriesElement'; export default class MCTChartLineLinear extends MCTChartSeriesElement { - addPoint(point, start) { - this.buffer[start] = point.x; - this.buffer[start + 1] = point.y; - } + addPoint(point, start) { + this.buffer[start] = point.x; + this.buffer[start + 1] = point.y; + } } - diff --git a/src/plugins/plot/chart/MCTChartLineStepAfter.js b/src/plugins/plot/chart/MCTChartLineStepAfter.js index 0073819170..05a7d2c2fe 100644 --- a/src/plugins/plot/chart/MCTChartLineStepAfter.js +++ b/src/plugins/plot/chart/MCTChartLineStepAfter.js @@ -23,52 +23,51 @@ import MCTChartSeriesElement from './MCTChartSeriesElement'; export default class MCTChartLineStepAfter extends MCTChartSeriesElement { - removePoint(index) { - if (index > 0 && index / 2 < this.count) { - this.buffer[index + 1] = this.buffer[index - 1]; - } + removePoint(index) { + if (index > 0 && index / 2 < this.count) { + this.buffer[index + 1] = this.buffer[index - 1]; + } + } + + vertexCountForPointAtIndex(index) { + if (index === 0 && this.count === 0) { + return 2; } - vertexCountForPointAtIndex(index) { - if (index === 0 && this.count === 0) { - return 2; - } + return 4; + } - return 4; + startIndexForPointAtIndex(index) { + if (index === 0) { + return 0; } - startIndexForPointAtIndex(index) { - if (index === 0) { - return 0; - } + return 2 + (index - 1) * 4; + } - return 2 + ((index - 1) * 4); - } - - addPoint(point, start) { - if (start === 0 && this.count === 0) { - // First point is easy. - this.buffer[start] = point.x; - this.buffer[start + 1] = point.y; // one point - } else if (start === 0 && this.count > 0) { - // Unshifting requires adding an extra point. - this.buffer[start] = point.x; - this.buffer[start + 1] = point.y; - this.buffer[start + 2] = this.buffer[start + 4]; - this.buffer[start + 3] = point.y; - } else { - // Appending anywhere in line, insert standard two points. - this.buffer[start] = point.x; - this.buffer[start + 1] = this.buffer[start - 1]; - this.buffer[start + 2] = point.x; - this.buffer[start + 3] = point.y; - - if (start < this.count * 2) { - // Insert into the middle, need to update the following - // point. - this.buffer[start + 5] = point.y; - } - } + addPoint(point, start) { + if (start === 0 && this.count === 0) { + // First point is easy. + this.buffer[start] = point.x; + this.buffer[start + 1] = point.y; // one point + } else if (start === 0 && this.count > 0) { + // Unshifting requires adding an extra point. + this.buffer[start] = point.x; + this.buffer[start + 1] = point.y; + this.buffer[start + 2] = this.buffer[start + 4]; + this.buffer[start + 3] = point.y; + } else { + // Appending anywhere in line, insert standard two points. + this.buffer[start] = point.x; + this.buffer[start + 1] = this.buffer[start - 1]; + this.buffer[start + 2] = point.x; + this.buffer[start + 3] = point.y; + + if (start < this.count * 2) { + // Insert into the middle, need to update the following + // point. + this.buffer[start + 5] = point.y; + } } + } } - diff --git a/src/plugins/plot/chart/MCTChartPointSet.js b/src/plugins/plot/chart/MCTChartPointSet.js index 4c572b772d..fb04eb58f9 100644 --- a/src/plugins/plot/chart/MCTChartPointSet.js +++ b/src/plugins/plot/chart/MCTChartPointSet.js @@ -24,9 +24,8 @@ import MCTChartSeriesElement from './MCTChartSeriesElement'; // TODO: Is this needed? This is identical to MCTChartLineLinear. Why is it a different class? export default class MCTChartPointSet extends MCTChartSeriesElement { - addPoint(point, start) { - this.buffer[start] = point.x; - this.buffer[start + 1] = point.y; - } + addPoint(point, start) { + this.buffer[start] = point.x; + this.buffer[start + 1] = point.y; + } } - diff --git a/src/plugins/plot/chart/MCTChartSeriesElement.js b/src/plugins/plot/chart/MCTChartSeriesElement.js index e8557655ce..709f817e68 100644 --- a/src/plugins/plot/chart/MCTChartSeriesElement.js +++ b/src/plugins/plot/chart/MCTChartSeriesElement.js @@ -24,134 +24,133 @@ import eventHelpers from '../lib/eventHelpers'; /** @abstract */ export default class MCTChartSeriesElement { - constructor(series, chart, offset) { - this.series = series; - this.chart = chart; - this.offset = offset; - this.buffer = new Float32Array(20000); - this.count = 0; + constructor(series, chart, offset) { + this.series = series; + this.chart = chart; + this.offset = offset; + this.buffer = new Float32Array(20000); + this.count = 0; - eventHelpers.extend(this); + eventHelpers.extend(this); - this.listenTo(series, 'add', this.append, this); - this.listenTo(series, 'remove', this.remove, this); - this.listenTo(series, 'reset', this.reset, this); - this.listenTo(series, 'destroy', this.destroy, this); - this.series.getSeriesData().forEach(function (point, index) { - this.append(point, index, series); - }, this); + this.listenTo(series, 'add', this.append, this); + this.listenTo(series, 'remove', this.remove, this); + this.listenTo(series, 'reset', this.reset, this); + this.listenTo(series, 'destroy', this.destroy, this); + this.series.getSeriesData().forEach(function (point, index) { + this.append(point, index, series); + }, this); + } + + getBuffer() { + if (this.isTempBuffer) { + this.buffer = new Float32Array(this.buffer); + this.isTempBuffer = false; } - getBuffer() { - if (this.isTempBuffer) { - this.buffer = new Float32Array(this.buffer); - this.isTempBuffer = false; - } + return this.buffer; + } - return this.buffer; + color() { + return this.series.get('color'); + } + + vertexCountForPointAtIndex(index) { + return 2; + } + + startIndexForPointAtIndex(index) { + return 2 * index; + } + + removeSegments(index, count) { + const target = index; + const start = index + count; + const end = this.count * 2; + this.buffer.copyWithin(target, start, end); + for (let zero = end - count; zero < end; zero++) { + this.buffer[zero] = 0; + } + } + + /** @abstract */ + removePoint(index) {} + + /** @abstract */ + addPoint(point, index) {} + + remove(point, index, series) { + const vertexCount = this.vertexCountForPointAtIndex(index); + const removalPoint = this.startIndexForPointAtIndex(index); + + this.removeSegments(removalPoint, vertexCount); + + // TODO useless makePoint call? + this.makePoint(point, series); + this.removePoint(removalPoint); + + this.count -= vertexCount / 2; + } + + makePoint(point, series) { + if (!this.offset.xVal) { + this.chart.setOffset(point, undefined, series); } - color() { - return this.series.get('color'); + return { + x: this.offset.xVal(point, series), + y: this.offset.yVal(point, series) + }; + } + + append(point, index, series) { + const pointsRequired = this.vertexCountForPointAtIndex(index); + const insertionPoint = this.startIndexForPointAtIndex(index); + this.growIfNeeded(pointsRequired); + this.makeInsertionPoint(insertionPoint, pointsRequired); + this.addPoint(this.makePoint(point, series), insertionPoint); + this.count += pointsRequired / 2; + } + + makeInsertionPoint(insertionPoint, pointsRequired) { + if (this.count * 2 > insertionPoint) { + if (!this.isTempBuffer) { + this.buffer = Array.prototype.slice.apply(this.buffer); + this.isTempBuffer = true; + } + + const target = insertionPoint + pointsRequired; + let start = insertionPoint; + for (; start < target; start++) { + this.buffer.splice(start, 0, 0); + } } + } - vertexCountForPointAtIndex(index) { - return 2; + reset() { + this.buffer = new Float32Array(20000); + this.count = 0; + if (this.offset.x) { + this.series.getSeriesData().forEach(function (point, index) { + this.append(point, index, this.series); + }, this); } + } - startIndexForPointAtIndex(index) { - return 2 * index; - } - - removeSegments(index, count) { - const target = index; - const start = index + count; - const end = this.count * 2; - this.buffer.copyWithin(target, start, end); - for (let zero = end - count; zero < end; zero++) { - this.buffer[zero] = 0; - } - } - - /** @abstract */ - removePoint(index) {} - - /** @abstract */ - addPoint(point, index) {} - - remove(point, index, series) { - const vertexCount = this.vertexCountForPointAtIndex(index); - const removalPoint = this.startIndexForPointAtIndex(index); - - this.removeSegments(removalPoint, vertexCount); - - // TODO useless makePoint call? - this.makePoint(point, series); - this.removePoint(removalPoint); - - this.count -= (vertexCount / 2); - } - - makePoint(point, series) { - if (!this.offset.xVal) { - this.chart.setOffset(point, undefined, series); - } - - return { - x: this.offset.xVal(point, series), - y: this.offset.yVal(point, series) - }; - } - - append(point, index, series) { - const pointsRequired = this.vertexCountForPointAtIndex(index); - const insertionPoint = this.startIndexForPointAtIndex(index); - this.growIfNeeded(pointsRequired); - this.makeInsertionPoint(insertionPoint, pointsRequired); - this.addPoint(this.makePoint(point, series), insertionPoint); - this.count += (pointsRequired / 2); - } - - makeInsertionPoint(insertionPoint, pointsRequired) { - if (this.count * 2 > insertionPoint) { - if (!this.isTempBuffer) { - this.buffer = Array.prototype.slice.apply(this.buffer); - this.isTempBuffer = true; - } - - const target = insertionPoint + pointsRequired; - let start = insertionPoint; - for (; start < target; start++) { - this.buffer.splice(start, 0, 0); - } - } - } - - reset() { - this.buffer = new Float32Array(20000); - this.count = 0; - if (this.offset.x) { - this.series.getSeriesData().forEach(function (point, index) { - this.append(point, index, this.series); - }, this); - } - } - - growIfNeeded(pointsRequired) { - const remainingPoints = this.buffer.length - this.count * 2; - let temp; - - if (remainingPoints <= pointsRequired) { - temp = new Float32Array(this.buffer.length + 20000); - temp.set(this.buffer); - this.buffer = temp; - } - } - - destroy() { - this.stopListening(); + growIfNeeded(pointsRequired) { + const remainingPoints = this.buffer.length - this.count * 2; + let temp; + + if (remainingPoints <= pointsRequired) { + temp = new Float32Array(this.buffer.length + 20000); + temp.set(this.buffer); + this.buffer = temp; } + } + destroy() { + this.stopListening(); + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/chart/MctChart.vue b/src/plugins/plot/chart/MctChart.vue index 24e961f096..a4bc9332fb 100644 --- a/src/plugins/plot/chart/MctChart.vue +++ b/src/plugins/plot/chart/MctChart.vue @@ -23,29 +23,25 @@ diff --git a/src/plugins/plot/chart/limitUtil.js b/src/plugins/plot/chart/limitUtil.js index 6ca4df4482..ffe82ce38a 100644 --- a/src/plugins/plot/chart/limitUtil.js +++ b/src/plugins/plot/chart/limitUtil.js @@ -1,32 +1,32 @@ export function getLimitClass(limit, prefix) { - let cssClass = ''; - //If color exists then use it, fall back to the cssClass - if (limit.color) { - cssClass = `${cssClass} ${prefix}${limit.color}`; - } else if (limit.cssClass) { - cssClass = `${cssClass}${limit.cssClass}`; + let cssClass = ''; + //If color exists then use it, fall back to the cssClass + if (limit.color) { + cssClass = `${cssClass} ${prefix}${limit.color}`; + } else if (limit.cssClass) { + cssClass = `${cssClass}${limit.cssClass}`; + } + + // If we applied the cssClass then skip these classes + if (limit.cssClass === undefined) { + if (limit.isUpper) { + cssClass = `${cssClass} ${prefix}upr`; + } else { + cssClass = `${cssClass} ${prefix}lwr`; } - // If we applied the cssClass then skip these classes - if (limit.cssClass === undefined) { - if (limit.isUpper) { - cssClass = `${cssClass} ${prefix}upr`; - } else { - cssClass = `${cssClass} ${prefix}lwr`; - } - - if (limit.level) { - cssClass = `${cssClass} ${prefix}${limit.level}`; - } - - if (limit.needsHorizontalAdjustment) { - cssClass = `${cssClass} --align-label-right`; - } - - if (limit.needsVerticalAdjustment) { - cssClass = `${cssClass} --align-label-below`; - } + if (limit.level) { + cssClass = `${cssClass} ${prefix}${limit.level}`; } - return cssClass; + if (limit.needsHorizontalAdjustment) { + cssClass = `${cssClass} --align-label-right`; + } + + if (limit.needsVerticalAdjustment) { + cssClass = `${cssClass} --align-label-below`; + } + } + + return cssClass; } diff --git a/src/plugins/plot/configuration/Collection.js b/src/plugins/plot/configuration/Collection.js index 03f93ec9e1..d46ae0adf2 100644 --- a/src/plugins/plot/configuration/Collection.js +++ b/src/plugins/plot/configuration/Collection.js @@ -27,91 +27,90 @@ import Model from './Model'; * @extends {Model} */ export default class Collection extends Model { - /** @type {Constructor} */ - modelClass = Model; + /** @type {Constructor} */ + modelClass = Model; - initialize(options) { - super.initialize(options); - if (options.models) { - this.models = options.models.map(this.modelFn, this); - } else { - this.models = []; - } + initialize(options) { + super.initialize(options); + if (options.models) { + this.models = options.models.map(this.modelFn, this); + } else { + this.models = []; + } + } + + modelFn(model) { + //TODO: Come back to this - why are we doing this? + if (model instanceof this.modelClass) { + model.collection = this; + + return model; } - modelFn(model) { - //TODO: Come back to this - why are we doing this? - if (model instanceof this.modelClass) { - model.collection = this; + return new this.modelClass({ + collection: this, + model: model + }); + } - return model; + first() { + return this.at(0); + } - } + forEach(iteree, context) { + this.models.forEach(iteree, context); + } - return new this.modelClass({ - collection: this, - model: model - }); + map(iteree, context) { + return this.models.map(iteree, context); + } + + filter(iteree, context) { + return this.models.filter(iteree, context); + } + + size() { + return this.models.length; + } + + at(index) { + return this.models[index]; + } + + add(model) { + model = this.modelFn(model); + const index = this.models.length; + this.models.push(model); + this.emit('add', model, index); + } + + insert(model, index) { + model = this.modelFn(model); + this.models.splice(index, 0, model); + this.emit('add', model, index + 1); + } + + indexOf(model) { + return this.models.findIndex((m) => m === model); + } + + remove(model) { + const index = this.indexOf(model); + + if (index === -1) { + throw new Error('model not found in collection.'); } - first() { - return this.at(0); - } + this.models.splice(index, 1); + this.emit('remove', model, index); + } - forEach(iteree, context) { - this.models.forEach(iteree, context); - } - - map(iteree, context) { - return this.models.map(iteree, context); - } - - filter(iteree, context) { - return this.models.filter(iteree, context); - } - - size() { - return this.models.length; - } - - at(index) { - return this.models[index]; - } - - add(model) { - model = this.modelFn(model); - const index = this.models.length; - this.models.push(model); - this.emit('add', model, index); - } - - insert(model, index) { - model = this.modelFn(model); - this.models.splice(index, 0, model); - this.emit('add', model, index + 1); - } - - indexOf(model) { - return this.models.findIndex(m => m === model); - } - - remove(model) { - const index = this.indexOf(model); - - if (index === -1) { - throw new Error('model not found in collection.'); - } - - this.models.splice(index, 1); - this.emit('remove', model, index); - } - - destroy(model) { - this.forEach(function (m) { - m.destroy(); - }); - this.stopListening(); - } + destroy(model) { + this.forEach(function (m) { + m.destroy(); + }); + this.stopListening(); + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/configuration/ConfigStore.js b/src/plugins/plot/configuration/ConfigStore.js index c47a5edbdb..02dc354be5 100644 --- a/src/plugins/plot/configuration/ConfigStore.js +++ b/src/plugins/plot/configuration/ConfigStore.js @@ -20,42 +20,42 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ class ConfigStore { - /** @type {Record} */ - store = {}; + /** @type {Record} */ + store = {}; - /** + /** @param {string} id */ - deleteStore(id) { - const obj = this.store[id]; + deleteStore(id) { + const obj = this.store[id]; - if (obj) { - if (obj.destroy) { - obj.destroy(); - } + if (obj) { + if (obj.destroy) { + obj.destroy(); + } - delete this.store[id]; - } + delete this.store[id]; } + } - deleteAll() { - Object.keys(this.store).forEach(id => this.deleteStore(id)); - } + deleteAll() { + Object.keys(this.store).forEach((id) => this.deleteStore(id)); + } - /** + /** @param {string} id @param {any} config */ - add(id, config) { - this.store[id] = config; - } + add(id, config) { + this.store[id] = config; + } - /** + /** @param {string} id */ - get(id) { - return this.store[id]; - } + get(id) { + return this.store[id]; + } } const STORE = new ConfigStore(); diff --git a/src/plugins/plot/configuration/LegendModel.js b/src/plugins/plot/configuration/LegendModel.js index e145bffee8..d8da5a500c 100644 --- a/src/plugins/plot/configuration/LegendModel.js +++ b/src/plugins/plot/configuration/LegendModel.js @@ -20,42 +20,42 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import Model from "./Model"; +import Model from './Model'; /** * TODO: doc strings. */ export default class LegendModel extends Model { - listenToSeriesCollection(seriesCollection) { - this.seriesCollection = seriesCollection; - this.listenTo(this.seriesCollection, 'add', this.setHeight, this); - this.listenTo(this.seriesCollection, 'remove', this.setHeight, this); - this.listenTo(this, 'change:expanded', this.setHeight, this); - this.set('expanded', this.get('expandByDefault')); - } + listenToSeriesCollection(seriesCollection) { + this.seriesCollection = seriesCollection; + this.listenTo(this.seriesCollection, 'add', this.setHeight, this); + this.listenTo(this.seriesCollection, 'remove', this.setHeight, this); + this.listenTo(this, 'change:expanded', this.setHeight, this); + this.set('expanded', this.get('expandByDefault')); + } - setHeight() { - const expanded = this.get('expanded'); - if (this.get('position') !== 'top') { - this.set('height', '0px'); - } else { - this.set('height', expanded ? (20 * (this.seriesCollection.size() + 1) + 40) + 'px' : '21px'); - } + setHeight() { + const expanded = this.get('expanded'); + if (this.get('position') !== 'top') { + this.set('height', '0px'); + } else { + this.set('height', expanded ? 20 * (this.seriesCollection.size() + 1) + 40 + 'px' : '21px'); } + } - /** - * @override - */ - defaultModel(options) { - return { - position: 'top', - expandByDefault: false, - hideLegendWhenSmall: false, - valueToShowWhenCollapsed: 'nearestValue', - showTimestampWhenExpanded: true, - showValueWhenExpanded: true, - showMaximumWhenExpanded: true, - showMinimumWhenExpanded: true, - showUnitsWhenExpanded: true - }; - } + /** + * @override + */ + defaultModel(options) { + return { + position: 'top', + expandByDefault: false, + hideLegendWhenSmall: false, + valueToShowWhenCollapsed: 'nearestValue', + showTimestampWhenExpanded: true, + showValueWhenExpanded: true, + showMaximumWhenExpanded: true, + showMinimumWhenExpanded: true, + showUnitsWhenExpanded: true + }; + } } diff --git a/src/plugins/plot/configuration/Model.js b/src/plugins/plot/configuration/Model.js index bd733764c4..428a2ecde5 100644 --- a/src/plugins/plot/configuration/Model.js +++ b/src/plugins/plot/configuration/Model.js @@ -21,7 +21,7 @@ *****************************************************************************/ import EventEmitter from 'eventemitter3'; -import eventHelpers from "../lib/eventHelpers"; +import eventHelpers from '../lib/eventHelpers'; import _ from 'lodash'; /** @@ -29,113 +29,111 @@ import _ from 'lodash'; * @template {object} O */ export default class Model extends EventEmitter { - /** - * @param {ModelOptions} options - */ - constructor(options) { - super(); - Object.defineProperty(this, '_events', { - value: this._events, - enumerable: false, - configurable: false, - writable: true - }); + /** + * @param {ModelOptions} options + */ + constructor(options) { + super(); + Object.defineProperty(this, '_events', { + value: this._events, + enumerable: false, + configurable: false, + writable: true + }); - //need to do this as we're already extending EventEmitter - eventHelpers.extend(this); + //need to do this as we're already extending EventEmitter + eventHelpers.extend(this); - if (!options) { - options = {}; - } - - // FIXME: this.id is defined as a method further below, but here it is - // assigned a possibly-undefined value. Is this code unused? - this.id = options.id; - - /** @type {ModelType} */ - this.model = options.model; - this.collection = options.collection; - const defaults = this.defaultModel(options); - if (!this.model) { - this.model = options.model = defaults; - } else { - _.defaultsDeep(this.model, defaults); - } - - this.initialize(options); - - /** @type {keyof ModelType } */ - this.idAttr = 'id'; + if (!options) { + options = {}; } - /** - * @param {ModelOptions} options - * @returns {ModelType} - */ - defaultModel(options) { - return {}; + // FIXME: this.id is defined as a method further below, but here it is + // assigned a possibly-undefined value. Is this code unused? + this.id = options.id; + + /** @type {ModelType} */ + this.model = options.model; + this.collection = options.collection; + const defaults = this.defaultModel(options); + if (!this.model) { + this.model = options.model = defaults; + } else { + _.defaultsDeep(this.model, defaults); } - /** - * @abstract - * @param {ModelOptions} options - */ - initialize(options) { + this.initialize(options); - } + /** @type {keyof ModelType } */ + this.idAttr = 'id'; + } - /** - * Destroy the model, removing all listeners and subscriptions. - */ - destroy() { - this.emit('destroy'); - this.removeAllListeners(); - } + /** + * @param {ModelOptions} options + * @returns {ModelType} + */ + defaultModel(options) { + return {}; + } - id() { - return this.get(this.idAttr); - } + /** + * @abstract + * @param {ModelOptions} options + */ + initialize(options) {} - /** - * @template {keyof ModelType} K - * @param {K} attribute - * @returns {ModelType[K]} - */ - get(attribute) { - return this.model[attribute]; - } + /** + * Destroy the model, removing all listeners and subscriptions. + */ + destroy() { + this.emit('destroy'); + this.removeAllListeners(); + } - /** - * @template {keyof ModelType} K - * @param {K} attribute - * @returns boolean - */ - has(attribute) { - return _.has(this.model, attribute); - } + id() { + return this.get(this.idAttr); + } - /** - * @template {keyof ModelType} K - * @param {K} attribute - * @param {ModelType[K]} value - */ - set(attribute, value) { - const oldValue = this.model[attribute]; - this.model[attribute] = value; - this.emit('change', attribute, value, oldValue, this); - this.emit('change:' + attribute, value, oldValue, this); - } + /** + * @template {keyof ModelType} K + * @param {K} attribute + * @returns {ModelType[K]} + */ + get(attribute) { + return this.model[attribute]; + } - /** - * @template {keyof ModelType} K - * @param {K} attribute - */ - unset(attribute) { - const oldValue = this.model[attribute]; - delete this.model[attribute]; - this.emit('change', attribute, undefined, oldValue, this); - this.emit('change:' + attribute, undefined, oldValue, this); - } + /** + * @template {keyof ModelType} K + * @param {K} attribute + * @returns boolean + */ + has(attribute) { + return _.has(this.model, attribute); + } + + /** + * @template {keyof ModelType} K + * @param {K} attribute + * @param {ModelType[K]} value + */ + set(attribute, value) { + const oldValue = this.model[attribute]; + this.model[attribute] = value; + this.emit('change', attribute, value, oldValue, this); + this.emit('change:' + attribute, value, oldValue, this); + } + + /** + * @template {keyof ModelType} K + * @param {K} attribute + */ + unset(attribute) { + const oldValue = this.model[attribute]; + delete this.model[attribute]; + this.emit('change', attribute, undefined, oldValue, this); + this.emit('change:' + attribute, undefined, oldValue, this); + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/configuration/PlotConfigurationModel.js b/src/plugins/plot/configuration/PlotConfigurationModel.js index bb67f6272a..87a0922a50 100644 --- a/src/plugins/plot/configuration/PlotConfigurationModel.js +++ b/src/plugins/plot/configuration/PlotConfigurationModel.js @@ -21,11 +21,11 @@ *****************************************************************************/ import _ from 'lodash'; -import Model from "./Model"; -import SeriesCollection from "./SeriesCollection"; -import XAxisModel from "./XAxisModel"; -import YAxisModel from "./YAxisModel"; -import LegendModel from "./LegendModel"; +import Model from './Model'; +import SeriesCollection from './SeriesCollection'; +import XAxisModel from './XAxisModel'; +import YAxisModel from './YAxisModel'; +import LegendModel from './LegendModel'; const MAX_Y_AXES = 3; const MAIN_Y_AXES_ID = 1; @@ -39,149 +39,157 @@ const MAX_ADDITIONAL_AXES = MAX_Y_AXES - 1; * @extends {Model} */ export default class PlotConfigurationModel extends Model { - /** - * Initializes all sub models and then passes references to submodels - * to those that need it. - * - * @override - * @param {import('./Model').ModelOptions} options - */ - initialize(options) { - this.openmct = options.openmct; + /** + * Initializes all sub models and then passes references to submodels + * to those that need it. + * + * @override + * @param {import('./Model').ModelOptions} options + */ + initialize(options) { + this.openmct = options.openmct; - // This is a type assertion for TypeScript, this error is never thrown in practice. - if (!options.model) { - throw new Error('Not a collection model.'); - } + // This is a type assertion for TypeScript, this error is never thrown in practice. + if (!options.model) { + throw new Error('Not a collection model.'); + } - this.xAxis = new XAxisModel({ - model: options.model.xAxis, - plot: this, - openmct: options.openmct - }); - this.yAxis = new YAxisModel({ - model: options.model.yAxis, + this.xAxis = new XAxisModel({ + model: options.model.xAxis, + plot: this, + openmct: options.openmct + }); + this.yAxis = new YAxisModel({ + model: options.model.yAxis, + plot: this, + openmct: options.openmct, + id: options.model.yAxis.id || MAIN_Y_AXES_ID + }); + //Add any axes in addition to the main yAxis above - we must always have at least 1 y-axis + //Addition axes ids will be the MAIN_Y_AXES_ID + x where x is between 1 and MAX_ADDITIONAL_AXES + this.additionalYAxes = []; + const hasAdditionalAxesConfiguration = Array.isArray(options.model.additionalYAxes); + + for (let yAxisCount = 0; yAxisCount < MAX_ADDITIONAL_AXES; yAxisCount++) { + const yAxisId = MAIN_Y_AXES_ID + yAxisCount + 1; + const yAxis = + hasAdditionalAxesConfiguration && + options.model.additionalYAxes.find((additionalYAxis) => additionalYAxis?.id === yAxisId); + if (yAxis) { + this.additionalYAxes.push( + new YAxisModel({ + model: yAxis, plot: this, openmct: options.openmct, - id: options.model.yAxis.id || MAIN_Y_AXES_ID - }); - //Add any axes in addition to the main yAxis above - we must always have at least 1 y-axis - //Addition axes ids will be the MAIN_Y_AXES_ID + x where x is between 1 and MAX_ADDITIONAL_AXES - this.additionalYAxes = []; - const hasAdditionalAxesConfiguration = Array.isArray(options.model.additionalYAxes); - - for (let yAxisCount = 0; yAxisCount < MAX_ADDITIONAL_AXES; yAxisCount++) { - const yAxisId = MAIN_Y_AXES_ID + yAxisCount + 1; - const yAxis = hasAdditionalAxesConfiguration && options.model.additionalYAxes.find(additionalYAxis => additionalYAxis?.id === yAxisId); - if (yAxis) { - this.additionalYAxes.push(new YAxisModel({ - model: yAxis, - plot: this, - openmct: options.openmct, - id: yAxis.id - })); - } else { - this.additionalYAxes.push(new YAxisModel({ - plot: this, - openmct: options.openmct, - id: yAxisId - })); - } - } - // end add additional axes - - this.legend = new LegendModel({ - model: options.model.legend, - plot: this, - openmct: options.openmct - }); - this.series = new SeriesCollection({ - models: options.model.series, + id: yAxis.id + }) + ); + } else { + this.additionalYAxes.push( + new YAxisModel({ plot: this, openmct: options.openmct, - palette: options.palette - }); - - if (this.get('domainObject').type === 'telemetry.plot.overlay') { - this.removeMutationListener = this.openmct.objects.observe( - this.get('domainObject'), - '*', - this.updateDomainObject.bind(this) - ); - } - - this.yAxis.listenToSeriesCollection(this.series); - this.additionalYAxes.forEach(yAxis => { - yAxis.listenToSeriesCollection(this.series); - }); - this.legend.listenToSeriesCollection(this.series); - - this.listenTo(this, 'destroy', this.onDestroy, this); + id: yAxisId + }) + ); + } } - /** - * Retrieve the persisted series config for a given identifier. - * @param {import('./PlotSeries').Identifier} identifier - * @returns {import('./PlotSeries').PlotSeriesModelType=} - */ - getPersistedSeriesConfig(identifier) { - const domainObject = this.get('domainObject'); - if (!domainObject.configuration || !domainObject.configuration.series) { - return; - } + // end add additional axes - return domainObject.configuration.series.filter(function (seriesConfig) { - return seriesConfig.identifier.key === identifier.key - && seriesConfig.identifier.namespace === identifier.namespace; - })[0]; - } - /** - * Retrieve the persisted filters for a given identifier. - */ - getPersistedFilters(identifier) { - const domainObject = this.get('domainObject'); - const keystring = this.openmct.objects.makeKeyString(identifier); + this.legend = new LegendModel({ + model: options.model.legend, + plot: this, + openmct: options.openmct + }); + this.series = new SeriesCollection({ + models: options.model.series, + plot: this, + openmct: options.openmct, + palette: options.palette + }); - if (!domainObject.configuration || !domainObject.configuration.filters) { - return; - } - - return domainObject.configuration.filters[keystring]; - } - /** - * Update the domain object with the given value. - */ - updateDomainObject(domainObject) { - this.set('domainObject', domainObject); + if (this.get('domainObject').type === 'telemetry.plot.overlay') { + this.removeMutationListener = this.openmct.objects.observe( + this.get('domainObject'), + '*', + this.updateDomainObject.bind(this) + ); } - /** - * Clean up all objects and remove all listeners. - */ - onDestroy() { - this.xAxis.destroy(); - this.yAxis.destroy(); - this.series.destroy(); - this.legend.destroy(); - if (this.removeMutationListener) { - this.removeMutationListener(); - } + this.yAxis.listenToSeriesCollection(this.series); + this.additionalYAxes.forEach((yAxis) => { + yAxis.listenToSeriesCollection(this.series); + }); + this.legend.listenToSeriesCollection(this.series); + + this.listenTo(this, 'destroy', this.onDestroy, this); + } + /** + * Retrieve the persisted series config for a given identifier. + * @param {import('./PlotSeries').Identifier} identifier + * @returns {import('./PlotSeries').PlotSeriesModelType=} + */ + getPersistedSeriesConfig(identifier) { + const domainObject = this.get('domainObject'); + if (!domainObject.configuration || !domainObject.configuration.series) { + return; } - /** - * Return defaults, which are extracted from the passed in domain - * object. - * @override - * @param {import('./Model').ModelOptions} options - */ - defaultModel(options) { - return { - series: [], - domainObject: options.domainObject, - xAxis: {}, - yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}), - additionalYAxes: _.cloneDeep(options.domainObject.configuration?.additionalYAxes ?? []), - legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {}) - }; + + return domainObject.configuration.series.filter(function (seriesConfig) { + return ( + seriesConfig.identifier.key === identifier.key && + seriesConfig.identifier.namespace === identifier.namespace + ); + })[0]; + } + /** + * Retrieve the persisted filters for a given identifier. + */ + getPersistedFilters(identifier) { + const domainObject = this.get('domainObject'); + const keystring = this.openmct.objects.makeKeyString(identifier); + + if (!domainObject.configuration || !domainObject.configuration.filters) { + return; } + + return domainObject.configuration.filters[keystring]; + } + /** + * Update the domain object with the given value. + */ + updateDomainObject(domainObject) { + this.set('domainObject', domainObject); + } + + /** + * Clean up all objects and remove all listeners. + */ + onDestroy() { + this.xAxis.destroy(); + this.yAxis.destroy(); + this.series.destroy(); + this.legend.destroy(); + if (this.removeMutationListener) { + this.removeMutationListener(); + } + } + /** + * Return defaults, which are extracted from the passed in domain + * object. + * @override + * @param {import('./Model').ModelOptions} options + */ + defaultModel(options) { + return { + series: [], + domainObject: options.domainObject, + xAxis: {}, + yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}), + additionalYAxes: _.cloneDeep(options.domainObject.configuration?.additionalYAxes ?? []), + legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {}) + }; + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/configuration/PlotSeries.js b/src/plugins/plot/configuration/PlotSeries.js index c7e2e14ee7..4f3cd30a9e 100644 --- a/src/plugins/plot/configuration/PlotSeries.js +++ b/src/plugins/plot/configuration/PlotSeries.js @@ -20,9 +20,9 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ import _ from 'lodash'; -import Model from "./Model"; +import Model from './Model'; import { MARKER_SHAPES } from '../draw/MarkerShapes'; -import configStore from "../configuration/ConfigStore"; +import configStore from '../configuration/ConfigStore'; import { symlog } from '../mathUtils'; /** @@ -64,487 +64,475 @@ import { symlog } from '../mathUtils'; * @extends {Model} */ export default class PlotSeries extends Model { - logMode = false; + logMode = false; - /** + /** @param {import('./Model').ModelOptions} options */ - constructor(options) { + constructor(options) { + super(options); - super(options); + this.logMode = this.getLogMode(options); - this.logMode = this.getLogMode(options); + this.listenTo(this, 'change:xKey', this.onXKeyChange, this); + this.listenTo(this, 'change:yKey', this.onYKeyChange, this); + this.persistedConfig = options.persistedConfig; + this.filters = options.filters; - this.listenTo(this, 'change:xKey', this.onXKeyChange, this); - this.listenTo(this, 'change:yKey', this.onYKeyChange, this); - this.persistedConfig = options.persistedConfig; - this.filters = options.filters; + // Model.apply(this, arguments); + this.onXKeyChange(this.get('xKey')); + this.onYKeyChange(this.get('yKey')); - // Model.apply(this, arguments); - this.onXKeyChange(this.get('xKey')); - this.onYKeyChange(this.get('yKey')); + this.unPlottableValues = [undefined, Infinity, -Infinity]; + } - this.unPlottableValues = [undefined, Infinity, -Infinity]; + getLogMode(options) { + const yAxisId = this.get('yAxisId'); + if (yAxisId === 1) { + return options.collection.plot.model.yAxis.logMode; + } else { + const foundYAxis = options.collection.plot.model.additionalYAxes.find( + (yAxis) => yAxis.id === yAxisId + ); + + return foundYAxis ? foundYAxis.logMode : false; + } + } + + /** + * Set defaults for telemetry series. + * @param {import('./Model').ModelOptions} options + * @override + */ + defaultModel(options) { + this.metadata = options.openmct.telemetry.getMetadata(options.domainObject); + + this.formats = options.openmct.telemetry.getFormatMap(this.metadata); + + //if the object is missing or doesn't have metadata for some reason + let range = {}; + if (this.metadata) { + range = this.metadata.valuesForHints(['range'])[0]; } - getLogMode(options) { - const yAxisId = this.get('yAxisId'); - if (yAxisId === 1) { - return options.collection.plot.model.yAxis.logMode; - } else { - const foundYAxis = options.collection.plot.model.additionalYAxes.find(yAxis => yAxis.id === yAxisId); + return { + name: options.domainObject.name, + unit: range.unit, + xKey: options.collection.plot.xAxis.get('key'), + yKey: range.key, + markers: true, + markerShape: 'point', + markerSize: 2.0, + alarmMarkers: true, + limitLines: false, + yAxisId: options.model.yAxisId || 1 + }; + } - return foundYAxis ? foundYAxis.logMode : false; - } + /** + * Remove real-time subscription when destroyed. + * @override + */ + destroy() { + super.destroy(); + this.openmct.time.off('bounds', this.updateLimits); + + if (this.unsubscribe) { + this.unsubscribe(); } - /** - * Set defaults for telemetry series. - * @param {import('./Model').ModelOptions} options - * @override - */ - defaultModel(options) { - this.metadata = options - .openmct - .telemetry - .getMetadata(options.domainObject); + if (this.removeMutationListener) { + this.removeMutationListener(); + } + } - this.formats = options - .openmct - .telemetry - .getFormatMap(this.metadata); + /** + * Set defaults for telemetry series. + * @override + * @param {import('./Model').ModelOptions} options + */ + initialize(options) { + this.openmct = options.openmct; + this.domainObject = options.domainObject; + this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); + this.dataStoreId = `data-${options.collection.plot.id}-${this.keyString}`; + this.updateSeriesData([]); + this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject); + this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject); + this.limits = []; + this.openmct.time.on('bounds', this.updateLimits); + this.removeMutationListener = this.openmct.objects.observe( + this.domainObject, + 'name', + this.updateName.bind(this) + ); + } - //if the object is missing or doesn't have metadata for some reason - let range = {}; - if (this.metadata) { - range = this.metadata.valuesForHints(['range'])[0]; - } + /** + * @param {Bounds} bounds + */ + updateLimits(bounds) { + this.emit('limitBounds', bounds); + } - return { - name: options.domainObject.name, - unit: range.unit, - xKey: options.collection.plot.xAxis.get('key'), - yKey: range.key, - markers: true, - markerShape: 'point', - markerSize: 2.0, - alarmMarkers: true, - limitLines: false, - yAxisId: options.model.yAxisId || 1 - }; + /** + * Fetch historical data and establish a realtime subscription. Returns + * a promise that is resolved when all connections have been successfully + * established. + * + * @returns {Promise} + */ + async fetch(options) { + let strategy; + + if (this.model.interpolate !== 'none') { + strategy = 'minmax'; } - /** - * Remove real-time subscription when destroyed. - * @override - */ - destroy() { - super.destroy(); - this.openmct.time.off('bounds', this.updateLimits); + options = Object.assign( + {}, + { + size: 1000, + strategy, + filters: this.filters + }, + options || {} + ); - if (this.unsubscribe) { - this.unsubscribe(); - } - - if (this.removeMutationListener) { - this.removeMutationListener(); - } + if (!this.unsubscribe) { + this.unsubscribe = this.openmct.telemetry.subscribe(this.domainObject, this.add.bind(this), { + filters: this.filters + }); } - /** - * Set defaults for telemetry series. - * @override - * @param {import('./Model').ModelOptions} options - */ - initialize(options) { - this.openmct = options.openmct; - this.domainObject = options.domainObject; - this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); - this.dataStoreId = `data-${options.collection.plot.id}-${this.keyString}`; - this.updateSeriesData([]); - this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject); - this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject); - this.limits = []; - this.openmct.time.on('bounds', this.updateLimits); - this.removeMutationListener = this.openmct.objects.observe( - this.domainObject, - 'name', - this.updateName.bind(this) - ); + try { + const points = await this.openmct.telemetry.request(this.domainObject, options); + const data = this.getSeriesData(); + // eslint-disable-next-line you-dont-need-lodash-underscore/concat + const newPoints = _(data) + .concat(points) + .sortBy(this.getXVal) + .uniq(true, (point) => [this.getXVal(point), this.getYVal(point)].join()) + .value(); + this.reset(newPoints); + } catch (error) { + console.warn('Error fetching data', error); + } + } + + updateName(name) { + if (name !== this.get('name')) { + this.set('name', name); + } + } + /** + * Update x formatter on x change. + */ + onXKeyChange(xKey) { + const format = this.formats[xKey]; + if (format) { + this.getXVal = format.parse.bind(format); + } + } + + /** + * Update y formatter on change, default to stepAfter interpolation if + * y range is an enumeration. + */ + onYKeyChange(newKey, oldKey) { + if (newKey === oldKey) { + return; } - /** - * @param {Bounds} bounds - */ - updateLimits(bounds) { - this.emit('limitBounds', bounds); + const valueMetadata = this.metadata.value(newKey); + //TODO: Should we do this even if there is a persisted config? + if (!this.persistedConfig || !this.persistedConfig.interpolate) { + if (valueMetadata.format === 'enum') { + this.set('interpolate', 'stepAfter'); + } else { + this.set('interpolate', 'linear'); + } } - /** - * Fetch historical data and establish a realtime subscription. Returns - * a promise that is resolved when all connections have been successfully - * established. - * - * @returns {Promise} - */ - async fetch(options) { - let strategy; + this.evaluate = function (datum) { + return this.limitEvaluator.evaluate(datum, valueMetadata); + }.bind(this); + this.set('unit', valueMetadata.unit); + const format = this.formats[newKey]; + this.getYVal = (value) => { + const y = format.parse(value); - if (this.model.interpolate !== 'none') { - strategy = 'minmax'; - } + return this.logMode ? symlog(y, 10) : y; + }; + } - options = Object.assign({}, { - size: 1000, - strategy, - filters: this.filters - }, options || {}); + formatX(point) { + return this.formats[this.get('xKey')].format(point); + } - if (!this.unsubscribe) { - this.unsubscribe = this.openmct - .telemetry - .subscribe( - this.domainObject, - this.add.bind(this), - { - filters: this.filters - } - ); - } + formatY(point) { + return this.formats[this.get('yKey')].format(point); + } - try { - const points = await this.openmct.telemetry.request(this.domainObject, options); - const data = this.getSeriesData(); - // eslint-disable-next-line you-dont-need-lodash-underscore/concat - const newPoints = _(data) - .concat(points) - .sortBy(this.getXVal) - .uniq(true, point => [this.getXVal(point), this.getYVal(point)].join()) - .value(); - this.reset(newPoints); - } catch (error) { - console.warn('Error fetching data', error); - } + /** + * Clear stats and recalculate from existing data. + */ + resetStats() { + this.unset('stats'); + this.getSeriesData().forEach(this.updateStats, this); + } + + /** + * Reset plot series. If new data is provided, will add that + * data to series after reset. + */ + reset(newData) { + this.updateSeriesData([]); + this.resetStats(); + this.emit('reset'); + if (newData) { + newData.forEach(function (point) { + this.add(point, true); + }, this); + } + } + /** + * Return the point closest to a given x value. + */ + nearestPoint(xValue) { + const insertIndex = this.sortedIndex(xValue); + const data = this.getSeriesData(); + const lowPoint = data[insertIndex - 1]; + const highPoint = data[insertIndex]; + const indexVal = this.getXVal(xValue); + const lowDistance = lowPoint ? indexVal - this.getXVal(lowPoint) : Number.POSITIVE_INFINITY; + const highDistance = highPoint ? this.getXVal(highPoint) - indexVal : Number.POSITIVE_INFINITY; + const nearestPoint = highDistance < lowDistance ? highPoint : lowPoint; + + return nearestPoint; + } + /** + * Override this to implement plot series loading functionality. Must return + * a promise that is resolved when loading is completed. + * + * @returns {Promise} + */ + async load(options) { + await this.fetch(options); + this.emit('load'); + const limitsResponse = await this.limitDefinition.limits(); + this.limits = []; + if (limitsResponse) { + this.limits = limitsResponse; } - updateName(name) { - if (name !== this.get('name')) { - this.set('name', name); - } - } - /** - * Update x formatter on x change. - */ - onXKeyChange(xKey) { - const format = this.formats[xKey]; - if (format) { - this.getXVal = format.parse.bind(format); - } + this.emit('limits', this); + this.emit('change:limitLines', this); + } + + /** + * Find the insert index for a given point to maintain sort order. + * @private + */ + sortedIndex(point) { + return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal); + } + /** + * Update min/max stats for the series. + * @private + */ + updateStats(point) { + const value = this.getYVal(point); + let stats = this.get('stats'); + let changed = false; + if (!stats) { + if ([Infinity, -Infinity].includes(value)) { + return; + } + + stats = { + minValue: value, + minPoint: point, + maxValue: value, + maxPoint: point + }; + changed = true; + } else { + if (stats.maxValue < value && value !== Infinity) { + stats.maxValue = value; + stats.maxPoint = point; + changed = true; + } + + if (stats.minValue > value && value !== -Infinity) { + stats.minValue = value; + stats.minPoint = point; + changed = true; + } } - /** - * Update y formatter on change, default to stepAfter interpolation if - * y range is an enumeration. - */ - onYKeyChange(newKey, oldKey) { - if (newKey === oldKey) { - return; - } + if (changed) { + this.set('stats', { + minValue: stats.minValue, + minPoint: stats.minPoint, + maxValue: stats.maxValue, + maxPoint: stats.maxPoint + }); + } + } - const valueMetadata = this.metadata.value(newKey); - //TODO: Should we do this even if there is a persisted config? - if (!this.persistedConfig || !this.persistedConfig.interpolate) { - if (valueMetadata.format === 'enum') { - this.set('interpolate', 'stepAfter'); - } else { - this.set('interpolate', 'linear'); - } - } + /** + * Add a point to the data array while maintaining the sort order of + * the array and preventing insertion of points with a duplicate x + * value. Can provide an optional argument to append a point without + * maintaining sort order and dupe checks, which improves performance + * when adding an array of points that are already properly sorted. + * + * @private + * @param {Object} point a telemetry datum. + * @param {Boolean} [appendOnly] default false, if true will append + * a point to the end without dupe checking. + */ + add(point, appendOnly) { + let data = this.getSeriesData(); + let insertIndex = data.length; + const currentYVal = this.getYVal(point); + const lastYVal = this.getYVal(data[insertIndex - 1]); - this.evaluate = function (datum) { - return this.limitEvaluator.evaluate(datum, valueMetadata); - }.bind(this); - this.set('unit', valueMetadata.unit); - const format = this.formats[newKey]; - this.getYVal = (value) => { - const y = format.parse(value); + if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) { + console.warn('[Plot] Invalid Y Values detected'); - return this.logMode ? symlog(y, 10) : y; - }; + return; } - formatX(point) { - return this.formats[this.get('xKey')].format(point); + if (!appendOnly) { + insertIndex = this.sortedIndex(point); + if (this.getXVal(data[insertIndex]) === this.getXVal(point)) { + return; + } + + if (this.getXVal(data[insertIndex - 1]) === this.getXVal(point)) { + return; + } } - formatY(point) { - return this.formats[this.get('yKey')].format(point); - } + this.updateStats(point); + point.mctLimitState = this.evaluate(point); + data.splice(insertIndex, 0, point); + this.updateSeriesData(data); + this.emit('add', point, insertIndex, this); + } - /** - * Clear stats and recalculate from existing data. - */ - resetStats() { - this.unset('stats'); - this.getSeriesData().forEach(this.updateStats, this); - } + /** + * + * @private + */ + isValueInvalid(val) { + return Number.isNaN(val) || this.unPlottableValues.includes(val); + } - /** - * Reset plot series. If new data is provided, will add that - * data to series after reset. - */ - reset(newData) { - this.updateSeriesData([]); + /** + * Remove a point from the data array and notify listeners. + * @private + */ + remove(point) { + let data = this.getSeriesData(); + const index = data.indexOf(point); + data.splice(index, 1); + this.updateSeriesData(data); + this.emit('remove', point, index, this); + } + /** + * Purges records outside a given x range. Changes removal method based + * on number of records to remove: for large purge, reset data and + * rebuild array. for small purge, removes points and emits updates. + * + * @public + * @param {Object} range + * @param {number} range.min minimum x value to keep + * @param {number} range.max maximum x value to keep. + */ + purgeRecordsOutsideRange(range) { + const startIndex = this.sortedIndex(range.min); + const endIndex = this.sortedIndex(range.max) + 1; + let data = this.getSeriesData(); + const pointsToRemove = startIndex + (data.length - endIndex + 1); + if (pointsToRemove > 0) { + if (pointsToRemove < 1000) { + data.slice(0, startIndex).forEach(this.remove, this); + data.slice(endIndex, data.length).forEach(this.remove, this); + this.updateSeriesData(data); this.resetStats(); - this.emit('reset'); - if (newData) { - newData.forEach(function (point) { - this.add(point, true); - }, this); - } + } else { + const newData = this.getSeriesData().slice(startIndex, endIndex); + this.reset(newData); + } } - /** - * Return the point closest to a given x value. - */ - nearestPoint(xValue) { - const insertIndex = this.sortedIndex(xValue); - const data = this.getSeriesData(); - const lowPoint = data[insertIndex - 1]; - const highPoint = data[insertIndex]; - const indexVal = this.getXVal(xValue); - const lowDistance = lowPoint - ? indexVal - this.getXVal(lowPoint) - : Number.POSITIVE_INFINITY; - const highDistance = highPoint - ? this.getXVal(highPoint) - indexVal - : Number.POSITIVE_INFINITY; - const nearestPoint = highDistance < lowDistance ? highPoint : lowPoint; - - return nearestPoint; - } - /** - * Override this to implement plot series loading functionality. Must return - * a promise that is resolved when loading is completed. - * - * @returns {Promise} - */ - async load(options) { - await this.fetch(options); - this.emit('load'); - const limitsResponse = await this.limitDefinition.limits(); - this.limits = []; - if (limitsResponse) { - this.limits = limitsResponse; - } - - this.emit('limits', this); - this.emit('change:limitLines', this); + } + /** + * Updates filters, clears the plot series, unsubscribes and resubscribes + * @public + */ + updateFiltersAndRefresh(updatedFilters) { + if (updatedFilters === undefined) { + return; } - /** - * Find the insert index for a given point to maintain sort order. - * @private - */ - sortedIndex(point) { - return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal); + let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); + + if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { + this.filters = deepCopiedFilters; + this.reset(); + if (this.unsubscribe) { + this.unsubscribe(); + delete this.unsubscribe; + } + + this.fetch(); + } else { + this.filters = deepCopiedFilters; } - /** - * Update min/max stats for the series. - * @private - */ - updateStats(point) { - const value = this.getYVal(point); - let stats = this.get('stats'); - let changed = false; - if (!stats) { - if ([Infinity, -Infinity].includes(value)) { - return; - } + } + getDisplayRange(xKey) { + const unsortedData = this.getSeriesData(); + this.updateSeriesData([]); + unsortedData.forEach((point) => this.add(point, false)); - stats = { - minValue: value, - minPoint: point, - maxValue: value, - maxPoint: point - }; - changed = true; - } else { - if (stats.maxValue < value && value !== Infinity) { - stats.maxValue = value; - stats.maxPoint = point; - changed = true; - } + let data = this.getSeriesData(); + const minValue = this.getXVal(data[0]); + const maxValue = this.getXVal(data[data.length - 1]); - if (stats.minValue > value && value !== -Infinity) { - stats.minValue = value; - stats.minPoint = point; - changed = true; - } - } - - if (changed) { - this.set('stats', { - minValue: stats.minValue, - minPoint: stats.minPoint, - maxValue: stats.maxValue, - maxPoint: stats.maxPoint - }); - } + return { + min: minValue, + max: maxValue + }; + } + markerOptionsDisplayText() { + const showMarkers = this.get('markers'); + if (!showMarkers) { + return 'Disabled'; } - /** - * Add a point to the data array while maintaining the sort order of - * the array and preventing insertion of points with a duplicate x - * value. Can provide an optional argument to append a point without - * maintaining sort order and dupe checks, which improves performance - * when adding an array of points that are already properly sorted. - * - * @private - * @param {Object} point a telemetry datum. - * @param {Boolean} [appendOnly] default false, if true will append - * a point to the end without dupe checking. - */ - add(point, appendOnly) { - let data = this.getSeriesData(); - let insertIndex = data.length; - const currentYVal = this.getYVal(point); - const lastYVal = this.getYVal(data[insertIndex - 1]); + const markerShapeKey = this.get('markerShape'); + const markerShape = MARKER_SHAPES[markerShapeKey].label; + const markerSize = this.get('markerSize'); - if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) { - console.warn('[Plot] Invalid Y Values detected'); + return `${markerShape}: ${markerSize}px`; + } + nameWithUnit() { + let unit = this.get('unit'); - return; - } + return this.get('name') + (unit ? ' ' + unit : ''); + } - if (!appendOnly) { - insertIndex = this.sortedIndex(point); - if (this.getXVal(data[insertIndex]) === this.getXVal(point)) { - return; - } + /** + * Update the series data with the given value. + */ + updateSeriesData(data) { + configStore.add(this.dataStoreId, data); + } - if (this.getXVal(data[insertIndex - 1]) === this.getXVal(point)) { - return; - } - } - - this.updateStats(point); - point.mctLimitState = this.evaluate(point); - data.splice(insertIndex, 0, point); - this.updateSeriesData(data); - this.emit('add', point, insertIndex, this); - } - - /** - * - * @private - */ - isValueInvalid(val) { - return Number.isNaN(val) || this.unPlottableValues.includes(val); - } - - /** - * Remove a point from the data array and notify listeners. - * @private - */ - remove(point) { - let data = this.getSeriesData(); - const index = data.indexOf(point); - data.splice(index, 1); - this.updateSeriesData(data); - this.emit('remove', point, index, this); - } - /** - * Purges records outside a given x range. Changes removal method based - * on number of records to remove: for large purge, reset data and - * rebuild array. for small purge, removes points and emits updates. - * - * @public - * @param {Object} range - * @param {number} range.min minimum x value to keep - * @param {number} range.max maximum x value to keep. - */ - purgeRecordsOutsideRange(range) { - const startIndex = this.sortedIndex(range.min); - const endIndex = this.sortedIndex(range.max) + 1; - let data = this.getSeriesData(); - const pointsToRemove = startIndex + (data.length - endIndex + 1); - if (pointsToRemove > 0) { - if (pointsToRemove < 1000) { - data.slice(0, startIndex).forEach(this.remove, this); - data.slice(endIndex, data.length).forEach(this.remove, this); - this.updateSeriesData(data); - this.resetStats(); - } else { - const newData = this.getSeriesData().slice(startIndex, endIndex); - this.reset(newData); - } - } - - } - /** - * Updates filters, clears the plot series, unsubscribes and resubscribes - * @public - */ - updateFiltersAndRefresh(updatedFilters) { - if (updatedFilters === undefined) { - return; - } - - let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); - - if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { - this.filters = deepCopiedFilters; - this.reset(); - if (this.unsubscribe) { - this.unsubscribe(); - delete this.unsubscribe; - } - - this.fetch(); - } else { - this.filters = deepCopiedFilters; - } - } - getDisplayRange(xKey) { - const unsortedData = this.getSeriesData(); - this.updateSeriesData([]); - unsortedData.forEach(point => this.add(point, false)); - - let data = this.getSeriesData(); - const minValue = this.getXVal(data[0]); - const maxValue = this.getXVal(data[data.length - 1]); - - return { - min: minValue, - max: maxValue - }; - } - markerOptionsDisplayText() { - const showMarkers = this.get('markers'); - if (!showMarkers) { - return "Disabled"; - } - - const markerShapeKey = this.get('markerShape'); - const markerShape = MARKER_SHAPES[markerShapeKey].label; - const markerSize = this.get('markerSize'); - - return `${markerShape}: ${markerSize}px`; - } - nameWithUnit() { - let unit = this.get('unit'); - - return this.get('name') + (unit ? ' ' + unit : ''); - } - - /** - * Update the series data with the given value. - */ - updateSeriesData(data) { - configStore.add(this.dataStoreId, data); - } - - /** + /** * Update the series data with the given value. * This return type definition is totally wrong, only covers sinwave generator. It needs to be generic. * @return-example {Array<{ @@ -561,9 +549,9 @@ export default class PlotSeries extends Model { yesterday: number }>} */ - getSeriesData() { - return configStore.get(this.dataStoreId) || []; - } + getSeriesData() { + return configStore.get(this.dataStoreId) || []; + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/configuration/SeriesCollection.js b/src/plugins/plot/configuration/SeriesCollection.js index 9e3c10b668..b57ff51931 100644 --- a/src/plugins/plot/configuration/SeriesCollection.js +++ b/src/plugins/plot/configuration/SeriesCollection.js @@ -21,168 +21,171 @@ *****************************************************************************/ import _ from 'lodash'; -import PlotSeries from "./PlotSeries"; -import Collection from "./Collection"; -import Color from "@/ui/color/Color"; -import ColorPalette from "@/ui/color/ColorPalette"; +import PlotSeries from './PlotSeries'; +import Collection from './Collection'; +import Color from '@/ui/color/Color'; +import ColorPalette from '@/ui/color/ColorPalette'; /** * @extends {Collection} */ export default class SeriesCollection extends Collection { - /** + /** @override @param {import('./Model').ModelOptions} options */ - initialize(options) { - super.initialize(options); - this.modelClass = PlotSeries; - this.plot = options.plot; - this.openmct = options.openmct; - this.palette = options.palette || new ColorPalette(); - this.listenTo(this, 'add', this.onSeriesAdd, this); - this.listenTo(this, 'remove', this.onSeriesRemove, this); - this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this); + initialize(options) { + super.initialize(options); + this.modelClass = PlotSeries; + this.plot = options.plot; + this.openmct = options.openmct; + this.palette = options.palette || new ColorPalette(); + this.listenTo(this, 'add', this.onSeriesAdd, this); + this.listenTo(this, 'remove', this.onSeriesRemove, this); + this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this); - const domainObject = this.plot.get('domainObject'); - if (domainObject.telemetry) { - this.addTelemetryObject(domainObject); - } else { - this.watchTelemetryContainer(domainObject); - } + const domainObject = this.plot.get('domainObject'); + if (domainObject.telemetry) { + this.addTelemetryObject(domainObject); + } else { + this.watchTelemetryContainer(domainObject); } - trackPersistedConfig(domainObject) { - domainObject.configuration.series.forEach(function (seriesConfig) { - const series = this.byIdentifier(seriesConfig.identifier); - if (series) { - series.persistedConfig = seriesConfig; - if (!series.persistedConfig.yAxisId) { - return; - } - - if (series.get('yAxisId') !== series.persistedConfig.yAxisId) { - series.set('yAxisId', series.persistedConfig.yAxisId); - } - } - }, this); - } - watchTelemetryContainer(domainObject) { - if (domainObject.type === 'telemetry.plot.stacked') { - return; + } + trackPersistedConfig(domainObject) { + domainObject.configuration.series.forEach(function (seriesConfig) { + const series = this.byIdentifier(seriesConfig.identifier); + if (series) { + series.persistedConfig = seriesConfig; + if (!series.persistedConfig.yAxisId) { + return; } - const composition = this.openmct.composition.get(domainObject); - this.listenTo(composition, 'add', this.addTelemetryObject, this); - this.listenTo(composition, 'remove', this.removeTelemetryObject, this); - composition.load(); - } - addTelemetryObject(domainObject, index) { - let seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier); - const filters = this.plot.getPersistedFilters(domainObject.identifier); - const plotObject = this.plot.get('domainObject'); - - if (!seriesConfig) { - seriesConfig = { - identifier: domainObject.identifier - }; - - if (plotObject.type === 'telemetry.plot.overlay') { - this.openmct.objects.mutate( - plotObject, - 'configuration.series[' + this.size() + ']', - seriesConfig - ); - seriesConfig = this.plot - .getPersistedSeriesConfig(domainObject.identifier); - } + if (series.get('yAxisId') !== series.persistedConfig.yAxisId) { + series.set('yAxisId', series.persistedConfig.yAxisId); } - - // Clone to prevent accidental mutation by ref. - seriesConfig = JSON.parse(JSON.stringify(seriesConfig)); - - if (!seriesConfig) { - throw "not possible"; - } - - this.add(new PlotSeries({ - model: seriesConfig, - domainObject: domainObject, - openmct: this.openmct, - collection: this, - persistedConfig: this.plot - .getPersistedSeriesConfig(domainObject.identifier), - filters: filters - })); + } + }, this); + } + watchTelemetryContainer(domainObject) { + if (domainObject.type === 'telemetry.plot.stacked') { + return; } - removeTelemetryObject(identifier) { - const plotObject = this.plot.get('domainObject'); - if (plotObject.type === 'telemetry.plot.overlay') { - const persistedIndex = plotObject.configuration.series.findIndex(s => { - return _.isEqual(identifier, s.identifier); - }); + const composition = this.openmct.composition.get(domainObject); + this.listenTo(composition, 'add', this.addTelemetryObject, this); + this.listenTo(composition, 'remove', this.removeTelemetryObject, this); + composition.load(); + } + addTelemetryObject(domainObject, index) { + let seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier); + const filters = this.plot.getPersistedFilters(domainObject.identifier); + const plotObject = this.plot.get('domainObject'); - const configIndex = this.models.findIndex(m => { - return _.isEqual(m.domainObject.identifier, identifier); - }); + if (!seriesConfig) { + seriesConfig = { + identifier: domainObject.identifier + }; - /* + if (plotObject.type === 'telemetry.plot.overlay') { + this.openmct.objects.mutate( + plotObject, + 'configuration.series[' + this.size() + ']', + seriesConfig + ); + seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier); + } + } + + // Clone to prevent accidental mutation by ref. + seriesConfig = JSON.parse(JSON.stringify(seriesConfig)); + + if (!seriesConfig) { + throw 'not possible'; + } + + this.add( + new PlotSeries({ + model: seriesConfig, + domainObject: domainObject, + openmct: this.openmct, + collection: this, + persistedConfig: this.plot.getPersistedSeriesConfig(domainObject.identifier), + filters: filters + }) + ); + } + removeTelemetryObject(identifier) { + const plotObject = this.plot.get('domainObject'); + if (plotObject.type === 'telemetry.plot.overlay') { + const persistedIndex = plotObject.configuration.series.findIndex((s) => { + return _.isEqual(identifier, s.identifier); + }); + + const configIndex = this.models.findIndex((m) => { + return _.isEqual(m.domainObject.identifier, identifier); + }); + + /* when cancelling out of edit mode, the config store and domain object are out of sync thus it is necesarry to check both and remove the models that are no longer in composition */ - if (persistedIndex === -1) { - this.remove(this.at(configIndex)); - } else { - this.remove(this.at(persistedIndex)); - // Because this is triggered by a composition change, we have - // to defer mutation of our plot object, otherwise we might - // mutate an outdated version of the plotObject. - setTimeout(function () { - const newPlotObject = this.plot.get('domainObject'); - const cSeries = newPlotObject.configuration.series.slice(); - cSeries.splice(persistedIndex, 1); - this.openmct.objects.mutate(newPlotObject, 'configuration.series', cSeries); - }.bind(this)); - } - } + if (persistedIndex === -1) { + this.remove(this.at(configIndex)); + } else { + this.remove(this.at(persistedIndex)); + // Because this is triggered by a composition change, we have + // to defer mutation of our plot object, otherwise we might + // mutate an outdated version of the plotObject. + setTimeout( + function () { + const newPlotObject = this.plot.get('domainObject'); + const cSeries = newPlotObject.configuration.series.slice(); + cSeries.splice(persistedIndex, 1); + this.openmct.objects.mutate(newPlotObject, 'configuration.series', cSeries); + }.bind(this) + ); + } } - onSeriesAdd(series) { - let seriesColor = series.get('color'); - if (seriesColor) { - if (!(seriesColor instanceof Color)) { - seriesColor = Color.fromHexString(seriesColor); - series.set('color', seriesColor); - } + } + onSeriesAdd(series) { + let seriesColor = series.get('color'); + if (seriesColor) { + if (!(seriesColor instanceof Color)) { + seriesColor = Color.fromHexString(seriesColor); + series.set('color', seriesColor); + } - this.palette.remove(seriesColor); - } else { - series.set('color', this.palette.getNextColor()); - } + this.palette.remove(seriesColor); + } else { + series.set('color', this.palette.getNextColor()); + } - this.listenTo(series, 'change:color', this.updateColorPalette, this); + this.listenTo(series, 'change:color', this.updateColorPalette, this); + } + onSeriesRemove(series) { + this.palette.return(series.get('color')); + this.stopListening(series); + series.destroy(); + } + updateColorPalette(newColor, oldColor) { + this.palette.remove(newColor); + const seriesWithColor = this.filter(function (series) { + return series.get('color') === newColor; + })[0]; + if (!seriesWithColor) { + this.palette.return(oldColor); } - onSeriesRemove(series) { - this.palette.return(series.get('color')); - this.stopListening(series); - series.destroy(); - } - updateColorPalette(newColor, oldColor) { - this.palette.remove(newColor); - const seriesWithColor = this.filter(function (series) { - return series.get('color') === newColor; - })[0]; - if (!seriesWithColor) { - this.palette.return(oldColor); - } - } - byIdentifier(identifier) { - return this.filter(function (series) { - const seriesIdentifier = series.get('identifier'); + } + byIdentifier(identifier) { + return this.filter(function (series) { + const seriesIdentifier = series.get('identifier'); - return seriesIdentifier.namespace === identifier.namespace - && seriesIdentifier.key === identifier.key; - })[0]; - } + return ( + seriesIdentifier.namespace === identifier.namespace && + seriesIdentifier.key === identifier.key + ); + })[0]; + } } /** diff --git a/src/plugins/plot/configuration/XAxisModel.js b/src/plugins/plot/configuration/XAxisModel.js index 57a23c4923..5c8497cba8 100644 --- a/src/plugins/plot/configuration/XAxisModel.js +++ b/src/plugins/plot/configuration/XAxisModel.js @@ -25,92 +25,92 @@ import Model from './Model'; * @extends {Model} */ export default class XAxisModel extends Model { - // Despite providing template types to the Model class, we still need to - // re-define the type of the following initialize() method's options arg. Tracking - // issue for this: https://github.com/microsoft/TypeScript/issues/32082 - // When they fix it, we can remove the `@param` we have here. - /** - * @override - * @param {import('./Model').ModelOptions} options - */ - initialize(options) { - this.plot = options.plot; + // Despite providing template types to the Model class, we still need to + // re-define the type of the following initialize() method's options arg. Tracking + // issue for this: https://github.com/microsoft/TypeScript/issues/32082 + // When they fix it, we can remove the `@param` we have here. + /** + * @override + * @param {import('./Model').ModelOptions} options + */ + initialize(options) { + this.plot = options.plot; - // This is a type assertion for TypeScript, this error is not thrown in practice. - if (!options.model) { - throw new Error('Not a collection model.'); - } - - this.set('label', options.model.name || ''); - - this.on('change:range', (newValue) => { - if (!this.get('frozen')) { - this.set('displayRange', newValue); - } - }); - - this.on('change:frozen', (frozen) => { - if (!frozen) { - this.set('range', this.get('range')); - } - }); - - if (this.get('range')) { - this.set('range', this.get('range')); - } - - this.listenTo(this, 'change:key', this.changeKey, this); + // This is a type assertion for TypeScript, this error is not thrown in practice. + if (!options.model) { + throw new Error('Not a collection model.'); } - /** - * @param {string} newKey - */ - changeKey(newKey) { - const series = this.plot.series.first(); - if (series) { - const xMetadata = series.metadata.value(newKey); - const xFormat = series.formats[newKey]; - this.set('label', xMetadata.name); - this.set('format', xFormat.format.bind(xFormat)); - } else { - this.set('format', function (x) { - return x; - }); - this.set('label', newKey); - } + this.set('label', options.model.name || ''); - this.plot.series.forEach(function (plotSeries) { - plotSeries.set('xKey', newKey); - }); - } - resetSeries() { - this.plot.series.forEach(function (plotSeries) { - plotSeries.reset(); - }); - } - /** - * @param {import('./Model').ModelOptions} options - * @override - */ - defaultModel(options) { - const bounds = options.openmct.time.bounds(); - const timeSystem = options.openmct.time.timeSystem(); - const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat); + this.on('change:range', (newValue) => { + if (!this.get('frozen')) { + this.set('displayRange', newValue); + } + }); - /** @type {XAxisModelType} */ - const defaultModel = { - name: timeSystem.name, - key: timeSystem.key, - format: format.format.bind(format), - range: { - min: bounds.start, - max: bounds.end - }, - frozen: false - }; + this.on('change:frozen', (frozen) => { + if (!frozen) { + this.set('range', this.get('range')); + } + }); - return defaultModel; + if (this.get('range')) { + this.set('range', this.get('range')); } + + this.listenTo(this, 'change:key', this.changeKey, this); + } + + /** + * @param {string} newKey + */ + changeKey(newKey) { + const series = this.plot.series.first(); + if (series) { + const xMetadata = series.metadata.value(newKey); + const xFormat = series.formats[newKey]; + this.set('label', xMetadata.name); + this.set('format', xFormat.format.bind(xFormat)); + } else { + this.set('format', function (x) { + return x; + }); + this.set('label', newKey); + } + + this.plot.series.forEach(function (plotSeries) { + plotSeries.set('xKey', newKey); + }); + } + resetSeries() { + this.plot.series.forEach(function (plotSeries) { + plotSeries.reset(); + }); + } + /** + * @param {import('./Model').ModelOptions} options + * @override + */ + defaultModel(options) { + const bounds = options.openmct.time.bounds(); + const timeSystem = options.openmct.time.timeSystem(); + const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat); + + /** @type {XAxisModelType} */ + const defaultModel = { + name: timeSystem.name, + key: timeSystem.key, + format: format.format.bind(format), + range: { + min: bounds.start, + max: bounds.end + }, + frozen: false + }; + + return defaultModel; + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/configuration/YAxisModel.js b/src/plugins/plot/configuration/YAxisModel.js index 79feb384ba..eed246f898 100644 --- a/src/plugins/plot/configuration/YAxisModel.js +++ b/src/plugins/plot/configuration/YAxisModel.js @@ -45,332 +45,345 @@ import Model from './Model'; * @extends {Model} */ export default class YAxisModel extends Model { - /** - * @override - * @param {import('./Model').ModelOptions} options - */ - initialize(options) { - this.plot = options.plot; - this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this); - this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this); - this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this); - this.listenTo(this, 'change:logMode', this.onLogModeChange, this); - this.listenTo(this, 'change:frozen', this.toggleFreeze, this); - this.listenTo(this, 'change:range', this.updateDisplayRange, this); - const range = this.get('range'); - this.updateDisplayRange(range); - //This is an edge case and should not happen - const invalidRange = !range || (range?.min === undefined || range?.max === undefined); - const invalidAutoScaleOff = (options.model.autoscale === false) && invalidRange; - if (invalidAutoScaleOff) { - this.set('autoscale', true); - } + /** + * @override + * @param {import('./Model').ModelOptions} options + */ + initialize(options) { + this.plot = options.plot; + this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this); + this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this); + this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this); + this.listenTo(this, 'change:logMode', this.onLogModeChange, this); + this.listenTo(this, 'change:frozen', this.toggleFreeze, this); + this.listenTo(this, 'change:range', this.updateDisplayRange, this); + const range = this.get('range'); + this.updateDisplayRange(range); + //This is an edge case and should not happen + const invalidRange = !range || range?.min === undefined || range?.max === undefined; + const invalidAutoScaleOff = options.model.autoscale === false && invalidRange; + if (invalidAutoScaleOff) { + this.set('autoscale', true); } - /** - * @param {import('./SeriesCollection').default} seriesCollection - */ - listenToSeriesCollection(seriesCollection) { - this.seriesCollection = seriesCollection; - this.listenTo(this.seriesCollection, 'add', series => { - this.trackSeries(series); - this.updateFromSeries(this.seriesCollection); - }, this); - this.listenTo(this.seriesCollection, 'remove', series => { - this.untrackSeries(series); - this.updateFromSeries(this.seriesCollection); - }, this); - this.seriesCollection.forEach(this.trackSeries, this); + } + /** + * @param {import('./SeriesCollection').default} seriesCollection + */ + listenToSeriesCollection(seriesCollection) { + this.seriesCollection = seriesCollection; + this.listenTo( + this.seriesCollection, + 'add', + (series) => { + this.trackSeries(series); this.updateFromSeries(this.seriesCollection); + }, + this + ); + this.listenTo( + this.seriesCollection, + 'remove', + (series) => { + this.untrackSeries(series); + this.updateFromSeries(this.seriesCollection); + }, + this + ); + this.seriesCollection.forEach(this.trackSeries, this); + this.updateFromSeries(this.seriesCollection); + } + toggleFreeze(frozen) { + if (!frozen) { + this.toggleAutoscale(this.get('autoscale')); } - toggleFreeze(frozen) { - if (!frozen) { - this.toggleAutoscale(this.get('autoscale')); - } - } - applyPadding(range) { - let padding = Math.abs(range.max - range.min) * this.get('autoscalePadding'); - if (padding === 0) { - padding = 1; - } - - return { - min: range.min - padding, - max: range.max + padding - }; - } - updatePadding(newPadding) { - if (this.get('autoscale') && !this.get('frozen') && this.has('stats')) { - this.set('displayRange', this.applyPadding(this.get('stats'))); - } - } - calculateAutoscaleExtents(newStats) { - if (this.get('autoscale') && !this.get('frozen')) { - if (!newStats) { - this.unset('displayRange'); - } else { - this.set('displayRange', this.applyPadding(newStats)); - } - } - } - updateStats(seriesStats) { - if (!this.has('stats')) { - this.set('stats', { - min: seriesStats.minValue, - max: seriesStats.maxValue - }); - - return; - } - - const stats = this.get('stats'); - let changed = false; - if (stats.min > seriesStats.minValue) { - changed = true; - stats.min = seriesStats.minValue; - } - - if (stats.max < seriesStats.maxValue) { - changed = true; - stats.max = seriesStats.maxValue; - } - - if (changed) { - this.set('stats', { - min: stats.min, - max: stats.max - }); - } - } - resetStats() { - //TODO: do we need the series id here? - this.unset('stats'); - this.getSeriesForYAxis(this.seriesCollection).forEach(series => { - if (series.has('stats')) { - this.updateStats(series.get('stats')); - } - }); - } - getSeriesForYAxis(seriesCollection) { - return seriesCollection.filter(series => { - const seriesYAxisId = series.get('yAxisId') || 1; - - return seriesYAxisId === this.id; - }); + } + applyPadding(range) { + let padding = Math.abs(range.max - range.min) * this.get('autoscalePadding'); + if (padding === 0) { + padding = 1; } - getYAxisForId(id) { - const plotModel = this.plot.get('domainObject'); - let yAxis; - if (this.id === 1) { - yAxis = plotModel.configuration?.yAxis; - } else { - if (plotModel.configuration?.additionalYAxes) { - yAxis = plotModel.configuration.additionalYAxes.find(additionalYAxis => additionalYAxis.id === id); - } - } - - return yAxis; + return { + min: range.min - padding, + max: range.max + padding + }; + } + updatePadding(newPadding) { + if (this.get('autoscale') && !this.get('frozen') && this.has('stats')) { + this.set('displayRange', this.applyPadding(this.get('stats'))); } - /** - * @param {import('./PlotSeries').default} series - */ - trackSeries(series) { - this.listenTo(series, 'change:stats', seriesStats => { - if (series.get('yAxisId') !== this.id) { - return; - } - - if (!seriesStats) { - this.resetStats(); - } else { - this.updateStats(seriesStats); - } - }); - this.listenTo(series, 'change:yKey', () => { - if (series.get('yAxisId') !== this.id) { - return; - } - - this.updateFromSeries(this.seriesCollection); - }); - - this.listenTo(series, 'change:yAxisId', (newYAxisId, oldYAxisId) => { - if (oldYAxisId && this.id === oldYAxisId) { - this.resetStats(); - this.updateFromSeries(this.seriesCollection); - } - - if (series.get('yAxisId') === this.id) { - this.resetStats(); - this.updateFromSeries(this.seriesCollection); - } - }); + } + calculateAutoscaleExtents(newStats) { + if (this.get('autoscale') && !this.get('frozen')) { + if (!newStats) { + this.unset('displayRange'); + } else { + this.set('displayRange', this.applyPadding(newStats)); + } } - untrackSeries(series) { - this.stopListening(series); + } + updateStats(seriesStats) { + if (!this.has('stats')) { + this.set('stats', { + min: seriesStats.minValue, + max: seriesStats.maxValue + }); + + return; + } + + const stats = this.get('stats'); + let changed = false; + if (stats.min > seriesStats.minValue) { + changed = true; + stats.min = seriesStats.minValue; + } + + if (stats.max < seriesStats.maxValue) { + changed = true; + stats.max = seriesStats.maxValue; + } + + if (changed) { + this.set('stats', { + min: stats.min, + max: stats.max + }); + } + } + resetStats() { + //TODO: do we need the series id here? + this.unset('stats'); + this.getSeriesForYAxis(this.seriesCollection).forEach((series) => { + if (series.has('stats')) { + this.updateStats(series.get('stats')); + } + }); + } + getSeriesForYAxis(seriesCollection) { + return seriesCollection.filter((series) => { + const seriesYAxisId = series.get('yAxisId') || 1; + + return seriesYAxisId === this.id; + }); + } + + getYAxisForId(id) { + const plotModel = this.plot.get('domainObject'); + let yAxis; + if (this.id === 1) { + yAxis = plotModel.configuration?.yAxis; + } else { + if (plotModel.configuration?.additionalYAxes) { + yAxis = plotModel.configuration.additionalYAxes.find( + (additionalYAxis) => additionalYAxis.id === id + ); + } + } + + return yAxis; + } + /** + * @param {import('./PlotSeries').default} series + */ + trackSeries(series) { + this.listenTo(series, 'change:stats', (seriesStats) => { + if (series.get('yAxisId') !== this.id) { + return; + } + + if (!seriesStats) { + this.resetStats(); + } else { + this.updateStats(seriesStats); + } + }); + this.listenTo(series, 'change:yKey', () => { + if (series.get('yAxisId') !== this.id) { + return; + } + + this.updateFromSeries(this.seriesCollection); + }); + + this.listenTo(series, 'change:yAxisId', (newYAxisId, oldYAxisId) => { + if (oldYAxisId && this.id === oldYAxisId) { this.resetStats(); this.updateFromSeries(this.seriesCollection); - } + } - /** - * This is called in order to map the user-provided `range` to the - * `displayRange` that we actually use for plot display. - * - * @param {import('./XAxisModel').NumberRange} range - */ - updateDisplayRange(range) { - if (this.get('autoscale')) { - return; - } - - const _range = { ...range }; - - if (this.get('logMode')) { - _range.min = symlog(range.min, 10); - _range.max = symlog(range.max, 10); - } - - this.set('displayRange', _range); - } - - /** - * @param {boolean} autoscale - */ - toggleAutoscale(autoscale) { - if (autoscale && this.has('stats')) { - this.set('displayRange', this.applyPadding(this.get('stats'))); - - return; - } - - const range = this.get('range'); - - if (range) { - // If we already have a user-defined range, make sure it maps to the - // range we'll actually use for the ticks. - - const _range = { ...range }; - - if (this.get('logMode')) { - _range.min = symlog(range.min, 10); - _range.max = symlog(range.max, 10); - } - - this.set('displayRange', _range); - } - } - - /** @param {boolean} logMode */ - onLogModeChange(logMode) { - const range = this.get('displayRange'); - - if (logMode) { - range.min = symlog(range.min, 10); - range.max = symlog(range.max, 10); - } else { - range.min = antisymlog(range.min, 10); - range.max = antisymlog(range.max, 10); - } - - this.set('displayRange', range); - - this.resetSeries(); - } - resetSeries() { - const series = this.getSeriesForYAxis(this.seriesCollection); - series.forEach((plotSeries) => { - plotSeries.logMode = this.get('logMode'); - plotSeries.reset(plotSeries.getSeriesData()); - }); - // Update the series collection labels and formatting + if (series.get('yAxisId') === this.id) { + this.resetStats(); this.updateFromSeries(this.seriesCollection); + } + }); + } + untrackSeries(series) { + this.stopListening(series); + this.resetStats(); + this.updateFromSeries(this.seriesCollection); + } + + /** + * This is called in order to map the user-provided `range` to the + * `displayRange` that we actually use for plot display. + * + * @param {import('./XAxisModel').NumberRange} range + */ + updateDisplayRange(range) { + if (this.get('autoscale')) { + return; } - /** - * For a given series collection, get the metadata of the current yKey for each series. - * Then return first available value of the given property from the metadata. - * @param {import('./SeriesCollection').default} series - * @param {String} property - */ - getMetadataValueByProperty(series, property) { - return series.map(s => (s.metadata ? s.metadata.value(s.get('yKey'))[property] : '')) - .reduce((a, b) => { - if (a === undefined) { - return b; - } + const _range = { ...range }; - if (a === b) { - return a; - } - - return ''; - }, undefined); + if (this.get('logMode')) { + _range.min = symlog(range.min, 10); + _range.max = symlog(range.max, 10); } - /** - * Update yAxis format, values, and label from known series. - * @param {import('./SeriesCollection').default} seriesCollection - */ - updateFromSeries(seriesCollection) { - const seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection); - if (!seriesForThisYAxis.length) { - return; - } - const yAxis = this.getYAxisForId(this.id); - const label = yAxis?.label; - const sampleSeries = seriesForThisYAxis[0]; - if (!sampleSeries || !sampleSeries.metadata) { - if (!label) { - this.unset('label'); - } + this.set('displayRange', _range); + } - return; - } + /** + * @param {boolean} autoscale + */ + toggleAutoscale(autoscale) { + if (autoscale && this.has('stats')) { + this.set('displayRange', this.applyPadding(this.get('stats'))); - const yKey = sampleSeries.get('yKey'); - const yMetadata = sampleSeries.metadata.value(yKey); - const yFormat = sampleSeries.formats[yKey]; - - if (this.get('logMode')) { - this.set('format', (n) => yFormat.format(antisymlog(n, 10))); - } else { - this.set('format', (n) => yFormat.format(n)); - } - - this.set('values', yMetadata.values); - - if (!label) { - const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name'); - if (labelName) { - this.set('label', labelName); - - return; - } - - //if the name is not available, set the units as the label - const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units'); - if (labelUnits) { - this.set('label', labelUnits); - - return; - } - } + return; } - /** - * @override - * @param {import('./Model').ModelOptions} options - * @returns {Partial} - */ - defaultModel(options) { - return { - frozen: false, - autoscale: true, - logMode: options.model?.logMode ?? false, - autoscalePadding: 0.1, - id: options.id, - range: options.model?.range - }; + + const range = this.get('range'); + + if (range) { + // If we already have a user-defined range, make sure it maps to the + // range we'll actually use for the ticks. + + const _range = { ...range }; + + if (this.get('logMode')) { + _range.min = symlog(range.min, 10); + _range.max = symlog(range.max, 10); + } + + this.set('displayRange', _range); } + } + + /** @param {boolean} logMode */ + onLogModeChange(logMode) { + const range = this.get('displayRange'); + + if (logMode) { + range.min = symlog(range.min, 10); + range.max = symlog(range.max, 10); + } else { + range.min = antisymlog(range.min, 10); + range.max = antisymlog(range.max, 10); + } + + this.set('displayRange', range); + + this.resetSeries(); + } + resetSeries() { + const series = this.getSeriesForYAxis(this.seriesCollection); + series.forEach((plotSeries) => { + plotSeries.logMode = this.get('logMode'); + plotSeries.reset(plotSeries.getSeriesData()); + }); + // Update the series collection labels and formatting + this.updateFromSeries(this.seriesCollection); + } + + /** + * For a given series collection, get the metadata of the current yKey for each series. + * Then return first available value of the given property from the metadata. + * @param {import('./SeriesCollection').default} series + * @param {String} property + */ + getMetadataValueByProperty(series, property) { + return series + .map((s) => (s.metadata ? s.metadata.value(s.get('yKey'))[property] : '')) + .reduce((a, b) => { + if (a === undefined) { + return b; + } + + if (a === b) { + return a; + } + + return ''; + }, undefined); + } + /** + * Update yAxis format, values, and label from known series. + * @param {import('./SeriesCollection').default} seriesCollection + */ + updateFromSeries(seriesCollection) { + const seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection); + if (!seriesForThisYAxis.length) { + return; + } + + const yAxis = this.getYAxisForId(this.id); + const label = yAxis?.label; + const sampleSeries = seriesForThisYAxis[0]; + if (!sampleSeries || !sampleSeries.metadata) { + if (!label) { + this.unset('label'); + } + + return; + } + + const yKey = sampleSeries.get('yKey'); + const yMetadata = sampleSeries.metadata.value(yKey); + const yFormat = sampleSeries.formats[yKey]; + + if (this.get('logMode')) { + this.set('format', (n) => yFormat.format(antisymlog(n, 10))); + } else { + this.set('format', (n) => yFormat.format(n)); + } + + this.set('values', yMetadata.values); + + if (!label) { + const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name'); + if (labelName) { + this.set('label', labelName); + + return; + } + + //if the name is not available, set the units as the label + const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units'); + if (labelUnits) { + this.set('label', labelUnits); + + return; + } + } + } + /** + * @override + * @param {import('./Model').ModelOptions} options + * @returns {Partial} + */ + defaultModel(options) { + return { + frozen: false, + autoscale: true, + logMode: options.model?.logMode ?? false, + autoscalePadding: 0.1, + id: options.id, + range: options.model?.range + }; + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/draw/Draw2D.js b/src/plugins/plot/draw/Draw2D.js index cb93cb2367..e7bf7ed501 100644 --- a/src/plugins/plot/draw/Draw2D.js +++ b/src/plugins/plot/draw/Draw2D.js @@ -24,12 +24,12 @@ import EventEmitter from 'EventEmitter'; import eventHelpers from '../lib/eventHelpers'; import { MARKER_SHAPES } from './MarkerShapes'; /** - * Create a new draw API utilizing the Canvas's 2D API for rendering. - * - * @constructor - * @param {CanvasElement} canvas the canvas object to render upon - * @throws {Error} an error is thrown if Canvas's 2D API is unavailab - */ + * Create a new draw API utilizing the Canvas's 2D API for rendering. + * + * @constructor + * @param {CanvasElement} canvas the canvas object to render upon + * @throws {Error} an error is thrown if Canvas's 2D API is unavailab + */ /** * Create a new draw API utilizing the Canvas's 2D API for rendering. @@ -39,16 +39,16 @@ import { MARKER_SHAPES } from './MarkerShapes'; * @throws {Error} an error is thrown if Canvas's 2D API is unavailab */ function Draw2D(canvas) { - this.canvas = canvas; - this.c2d = canvas.getContext('2d'); - this.width = canvas.width; - this.height = canvas.height; - this.dimensions = [this.width, this.height]; - this.origin = [0, 0]; + this.canvas = canvas; + this.c2d = canvas.getContext('2d'); + this.width = canvas.width; + this.height = canvas.height; + this.dimensions = [this.width, this.height]; + this.origin = [0, 0]; - if (!this.c2d) { - throw new Error("Canvas 2d API unavailable."); - } + if (!this.c2d) { + throw new Error('Canvas 2d API unavailable.'); + } } Object.assign(Draw2D.prototype, EventEmitter.prototype); @@ -56,108 +56,95 @@ eventHelpers.extend(Draw2D.prototype); // Convert from logical to physical x coordinates Draw2D.prototype.x = function (v) { - return ((v - this.origin[0]) / this.dimensions[0]) * this.width; + return ((v - this.origin[0]) / this.dimensions[0]) * this.width; }; // Convert from logical to physical y coordinates Draw2D.prototype.y = function (v) { - return this.height - - ((v - this.origin[1]) / this.dimensions[1]) * this.height; + return this.height - ((v - this.origin[1]) / this.dimensions[1]) * this.height; }; // Set the color to be used for drawing operations Draw2D.prototype.setColor = function (color) { - const mappedColor = color.map(function (c, i) { - return i < 3 ? Math.floor(c * 255) : (c); - }).join(','); - this.c2d.strokeStyle = "rgba(" + mappedColor + ")"; - this.c2d.fillStyle = "rgba(" + mappedColor + ")"; + const mappedColor = color + .map(function (c, i) { + return i < 3 ? Math.floor(c * 255) : c; + }) + .join(','); + this.c2d.strokeStyle = 'rgba(' + mappedColor + ')'; + this.c2d.fillStyle = 'rgba(' + mappedColor + ')'; }; Draw2D.prototype.clear = function () { - this.width = this.canvas.width = this.canvas.offsetWidth; - this.height = this.canvas.height = this.canvas.offsetHeight; - this.c2d.clearRect(0, 0, this.width, this.height); + this.width = this.canvas.width = this.canvas.offsetWidth; + this.height = this.canvas.height = this.canvas.offsetHeight; + this.c2d.clearRect(0, 0, this.width, this.height); }; Draw2D.prototype.setDimensions = function (newDimensions, newOrigin) { - this.dimensions = newDimensions; - this.origin = newOrigin; + this.dimensions = newDimensions; + this.origin = newOrigin; }; Draw2D.prototype.drawLine = function (buf, color, points) { - let i; + let i; - this.setColor(color); + this.setColor(color); - // Configure context to draw two-pixel-thick lines - this.c2d.lineWidth = 1; + // Configure context to draw two-pixel-thick lines + this.c2d.lineWidth = 1; - // Start a new path... - if (buf.length > 1) { - this.c2d.beginPath(); - this.c2d.moveTo(this.x(buf[0]), this.y(buf[1])); - } + // Start a new path... + if (buf.length > 1) { + this.c2d.beginPath(); + this.c2d.moveTo(this.x(buf[0]), this.y(buf[1])); + } - // ...and add points to it... - for (i = 2; i < points * 2; i = i + 2) { - this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1])); - } + // ...and add points to it... + for (i = 2; i < points * 2; i = i + 2) { + this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1])); + } - // ...before finally drawing it. - this.c2d.stroke(); + // ...before finally drawing it. + this.c2d.stroke(); }; Draw2D.prototype.drawSquare = function (min, max, color) { - const x1 = this.x(min[0]); - const y1 = this.y(min[1]); - const w = this.x(max[0]) - x1; - const h = this.y(max[1]) - y1; + const x1 = this.x(min[0]); + const y1 = this.y(min[1]); + const w = this.x(max[0]) - x1; + const h = this.y(max[1]) - y1; - this.setColor(color); - this.c2d.fillRect(x1, y1, w, h); + this.setColor(color); + this.c2d.fillRect(x1, y1, w, h); }; -Draw2D.prototype.drawPoints = function ( - buf, - color, - points, - pointSize, - shape -) { - const drawC2DShape = MARKER_SHAPES[shape].drawC2D.bind(this); +Draw2D.prototype.drawPoints = function (buf, color, points, pointSize, shape) { + const drawC2DShape = MARKER_SHAPES[shape].drawC2D.bind(this); - this.setColor(color); + this.setColor(color); - for (let i = 0; i < points; i++) { - drawC2DShape( - this.x(buf[i * 2]), - this.y(buf[i * 2 + 1]), - pointSize - ); - } + for (let i = 0; i < points; i++) { + drawC2DShape(this.x(buf[i * 2]), this.y(buf[i * 2 + 1]), pointSize); + } }; Draw2D.prototype.drawLimitPoint = function (x, y, size) { - this.c2d.fillRect(x + size, y, size, size); - this.c2d.fillRect(x, y + size, size, size); - this.c2d.fillRect(x - size, y, size, size); - this.c2d.fillRect(x, y - size, size, size); + this.c2d.fillRect(x + size, y, size, size); + this.c2d.fillRect(x, y + size, size, size); + this.c2d.fillRect(x - size, y, size, size); + this.c2d.fillRect(x, y - size, size, size); }; Draw2D.prototype.drawLimitPoints = function (points, color, pointSize) { - const limitSize = pointSize * 2; - const offset = limitSize / 2; + const limitSize = pointSize * 2; + const offset = limitSize / 2; - this.setColor(color); + this.setColor(color); - for (let i = 0; i < points.length; i++) { - this.drawLimitPoint( - this.x(points[i].x) - offset, - this.y(points[i].y) - offset, - limitSize - ); - } + for (let i = 0; i < points.length; i++) { + this.drawLimitPoint(this.x(points[i].x) - offset, this.y(points[i].y) - offset, limitSize); + } }; export default Draw2D; diff --git a/src/plugins/plot/draw/DrawLoader.js b/src/plugins/plot/draw/DrawLoader.js index 7b2ad388bc..d40a5e7c63 100644 --- a/src/plugins/plot/draw/DrawLoader.js +++ b/src/plugins/plot/draw/DrawLoader.js @@ -24,79 +24,75 @@ import DrawWebGL from './DrawWebGL'; import Draw2D from './Draw2D'; const CHARTS = [ - { - MAX_INSTANCES: 16, - API: DrawWebGL, - ALLOCATIONS: [] - }, - { - MAX_INSTANCES: Number.POSITIVE_INFINITY, - API: Draw2D, - ALLOCATIONS: [] - } + { + MAX_INSTANCES: 16, + API: DrawWebGL, + ALLOCATIONS: [] + }, + { + MAX_INSTANCES: Number.POSITIVE_INFINITY, + API: Draw2D, + ALLOCATIONS: [] + } ]; /** - * Draw loader attaches a draw API to a canvas element and returns the - * draw API. - */ + * Draw loader attaches a draw API to a canvas element and returns the + * draw API. + */ export const DrawLoader = { - /** + /** * Return the first draw API available. Returns * `undefined` if a draw API could not be constructed. *. * @param {CanvasElement} canvas - The canvas eelement to attach the draw API to. */ - getDrawAPI: function (canvas, overlay) { - let api; + getDrawAPI: function (canvas, overlay) { + let api; - CHARTS.forEach(function (CHART_TYPE) { - if (api) { - return; - } + CHARTS.forEach(function (CHART_TYPE) { + if (api) { + return; + } - if (CHART_TYPE.ALLOCATIONS.length - >= CHART_TYPE.MAX_INSTANCES) { - return; - } + if (CHART_TYPE.ALLOCATIONS.length >= CHART_TYPE.MAX_INSTANCES) { + return; + } - try { - api = new CHART_TYPE.API(canvas, overlay); - CHART_TYPE.ALLOCATIONS.push(api); - } catch (e) { - console.warn([ - "Could not instantiate chart", - CHART_TYPE.API.name, - ";", - e.message - ].join(" ")); - } - }); + try { + api = new CHART_TYPE.API(canvas, overlay); + CHART_TYPE.ALLOCATIONS.push(api); + } catch (e) { + console.warn( + ['Could not instantiate chart', CHART_TYPE.API.name, ';', e.message].join(' ') + ); + } + }); - if (!api) { - console.warn("Cannot initialize mct-chart."); - } - - return api; - }, - /** - * Returns a fallback draw api. - */ - getFallbackDrawAPI: function (canvas, overlay) { - const api = new CHARTS[1].API(canvas, overlay); - CHARTS[1].ALLOCATIONS.push(api); - - return api; - }, - releaseDrawAPI: function (api) { - CHARTS.forEach(function (CHART_TYPE) { - if (api instanceof CHART_TYPE.API) { - CHART_TYPE.ALLOCATIONS.splice(CHART_TYPE.ALLOCATIONS.indexOf(api), 1); - } - }); - if (api.destroy) { - api.destroy(); - } + if (!api) { + console.warn('Cannot initialize mct-chart.'); } + + return api; + }, + /** + * Returns a fallback draw api. + */ + getFallbackDrawAPI: function (canvas, overlay) { + const api = new CHARTS[1].API(canvas, overlay); + CHARTS[1].ALLOCATIONS.push(api); + + return api; + }, + releaseDrawAPI: function (api) { + CHARTS.forEach(function (CHART_TYPE) { + if (api instanceof CHART_TYPE.API) { + CHART_TYPE.ALLOCATIONS.splice(CHART_TYPE.ALLOCATIONS.indexOf(api), 1); + } + }); + if (api.destroy) { + api.destroy(); + } + } }; diff --git a/src/plugins/plot/draw/DrawWebGL.js b/src/plugins/plot/draw/DrawWebGL.js index db9fcd70b5..be3a93b862 100644 --- a/src/plugins/plot/draw/DrawWebGL.js +++ b/src/plugins/plot/draw/DrawWebGL.js @@ -83,124 +83,118 @@ const VERTEX_SHADER = ` * @throws {Error} an error is thrown if WebGL is unavailable. */ function DrawWebGL(canvas, overlay) { - this.canvas = canvas; - this.gl = this.canvas.getContext("webgl", { preserveDrawingBuffer: true }) - || this.canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true }); + this.canvas = canvas; + this.gl = + this.canvas.getContext('webgl', { preserveDrawingBuffer: true }) || + this.canvas.getContext('experimental-webgl', { preserveDrawingBuffer: true }); - this.overlay = overlay; - this.c2d = overlay.getContext('2d'); - if (!this.c2d) { - throw new Error("No canvas 2d!"); - } + this.overlay = overlay; + this.c2d = overlay.getContext('2d'); + if (!this.c2d) { + throw new Error('No canvas 2d!'); + } - // Ensure a context was actually available before proceeding - if (!this.gl) { - throw new Error("WebGL unavailable."); - } + // Ensure a context was actually available before proceeding + if (!this.gl) { + throw new Error('WebGL unavailable.'); + } - this.initContext(); + this.initContext(); - this.listenTo(this.canvas, "webglcontextlost", this.onContextLost, this); + this.listenTo(this.canvas, 'webglcontextlost', this.onContextLost, this); } Object.assign(DrawWebGL.prototype, EventEmitter.prototype); eventHelpers.extend(DrawWebGL.prototype); DrawWebGL.prototype.onContextLost = function (event) { - this.emit('error'); - this.isContextLost = true; - this.destroy(); - // TODO re-initialize and re-draw on context restored + this.emit('error'); + this.isContextLost = true; + this.destroy(); + // TODO re-initialize and re-draw on context restored }; DrawWebGL.prototype.initContext = function () { - // Initialize shaders - this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER); - this.gl.shaderSource(this.vertexShader, VERTEX_SHADER); - this.gl.compileShader(this.vertexShader); + // Initialize shaders + this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER); + this.gl.shaderSource(this.vertexShader, VERTEX_SHADER); + this.gl.compileShader(this.vertexShader); - this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER); - this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER); - this.gl.compileShader(this.fragmentShader); + this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER); + this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER); + this.gl.compileShader(this.fragmentShader); - // Assemble vertex/fragment shaders into programs - this.program = this.gl.createProgram(); - this.gl.attachShader(this.program, this.vertexShader); - this.gl.attachShader(this.program, this.fragmentShader); - this.gl.linkProgram(this.program); - this.gl.useProgram(this.program); + // Assemble vertex/fragment shaders into programs + this.program = this.gl.createProgram(); + this.gl.attachShader(this.program, this.vertexShader); + this.gl.attachShader(this.program, this.fragmentShader); + this.gl.linkProgram(this.program); + this.gl.useProgram(this.program); - // Get locations for attribs/uniforms from the - // shader programs (to pass values into shaders at draw-time) - this.aVertexPosition = this.gl.getAttribLocation(this.program, "aVertexPosition"); - this.uColor = this.gl.getUniformLocation(this.program, "uColor"); - this.uMarkerShape = this.gl.getUniformLocation(this.program, "uMarkerShape"); - this.uDimensions = this.gl.getUniformLocation(this.program, "uDimensions"); - this.uOrigin = this.gl.getUniformLocation(this.program, "uOrigin"); - this.uPointSize = this.gl.getUniformLocation(this.program, "uPointSize"); + // Get locations for attribs/uniforms from the + // shader programs (to pass values into shaders at draw-time) + this.aVertexPosition = this.gl.getAttribLocation(this.program, 'aVertexPosition'); + this.uColor = this.gl.getUniformLocation(this.program, 'uColor'); + this.uMarkerShape = this.gl.getUniformLocation(this.program, 'uMarkerShape'); + this.uDimensions = this.gl.getUniformLocation(this.program, 'uDimensions'); + this.uOrigin = this.gl.getUniformLocation(this.program, 'uOrigin'); + this.uPointSize = this.gl.getUniformLocation(this.program, 'uPointSize'); - this.gl.enableVertexAttribArray(this.aVertexPosition); + this.gl.enableVertexAttribArray(this.aVertexPosition); - // Create a buffer to holds points which will be drawn - this.buffer = this.gl.createBuffer(); - - // Enable blending, for smoothness - this.gl.enable(this.gl.BLEND); - this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); + // Create a buffer to holds points which will be drawn + this.buffer = this.gl.createBuffer(); + // Enable blending, for smoothness + this.gl.enable(this.gl.BLEND); + this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); }; DrawWebGL.prototype.destroy = function () { - this.stopListening(); + this.stopListening(); }; // Convert from logical to physical x coordinates DrawWebGL.prototype.x = function (v) { - return ((v - this.origin[0]) / this.dimensions[0]) * this.width; + return ((v - this.origin[0]) / this.dimensions[0]) * this.width; }; // Convert from logical to physical y coordinates DrawWebGL.prototype.y = function (v) { - return this.height - - ((v - this.origin[1]) / this.dimensions[1]) * this.height; + return this.height - ((v - this.origin[1]) / this.dimensions[1]) * this.height; }; DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) { - if (this.isContextLost) { - return; - } + if (this.isContextLost) { + return; + } - const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0; + const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0; - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer); - this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW); - this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0); - this.gl.uniform4fv(this.uColor, color); - this.gl.uniform1i(this.uMarkerShape, shapeCode); - if (points !== 0) { - this.gl.drawArrays(drawType, 0, points); - } + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer); + this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW); + this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0); + this.gl.uniform4fv(this.uColor, color); + this.gl.uniform1i(this.uMarkerShape, shapeCode); + if (points !== 0) { + this.gl.drawArrays(drawType, 0, points); + } }; DrawWebGL.prototype.clear = function () { - if (this.isContextLost) { - return; - } + if (this.isContextLost) { + return; + } - this.height = this.canvas.height = this.canvas.offsetHeight; - this.width = this.canvas.width = this.canvas.offsetWidth; - this.overlay.height = this.overlay.offsetHeight; - this.overlay.width = this.overlay.offsetWidth; - // Set the viewport size; note that we use the width/height - // that our WebGL context reports, which may be lower - // resolution than the canvas we requested. - this.gl.viewport( - 0, - 0, - this.gl.drawingBufferWidth, - this.gl.drawingBufferHeight - ); - this.gl.clear(this.gl.COLOR_BUFFER_BIT + this.gl.DEPTH_BUFFER_BIT); + this.height = this.canvas.height = this.canvas.offsetHeight; + this.width = this.canvas.width = this.canvas.offsetWidth; + this.overlay.height = this.overlay.offsetHeight; + this.overlay.width = this.overlay.offsetWidth; + // Set the viewport size; note that we use the width/height + // that our WebGL context reports, which may be lower + // resolution than the canvas we requested. + this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight); + this.gl.clear(this.gl.COLOR_BUFFER_BIT + this.gl.DEPTH_BUFFER_BIT); }; /** @@ -211,17 +205,16 @@ DrawWebGL.prototype.clear = function () { * origin of the chart */ DrawWebGL.prototype.setDimensions = function (dimensions, origin) { - this.dimensions = dimensions; - this.origin = origin; - if (this.isContextLost) { - return; - } + this.dimensions = dimensions; + this.origin = origin; + if (this.isContextLost) { + return; + } - if (dimensions && dimensions.length > 0 - && origin && origin.length > 0) { - this.gl.uniform2fv(this.uDimensions, dimensions); - this.gl.uniform2fv(this.uOrigin, origin); - } + if (dimensions && dimensions.length > 0 && origin && origin.length > 0) { + this.gl.uniform2fv(this.uDimensions, dimensions); + this.gl.uniform2fv(this.uOrigin, origin); + } }; /** @@ -235,11 +228,11 @@ DrawWebGL.prototype.setDimensions = function (dimensions, origin) { * @param {number} points the number of points to draw */ DrawWebGL.prototype.drawLine = function (buf, color, points) { - if (this.isContextLost) { - return; - } + if (this.isContextLost) { + return; + } - this.doDraw(this.gl.LINE_STRIP, buf, color, points); + this.doDraw(this.gl.LINE_STRIP, buf, color, points); }; /** @@ -247,12 +240,12 @@ DrawWebGL.prototype.drawLine = function (buf, color, points) { * */ DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize, shape) { - if (this.isContextLost) { - return; - } + if (this.isContextLost) { + return; + } - this.gl.uniform1f(this.uPointSize, pointSize); - this.doDraw(this.gl.POINTS, buf, color, points, shape); + this.gl.uniform1f(this.uPointSize, pointSize); + this.doDraw(this.gl.POINTS, buf, color, points, shape); }; /** @@ -265,39 +258,40 @@ DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize, shape) * is in the range of 0.0-1.0 */ DrawWebGL.prototype.drawSquare = function (min, max, color) { - if (this.isContextLost) { - return; - } + if (this.isContextLost) { + return; + } - this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array( - min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]]) - ), color, 4); + this.doDraw( + this.gl.TRIANGLE_FAN, + new Float32Array(min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])), + color, + 4 + ); }; DrawWebGL.prototype.drawLimitPoint = function (x, y, size) { - this.c2d.fillRect(x + size, y, size, size); - this.c2d.fillRect(x, y + size, size, size); - this.c2d.fillRect(x - size, y, size, size); - this.c2d.fillRect(x, y - size, size, size); + this.c2d.fillRect(x + size, y, size, size); + this.c2d.fillRect(x, y + size, size, size); + this.c2d.fillRect(x - size, y, size, size); + this.c2d.fillRect(x, y - size, size, size); }; DrawWebGL.prototype.drawLimitPoints = function (points, color, pointSize) { - const limitSize = pointSize * 2; - const offset = limitSize / 2; + const limitSize = pointSize * 2; + const offset = limitSize / 2; - const mappedColor = color.map(function (c, i) { - return i < 3 ? Math.floor(c * 255) : (c); - }).join(','); - this.c2d.strokeStyle = "rgba(" + mappedColor + ")"; - this.c2d.fillStyle = "rgba(" + mappedColor + ")"; + const mappedColor = color + .map(function (c, i) { + return i < 3 ? Math.floor(c * 255) : c; + }) + .join(','); + this.c2d.strokeStyle = 'rgba(' + mappedColor + ')'; + this.c2d.fillStyle = 'rgba(' + mappedColor + ')'; - for (let i = 0; i < points.length; i++) { - this.drawLimitPoint( - this.x(points[i].x) - offset, - this.y(points[i].y) - offset, - limitSize - ); - } + for (let i = 0; i < points.length; i++) { + this.drawLimitPoint(this.x(points[i].x) - offset, this.y(points[i].y) - offset, limitSize); + } }; export default DrawWebGL; diff --git a/src/plugins/plot/draw/MarkerShapes.js b/src/plugins/plot/draw/MarkerShapes.js index b9edd31f24..532da9efaa 100644 --- a/src/plugins/plot/draw/MarkerShapes.js +++ b/src/plugins/plot/draw/MarkerShapes.js @@ -21,66 +21,66 @@ *****************************************************************************/ /** - * @label string (required) display name of shape - * @drawWebGL integer (unique, required) index provided to WebGL Fragment Shader - * @drawC2D function (required) canvas2d draw function - */ + * @label string (required) display name of shape + * @drawWebGL integer (unique, required) index provided to WebGL Fragment Shader + * @drawC2D function (required) canvas2d draw function + */ export const MARKER_SHAPES = { - point: { - label: 'Point', - drawWebGL: 1, - drawC2D: function (x, y, size) { - const offset = size / 2; + point: { + label: 'Point', + drawWebGL: 1, + drawC2D: function (x, y, size) { + const offset = size / 2; - this.c2d.fillRect(x - offset, y - offset, size, size); - } - }, - circle: { - label: 'Circle', - drawWebGL: 2, - drawC2D: function (x, y, size) { - const radius = size / 2; - - this.c2d.beginPath(); - this.c2d.arc(x, y, radius, 0, 2 * Math.PI, false); - this.c2d.closePath(); - this.c2d.fill(); - } - }, - diamond: { - label: 'Diamond', - drawWebGL: 3, - drawC2D: function (x, y, size) { - const offset = size / 2; - const top = [x, y + offset]; - const right = [x + offset, y]; - const bottom = [x, y - offset]; - const left = [x - offset, y]; - - this.c2d.beginPath(); - this.c2d.moveTo(...top); - this.c2d.lineTo(...right); - this.c2d.lineTo(...bottom); - this.c2d.lineTo(...left); - this.c2d.closePath(); - this.c2d.fill(); - } - }, - triangle: { - label: 'Triangle', - drawWebGL: 4, - drawC2D: function (x, y, size) { - const offset = size / 2; - const v1 = [x, y - offset]; - const v2 = [x - offset, y + offset]; - const v3 = [x + offset, y + offset]; - - this.c2d.beginPath(); - this.c2d.moveTo(...v1); - this.c2d.lineTo(...v2); - this.c2d.lineTo(...v3); - this.c2d.closePath(); - this.c2d.fill(); - } + this.c2d.fillRect(x - offset, y - offset, size, size); } + }, + circle: { + label: 'Circle', + drawWebGL: 2, + drawC2D: function (x, y, size) { + const radius = size / 2; + + this.c2d.beginPath(); + this.c2d.arc(x, y, radius, 0, 2 * Math.PI, false); + this.c2d.closePath(); + this.c2d.fill(); + } + }, + diamond: { + label: 'Diamond', + drawWebGL: 3, + drawC2D: function (x, y, size) { + const offset = size / 2; + const top = [x, y + offset]; + const right = [x + offset, y]; + const bottom = [x, y - offset]; + const left = [x - offset, y]; + + this.c2d.beginPath(); + this.c2d.moveTo(...top); + this.c2d.lineTo(...right); + this.c2d.lineTo(...bottom); + this.c2d.lineTo(...left); + this.c2d.closePath(); + this.c2d.fill(); + } + }, + triangle: { + label: 'Triangle', + drawWebGL: 4, + drawC2D: function (x, y, size) { + const offset = size / 2; + const v1 = [x, y - offset]; + const v2 = [x - offset, y + offset]; + const v3 = [x + offset, y + offset]; + + this.c2d.beginPath(); + this.c2d.moveTo(...v1); + this.c2d.lineTo(...v2); + this.c2d.lineTo(...v3); + this.c2d.closePath(); + this.c2d.fill(); + } + } }; diff --git a/src/plugins/plot/inspector/PlotOptions.vue b/src/plugins/plot/inspector/PlotOptions.vue index a4bbcccebf..5cf131dee9 100644 --- a/src/plugins/plot/inspector/PlotOptions.vue +++ b/src/plugins/plot/inspector/PlotOptions.vue @@ -20,45 +20,45 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/PlotOptionsBrowse.vue b/src/plugins/plot/inspector/PlotOptionsBrowse.vue index 75c07e5311..2ef301e256 100644 --- a/src/plugins/plot/inspector/PlotOptionsBrowse.vue +++ b/src/plugins/plot/inspector/PlotOptionsBrowse.vue @@ -20,290 +20,255 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/PlotOptionsEdit.vue b/src/plugins/plot/inspector/PlotOptionsEdit.vue index 76524a41e3..83d16391ba 100644 --- a/src/plugins/plot/inspector/PlotOptionsEdit.vue +++ b/src/plugins/plot/inspector/PlotOptionsEdit.vue @@ -20,218 +20,201 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/PlotOptionsItem.vue b/src/plugins/plot/inspector/PlotOptionsItem.vue index c290cf5b40..37da490c96 100644 --- a/src/plugins/plot/inspector/PlotOptionsItem.vue +++ b/src/plugins/plot/inspector/PlotOptionsItem.vue @@ -20,182 +20,168 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/PlotsInspectorViewProvider.js b/src/plugins/plot/inspector/PlotsInspectorViewProvider.js index 0f39c36d9d..82f0bca377 100644 --- a/src/plugins/plot/inspector/PlotsInspectorViewProvider.js +++ b/src/plugins/plot/inspector/PlotsInspectorViewProvider.js @@ -1,59 +1,58 @@ - -import PlotOptions from "./PlotOptions.vue"; +import PlotOptions from './PlotOptions.vue'; import Vue from 'vue'; export default function PlotsInspectorViewProvider(openmct) { - return { - key: 'plots-inspector', - name: 'Config', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: 'plots-inspector', + name: 'Config', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - let object = selection[0][0].context.item; - let parent = selection[0].length > 1 && selection[0][1].context.item; + let object = selection[0][0].context.item; + let parent = selection[0].length > 1 && selection[0][1].context.item; - const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay'; - const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked'; + const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay'; + const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked'; - return isOverlayPlotObject || isParentStackedPlotObject; + return isOverlayPlotObject || isParentStackedPlotObject; + }, + view: function (selection) { + let component; + let objectPath; + + if (selection.length) { + objectPath = selection[0].map((selectionItem) => { + return selectionItem.context.item; + }); + } + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + PlotOptions: PlotOptions + }, + provide: { + openmct, + domainObject: selection[0][0].context.item, + path: objectPath + }, + template: '' + }); }, - view: function (selection) { - let component; - let objectPath; - - if (selection.length) { - objectPath = selection[0].map((selectionItem) => { - return selectionItem.context.item; - }); - } - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - PlotOptions: PlotOptions - }, - provide: { - openmct, - domainObject: selection[0][0].context.item, - path: objectPath - }, - template: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js b/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js index 249bb56c6a..26a4306c28 100644 --- a/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js +++ b/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js @@ -1,57 +1,56 @@ - -import PlotOptions from "./PlotOptions.vue"; +import PlotOptions from './PlotOptions.vue'; import Vue from 'vue'; export default function StackedPlotsInspectorViewProvider(openmct) { - return { - key: 'stacked-plots-inspector', - name: 'Config', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: 'stacked-plots-inspector', + name: 'Config', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - const object = selection[0][0].context.item; + const object = selection[0][0].context.item; - const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked'; + const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked'; - return isStackedPlotObject; + return isStackedPlotObject; + }, + view: function (selection) { + let component; + let objectPath; + + if (selection.length) { + objectPath = selection[0].map((selectionItem) => { + return selectionItem.context.item; + }); + } + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + PlotOptions: PlotOptions + }, + provide: { + openmct, + domainObject: selection[0][0].context.item, + path: objectPath + }, + template: '' + }); }, - view: function (selection) { - let component; - let objectPath; - - if (selection.length) { - objectPath = selection[0].map((selectionItem) => { - return selectionItem.context.item; - }); - } - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - PlotOptions: PlotOptions - }, - provide: { - openmct, - domainObject: selection[0][0].context.item, - path: objectPath - }, - template: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/plot/inspector/forms/LegendForm.vue b/src/plugins/plot/inspector/forms/LegendForm.vue index f29d8ebf81..5c72b1a382 100644 --- a/src/plugins/plot/inspector/forms/LegendForm.vue +++ b/src/plugins/plot/inspector/forms/LegendForm.vue @@ -20,223 +20,229 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/forms/SeriesForm.vue b/src/plugins/plot/inspector/forms/SeriesForm.vue index 5dc637c8c0..39f7e8e10a 100644 --- a/src/plugins/plot/inspector/forms/SeriesForm.vue +++ b/src/plugins/plot/inspector/forms/SeriesForm.vue @@ -20,390 +20,336 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/forms/YAxisForm.vue b/src/plugins/plot/inspector/forms/YAxisForm.vue index 834235bb09..d9f57b402f 100644 --- a/src/plugins/plot/inspector/forms/YAxisForm.vue +++ b/src/plugins/plot/inspector/forms/YAxisForm.vue @@ -20,340 +20,324 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/forms/formUtil.js b/src/plugins/plot/inspector/forms/formUtil.js index 661d9c4b9b..144ae45648 100644 --- a/src/plugins/plot/inspector/forms/formUtil.js +++ b/src/plugins/plot/inspector/forms/formUtil.js @@ -1,19 +1,19 @@ export function coerce(value, coerceFunc) { - if (coerceFunc) { - return coerceFunc(value); - } + if (coerceFunc) { + return coerceFunc(value); + } - return value; + return value; } export function validate(value, model, validateFunc) { - if (validateFunc) { - return validateFunc(value, model); - } + if (validateFunc) { + return validateFunc(value, model); + } - return true; + return true; } export function objectPath(path) { - return path && typeof path !== 'function' ? () => path : path; + return path && typeof path !== 'function' ? () => path : path; } diff --git a/src/plugins/plot/legend/PlotLegend.vue b/src/plugins/plot/legend/PlotLegend.vue index 75d6787944..9c7e581c28 100644 --- a/src/plugins/plot/legend/PlotLegend.vue +++ b/src/plugins/plot/legend/PlotLegend.vue @@ -20,228 +20,204 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/legend/PlotLegendItemCollapsed.vue b/src/plugins/plot/legend/PlotLegendItemCollapsed.vue index 26ab30764b..f87c24976b 100644 --- a/src/plugins/plot/legend/PlotLegendItemCollapsed.vue +++ b/src/plugins/plot/legend/PlotLegendItemCollapsed.vue @@ -20,155 +20,171 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/legend/PlotLegendItemExpanded.vue b/src/plugins/plot/legend/PlotLegendItemExpanded.vue index b1cd427ecd..ad2e8270b9 100644 --- a/src/plugins/plot/legend/PlotLegendItemExpanded.vue +++ b/src/plugins/plot/legend/PlotLegendItemExpanded.vue @@ -20,190 +20,192 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/lib/eventHelpers.js b/src/plugins/plot/lib/eventHelpers.js index 90b5dedecf..83b4e20944 100644 --- a/src/plugins/plot/lib/eventHelpers.js +++ b/src/plugins/plot/lib/eventHelpers.js @@ -22,71 +22,72 @@ /*jscs:disable disallowDanglingUnderscores */ const helperFunctions = { - listenTo: function (object, event, callback, context) { - if (!this._listeningTo) { - this._listeningTo = []; - } - - const listener = { - object: object, - event: event, - callback: callback, - context: context, - _cb: context ? callback.bind(context) : callback - }; - if (object.addEventListener) { - object.addEventListener(event, listener._cb); - } else { - object.on(event, listener._cb); - } - - this._listeningTo.push(listener); - }, - - stopListening: function (object, event, callback, context) { - if (!this._listeningTo) { - this._listeningTo = []; - } - - this._listeningTo.filter(function (listener) { - if (object && object !== listener.object) { - return false; - } - - if (event && event !== listener.event) { - return false; - } - - if (callback && callback !== listener.callback) { - return false; - } - - if (context && context !== listener.context) { - return false; - } - - return true; - }) - .map(function (listener) { - if (listener.unlisten) { - listener.unlisten(); - } else if (listener.object.removeEventListener) { - listener.object.removeEventListener(listener.event, listener._cb); - } else { - listener.object.off(listener.event, listener._cb); - } - - return listener; - }) - .forEach(function (listener) { - this._listeningTo.splice(this._listeningTo.indexOf(listener), 1); - }, this); - }, - - extend: function (object) { - object.listenTo = helperFunctions.listenTo; - object.stopListening = helperFunctions.stopListening; + listenTo: function (object, event, callback, context) { + if (!this._listeningTo) { + this._listeningTo = []; } + + const listener = { + object: object, + event: event, + callback: callback, + context: context, + _cb: context ? callback.bind(context) : callback + }; + if (object.addEventListener) { + object.addEventListener(event, listener._cb); + } else { + object.on(event, listener._cb); + } + + this._listeningTo.push(listener); + }, + + stopListening: function (object, event, callback, context) { + if (!this._listeningTo) { + this._listeningTo = []; + } + + this._listeningTo + .filter(function (listener) { + if (object && object !== listener.object) { + return false; + } + + if (event && event !== listener.event) { + return false; + } + + if (callback && callback !== listener.callback) { + return false; + } + + if (context && context !== listener.context) { + return false; + } + + return true; + }) + .map(function (listener) { + if (listener.unlisten) { + listener.unlisten(); + } else if (listener.object.removeEventListener) { + listener.object.removeEventListener(listener.event, listener._cb); + } else { + listener.object.off(listener.event, listener._cb); + } + + return listener; + }) + .forEach(function (listener) { + this._listeningTo.splice(this._listeningTo.indexOf(listener), 1); + }, this); + }, + + extend: function (object) { + object.listenTo = helperFunctions.listenTo; + object.stopListening = helperFunctions.stopListening; + } }; export default helperFunctions; diff --git a/src/plugins/plot/mathUtils.js b/src/plugins/plot/mathUtils.js index 38dc356187..c9f4f30de8 100644 --- a/src/plugins/plot/mathUtils.js +++ b/src/plugins/plot/mathUtils.js @@ -8,11 +8,11 @@ Returns the logarithm of a number, using the given base or the natural number @param {number=} base log base, defaults to e */ export function log(n, base = e) { - if (base === e) { - return Math.log(n); - } + if (base === e) { + return Math.log(n); + } - return Math.log(n) / Math.log(base); + return Math.log(n) / Math.log(base); } /** @@ -22,7 +22,7 @@ natural number `e` as base if not specified. @param {number=} base log base, defaults to e */ export function antilog(n, base = e) { - return Math.pow(base, n); + return Math.pow(base, n); } /** @@ -31,7 +31,7 @@ A symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297# @param {number=} base log base, defaults to e */ export function symlog(n, base = e) { - return Math.sign(n) * log(Math.abs(n) + 1, base); + return Math.sign(n) * log(Math.abs(n) + 1, base); } /** @@ -40,5 +40,5 @@ An inverse symmetric logarithm function. See https://github.com/nasa/openmct/iss @param {number=} base log base, defaults to e */ export function antisymlog(n, base = e) { - return Math.sign(n) * (antilog(Math.abs(n), base) - 1); + return Math.sign(n) * (antilog(Math.abs(n), base) - 1); } diff --git a/src/plugins/plot/overlayPlot/OverlayPlotCompositionPolicy.js b/src/plugins/plot/overlayPlot/OverlayPlotCompositionPolicy.js index e769d0181b..ca5185e8b0 100644 --- a/src/plugins/plot/overlayPlot/OverlayPlotCompositionPolicy.js +++ b/src/plugins/plot/overlayPlot/OverlayPlotCompositionPolicy.js @@ -1,29 +1,29 @@ export default function OverlayPlotCompositionPolicy(openmct) { - function hasNumericTelemetry(domainObject) { - const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); - if (!hasTelemetry) { - return false; - } - - let metadata = openmct.telemetry.getMetadata(domainObject); - - return metadata.values().length > 0 && hasDomainAndRange(metadata); + function hasNumericTelemetry(domainObject) { + const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); + if (!hasTelemetry) { + return false; } - function hasDomainAndRange(metadata) { - return (metadata.valuesForHints(['range']).length > 0 - && metadata.valuesForHints(['domain']).length > 0); + let metadata = openmct.telemetry.getMetadata(domainObject); + + return metadata.values().length > 0 && hasDomainAndRange(metadata); + } + + function hasDomainAndRange(metadata) { + return ( + metadata.valuesForHints(['range']).length > 0 && + metadata.valuesForHints(['domain']).length > 0 + ); + } + + return { + allow: function (parent, child) { + if (parent.type === 'telemetry.plot.overlay' && hasNumericTelemetry(child) === false) { + return false; + } + + return true; } - - return { - allow: function (parent, child) { - - if (parent.type === 'telemetry.plot.overlay' - && (hasNumericTelemetry(child) === false)) { - return false; - } - - return true; - } - }; + }; } diff --git a/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js b/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js index 09ba5cf080..87d4a0f3d6 100644 --- a/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js +++ b/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js @@ -24,62 +24,62 @@ import Plot from '../Plot.vue'; import Vue from 'vue'; export default function OverlayPlotViewProvider(openmct) { - function isCompactView(objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); + function isCompactView(objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip'); - return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); - } + return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); + } - return { - key: 'plot-overlay', - name: 'Overlay Plot', - cssClass: 'icon-telemetry', - canView(domainObject, objectPath) { - return domainObject.type === 'telemetry.plot.overlay'; - }, + return { + key: 'plot-overlay', + name: 'Overlay Plot', + cssClass: 'icon-telemetry', + canView(domainObject, objectPath) { + return domainObject.type === 'telemetry.plot.overlay'; + }, - canEdit(domainObject, objectPath) { - return domainObject.type === 'telemetry.plot.overlay'; - }, + canEdit(domainObject, objectPath) { + return domainObject.type === 'telemetry.plot.overlay'; + }, - view: function (domainObject, objectPath) { - let component; + view: function (domainObject, objectPath) { + let component; - return { - show: function (element) { - let isCompact = isCompactView(objectPath); - component = new Vue({ - el: element, - components: { - Plot - }, - provide: { - openmct, - domainObject, - path: objectPath - }, - data() { - return { - options: { - compact: isCompact - } - }; - }, - template: '' - }); - }, - getViewContext() { - if (!component) { - return {}; - } - - return component.$refs.plotComponent.getViewContext(); - }, - destroy: function () { - component.$destroy(); - component = undefined; + return { + show: function (element) { + let isCompact = isCompactView(objectPath); + component = new Vue({ + el: element, + components: { + Plot + }, + provide: { + openmct, + domainObject, + path: objectPath + }, + data() { + return { + options: { + compact: isCompact } - }; + }; + }, + template: '' + }); + }, + getViewContext() { + if (!component) { + return {}; + } + + return component.$refs.plotComponent.getViewContext(); + }, + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/plot/overlayPlot/pluginSpec.js b/src/plugins/plot/overlayPlot/pluginSpec.js index dcb0eaab9b..27b215f121 100644 --- a/src/plugins/plot/overlayPlot/pluginSpec.js +++ b/src/plugins/plot/overlayPlot/pluginSpec.js @@ -20,485 +20,502 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing"; -import PlotVuePlugin from "../plugin"; -import Vue from "vue"; -import Plot from "../Plot.vue"; -import configStore from "../configuration/ConfigStore"; -import EventEmitter from "EventEmitter"; -import PlotOptions from "../inspector/PlotOptions.vue"; +import { + createMouseEvent, + createOpenMct, + resetApplicationState, + spyOnBuiltins +} from 'utils/testing'; +import PlotVuePlugin from '../plugin'; +import Vue from 'vue'; +import Plot from '../Plot.vue'; +import configStore from '../configuration/ConfigStore'; +import EventEmitter from 'EventEmitter'; +import PlotOptions from '../inspector/PlotOptions.vue'; -describe("the plugin", function () { - let element; - let child; - let openmct; - let telemetryPromise; - let telemetryPromiseResolve; - let mockObjectPath; - let overlayPlotObject = { +describe('the plugin', function () { + let element; + let child; + let openmct; + let telemetryPromise; + let telemetryPromiseResolve; + let mockObjectPath; + let overlayPlotObject = { + identifier: { + namespace: '', + key: 'test-plot' + }, + type: 'telemetry.plot.overlay', + name: 'Test Overlay Plot', + composition: [], + configuration: { + series: [] + } + }; + + beforeEach((done) => { + mockObjectPath = [ + { + name: 'mock folder', + type: 'fake-folder', identifier: { - namespace: "", - key: "test-plot" - }, - type: "telemetry.plot.overlay", - name: "Test Overlay Plot", - composition: [], - configuration: { - series: [] + key: 'mock-folder', + namespace: '' } + }, + { + name: 'mock parent folder', + type: 'time-strip', + identifier: { + key: 'mock-parent-folder', + namespace: '' + } + } + ]; + const testTelemetry = [ + { + utc: 1, + 'some-key': 'some-value 1', + 'some-other-key': 'some-other-value 1', + 'some-key2': 'some-value2 1', + 'some-other-key2': 'some-other-value2 1' + }, + { + utc: 2, + 'some-key': 'some-value 2', + 'some-other-key': 'some-other-value 2', + 'some-key2': 'some-value2 2', + 'some-other-key2': 'some-other-value2 2' + }, + { + utc: 3, + 'some-key': 'some-value 3', + 'some-other-key': 'some-other-value 3', + 'some-key2': 'some-value2 2', + 'some-other-key2': 'some-other-value2 2' + } + ]; + + const timeSystem = { + timeSystemKey: 'utc', + bounds: { + start: 0, + end: 4 + } }; - beforeEach((done) => { - mockObjectPath = [ - { - name: 'mock folder', - type: 'fake-folder', - identifier: { - key: 'mock-folder', - namespace: '' - } - }, - { - name: 'mock parent folder', - type: 'time-strip', - identifier: { - key: 'mock-parent-folder', - namespace: '' - } - } - ]; - const testTelemetry = [ - { - 'utc': 1, - 'some-key': 'some-value 1', - 'some-other-key': 'some-other-value 1', - 'some-key2': 'some-value2 1', - 'some-other-key2': 'some-other-value2 1' - }, - { - 'utc': 2, - 'some-key': 'some-value 2', - 'some-other-key': 'some-other-value 2', - 'some-key2': 'some-value2 2', - 'some-other-key2': 'some-other-value2 2' - }, - { - 'utc': 3, - 'some-key': 'some-value 3', - 'some-other-key': 'some-other-value 3', - 'some-key2': 'some-value2 2', - 'some-other-key2': 'some-other-value2 2' - } - ]; + openmct = createOpenMct(timeSystem); - const timeSystem = { - timeSystemKey: 'utc', - bounds: { - start: 0, - end: 4 - } - }; - - openmct = createOpenMct(timeSystem); - - telemetryPromise = new Promise((resolve) => { - telemetryPromiseResolve = resolve; - }); - - spyOn(openmct.telemetry, 'request').and.callFake(() => { - telemetryPromiseResolve(testTelemetry); - - return telemetryPromise; - }); - - openmct.install(new PlotVuePlugin()); - - element = document.createElement("div"); - element.style.width = "640px"; - element.style.height = "480px"; - child = document.createElement("div"); - child.style.width = "640px"; - child.style.height = "480px"; - element.appendChild(child); - document.body.appendChild(element); - - spyOn(window, 'ResizeObserver').and.returnValue({ - observe() {}, - unobserve() {}, - disconnect() {} - }); - - openmct.types.addType("test-object", { - creatable: true - }); - - spyOnBuiltins(["requestAnimationFrame"]); - window.requestAnimationFrame.and.callFake((callBack) => { - callBack(); - }); - - openmct.router.path = [overlayPlotObject]; - openmct.on("start", done); - openmct.startHeadless(); + telemetryPromise = new Promise((resolve) => { + telemetryPromiseResolve = resolve; }); - afterEach((done) => { - openmct.time.timeSystem('utc', { - start: 0, - end: 1 - }); - configStore.deleteAll(); - resetApplicationState(openmct).then(done).catch(done); + spyOn(openmct.telemetry, 'request').and.callFake(() => { + telemetryPromiseResolve(testTelemetry); + + return telemetryPromise; }); + openmct.install(new PlotVuePlugin()); + + element = document.createElement('div'); + element.style.width = '640px'; + element.style.height = '480px'; + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + element.appendChild(child); + document.body.appendChild(element); + + spyOn(window, 'ResizeObserver').and.returnValue({ + observe() {}, + unobserve() {}, + disconnect() {} + }); + + openmct.types.addType('test-object', { + creatable: true + }); + + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((callBack) => { + callBack(); + }); + + openmct.router.path = [overlayPlotObject]; + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach((done) => { + openmct.time.timeSystem('utc', { + start: 0, + end: 1 + }); + configStore.deleteAll(); + resetApplicationState(openmct).then(done).catch(done); + }); + + afterAll(() => { + openmct.router.path = null; + }); + + describe('the plot views', () => { + it('provides an overlay plot view for objects with telemetry', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'telemetry.plot.overlay', + telemetry: { + values: [ + { + key: 'some-key' + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + let plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-overlay'); + expect(plotView).toBeDefined(); + }); + }); + + describe('The overlay plot view with multiple axes', () => { + let testTelemetryObject; + let testTelemetryObject2; + let config; + let component; + let mockComposition; + afterAll(() => { + component.$destroy(); + openmct.router.path = null; + }); + + beforeEach(() => { + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key', + name: 'Some attribute', + hints: { + range: 1 + } + }, + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 2 + } + } + ] + } + }; + + testTelemetryObject2 = { + identifier: { + namespace: '', + key: 'test-object2' + }, + type: 'test-object', + name: 'Test Object2', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key2', + name: 'Some attribute2', + hints: { + range: 1 + } + }, + { + key: 'some-other-key2', + name: 'Another attribute2', + hints: { + range: 2 + } + } + ] + } + }; + overlayPlotObject.composition = [ + { + identifier: testTelemetryObject.identifier + }, + { + identifier: testTelemetryObject2.identifier + } + ]; + overlayPlotObject.configuration.series = [ + { + identifier: testTelemetryObject.identifier, + yAxisId: 1 + }, + { + identifier: testTelemetryObject2.identifier, + yAxisId: 3 + } + ]; + overlayPlotObject.configuration.additionalYAxes = [ + { + label: 'Test Object Label', + id: 2 + }, + { + label: 'Test Object 2 Label', + id: 3 + } + ]; + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testTelemetryObject); + mockComposition.emit('add', testTelemetryObject2); + + return [testTelemetryObject, testTelemetryObject2]; + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + Plot + }, + provide: { + openmct: openmct, + domainObject: overlayPlotObject, + composition: openmct.composition.get(overlayPlotObject), + path: [overlayPlotObject] + }, + template: '' + }); + + return telemetryPromise.then(Vue.nextTick()).then(() => { + const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); + config = configStore.get(configId); + }); + }); + + it('Renders multiple Y-axis for the telemetry objects', (done) => { + config.yAxis.set('displayRange', { + min: 10, + max: 20 + }); + Vue.nextTick(() => { + let yAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper' + ); + expect(yAxisElement.length).toBe(2); + done(); + }); + }); + + describe('the inspector view', () => { + let inspectorComponent; + let viewComponentObject; + let selection; + beforeEach((done) => { + selection = [ + [ + { + context: { + item: { + id: overlayPlotObject.identifier.key, + identifier: overlayPlotObject.identifier, + type: overlayPlotObject.type, + configuration: overlayPlotObject.configuration, + composition: overlayPlotObject.composition + } + } + } + ] + ]; + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + inspectorComponent = new Vue({ + el: viewContainer, + components: { + PlotOptions + }, + provide: { + openmct: openmct, + domainObject: selection[0][0].context.item, + path: [selection[0][0].context.item] + }, + template: '' + }); + + Vue.nextTick(() => { + viewComponentObject = inspectorComponent.$root.$children[0]; + done(); + }); + }); + + afterEach(() => { openmct.router.path = null; + }); + + describe('in edit mode', () => { + let editOptionsEl; + + beforeEach((done) => { + viewComponentObject.setEditState(true); + Vue.nextTick(() => { + editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); + done(); + }); + }); + + it('shows multiple yAxis options', () => { + const yAxisProperties = editOptionsEl.querySelectorAll( + '.js-yaxis-grid-properties .l-inspector-part h2' + ); + expect(yAxisProperties.length).toEqual(2); + }); + + it('saves yAxis options', () => { + //toggle log mode and save + config.additionalYAxes[1].set('displayRange', { + min: 10, + max: 20 + }); + const yAxisProperties = editOptionsEl.querySelectorAll('.js-log-mode-input'); + const clickEvent = createMouseEvent('click'); + yAxisProperties[1].dispatchEvent(clickEvent); + + expect(config.additionalYAxes[1].get('logMode')).toEqual(true); + }); + }); + }); + }); + + describe('The overlay plot view with single axes', () => { + let testTelemetryObject; + let config; + let component; + let mockComposition; + + afterAll(() => { + component.$destroy(); + openmct.router.path = null; }); - describe("the plot views", () => { - it("provides an overlay plot view for objects with telemetry", () => { - const testTelemetryObject = { - id: "test-object", - type: "telemetry.plot.overlay", - telemetry: { - values: [{ - key: "some-key" - }] - } - }; + beforeEach(() => { + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key', + name: 'Some attribute', + hints: { + range: 1 + } + }, + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 2 + } + } + ] + } + }; - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay"); - expect(plotView).toBeDefined(); - }); + overlayPlotObject.composition = [ + { + identifier: testTelemetryObject.identifier + } + ]; + overlayPlotObject.configuration.series = [ + { + identifier: testTelemetryObject.identifier + } + ]; + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testTelemetryObject); + return [testTelemetryObject]; + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + Plot + }, + provide: { + openmct: openmct, + domainObject: overlayPlotObject, + composition: openmct.composition.get(overlayPlotObject), + path: [overlayPlotObject] + }, + template: '' + }); + + return telemetryPromise.then(Vue.nextTick()).then(() => { + const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); + config = configStore.get(configId); + }); }); - describe("The overlay plot view with multiple axes", () => { - let testTelemetryObject; - let testTelemetryObject2; - let config; - let component; - let mockComposition; - - afterAll(() => { - component.$destroy(); - openmct.router.path = null; - }); - - beforeEach(() => { - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key", - name: "Some attribute", - hints: { - range: 1 - } - }, { - key: "some-other-key", - name: "Another attribute", - hints: { - range: 2 - } - }] - } - }; - - testTelemetryObject2 = { - identifier: { - namespace: "", - key: "test-object2" - }, - type: "test-object", - name: "Test Object2", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key2", - name: "Some attribute2", - hints: { - range: 1 - } - }, { - key: "some-other-key2", - name: "Another attribute2", - hints: { - range: 2 - } - }] - } - }; - overlayPlotObject.composition = [ - { - identifier: testTelemetryObject.identifier - }, - { - identifier: testTelemetryObject2.identifier - } - ]; - overlayPlotObject.configuration.series = [ - { - identifier: testTelemetryObject.identifier, - yAxisId: 1 - }, - { - identifier: testTelemetryObject2.identifier, - yAxisId: 3 - } - ]; - overlayPlotObject.configuration.additionalYAxes = [ - { - label: 'Test Object Label', - id: 2 - }, - { - label: 'Test Object 2 Label', - id: 3 - } - ]; - mockComposition = new EventEmitter(); - mockComposition.load = () => { - mockComposition.emit('add', testTelemetryObject); - mockComposition.emit('add', testTelemetryObject2); - - return [testTelemetryObject, testTelemetryObject2]; - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - let viewContainer = document.createElement("div"); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - Plot - }, - provide: { - openmct: openmct, - domainObject: overlayPlotObject, - composition: openmct.composition.get(overlayPlotObject), - path: [overlayPlotObject] - }, - template: '' - }); - - return telemetryPromise - .then(Vue.nextTick()) - .then(() => { - const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); - config = configStore.get(configId); - }); - }); - - it("Renders multiple Y-axis for the telemetry objects", (done) => { - config.yAxis.set('displayRange', { - min: 10, - max: 20 - }); - Vue.nextTick(() => { - let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper"); - expect(yAxisElement.length).toBe(2); - done(); - }); - }); - - describe('the inspector view', () => { - let inspectorComponent; - let viewComponentObject; - let selection; - beforeEach((done) => { - selection = [ - [ - { - context: { - item: { - id: overlayPlotObject.identifier.key, - identifier: overlayPlotObject.identifier, - type: overlayPlotObject.type, - configuration: overlayPlotObject.configuration, - composition: overlayPlotObject.composition - } - } - } - ] - ]; - - let viewContainer = document.createElement('div'); - child.append(viewContainer); - inspectorComponent = new Vue({ - el: viewContainer, - components: { - PlotOptions - }, - provide: { - openmct: openmct, - domainObject: selection[0][0].context.item, - path: [selection[0][0].context.item] - }, - template: '' - }); - - Vue.nextTick(() => { - viewComponentObject = inspectorComponent.$root.$children[0]; - done(); - }); - }); - - afterEach(() => { - openmct.router.path = null; - }); - - describe('in edit mode', () => { - let editOptionsEl; - - beforeEach((done) => { - viewComponentObject.setEditState(true); - Vue.nextTick(() => { - editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); - done(); - }); - }); - - it('shows multiple yAxis options', () => { - const yAxisProperties = editOptionsEl.querySelectorAll(".js-yaxis-grid-properties .l-inspector-part h2"); - expect(yAxisProperties.length).toEqual(2); - }); - - it('saves yAxis options', () => { - //toggle log mode and save - config.additionalYAxes[1].set('displayRange', { - min: 10, - max: 20 - }); - const yAxisProperties = editOptionsEl.querySelectorAll(".js-log-mode-input"); - const clickEvent = createMouseEvent("click"); - yAxisProperties[1].dispatchEvent(clickEvent); - - expect(config.additionalYAxes[1].get('logMode')).toEqual(true); - - }); - }); - - }); - - }); - - describe("The overlay plot view with single axes", () => { - let testTelemetryObject; - let config; - let component; - let mockComposition; - - afterAll(() => { - component.$destroy(); - openmct.router.path = null; - }); - - beforeEach(() => { - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key", - name: "Some attribute", - hints: { - range: 1 - } - }, { - key: "some-other-key", - name: "Another attribute", - hints: { - range: 2 - } - }] - } - }; - - overlayPlotObject.composition = [ - { - identifier: testTelemetryObject.identifier - } - ]; - overlayPlotObject.configuration.series = [ - { - identifier: testTelemetryObject.identifier - } - ]; - mockComposition = new EventEmitter(); - mockComposition.load = () => { - mockComposition.emit('add', testTelemetryObject); - - return [testTelemetryObject]; - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - let viewContainer = document.createElement("div"); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - Plot - }, - provide: { - openmct: openmct, - domainObject: overlayPlotObject, - composition: openmct.composition.get(overlayPlotObject), - path: [overlayPlotObject] - }, - template: '' - }); - - return telemetryPromise - .then(Vue.nextTick()) - .then(() => { - const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); - config = configStore.get(configId); - }); - }); - - it("Renders single Y-axis for the telemetry object", (done) => { - config.yAxis.set('displayRange', { - min: 10, - max: 20 - }); - Vue.nextTick(() => { - let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper"); - expect(yAxisElement.length).toBe(1); - done(); - }); - }); + it('Renders single Y-axis for the telemetry object', (done) => { + config.yAxis.set('displayRange', { + min: 10, + max: 20 + }); + Vue.nextTick(() => { + let yAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper' + ); + expect(yAxisElement.length).toBe(1); + done(); + }); }); + }); }); diff --git a/src/plugins/plot/plugin.js b/src/plugins/plot/plugin.js index 315a9a6048..94d76d673d 100644 --- a/src/plugins/plot/plugin.js +++ b/src/plugins/plot/plugin.js @@ -25,60 +25,61 @@ import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider'; import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider'; import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy'; import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy'; -import PlotViewActions from "./actions/ViewActions"; -import StackedPlotsInspectorViewProvider from "./inspector/StackedPlotsInspectorViewProvider"; -import stackedPlotConfigurationInterceptor from "./stackedPlot/stackedPlotConfigurationInterceptor"; +import PlotViewActions from './actions/ViewActions'; +import StackedPlotsInspectorViewProvider from './inspector/StackedPlotsInspectorViewProvider'; +import stackedPlotConfigurationInterceptor from './stackedPlot/stackedPlotConfigurationInterceptor'; export default function () { - return function install(openmct) { + return function install(openmct) { + openmct.types.addType('telemetry.plot.overlay', { + key: 'telemetry.plot.overlay', + name: 'Overlay Plot', + cssClass: 'icon-plot-overlay', + description: + 'Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.', + creatable: true, + initialize: function (domainObject) { + domainObject.composition = []; + domainObject.configuration = { + //series is an array of objects of type: {identifier, series: {color...}, yAxis:{}} + series: [] + }; + }, + priority: 891 + }); - openmct.types.addType('telemetry.plot.overlay', { - key: "telemetry.plot.overlay", - name: "Overlay Plot", - cssClass: "icon-plot-overlay", - description: "Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.", - creatable: true, - initialize: function (domainObject) { - domainObject.composition = []; - domainObject.configuration = { - //series is an array of objects of type: {identifier, series: {color...}, yAxis:{}} - series: [] - }; - }, - priority: 891 - }); + openmct.types.addType('telemetry.plot.stacked', { + key: 'telemetry.plot.stacked', + name: 'Stacked Plot', + cssClass: 'icon-plot-stacked', + description: + 'Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.', + creatable: true, + initialize: function (domainObject) { + domainObject.composition = []; + domainObject.configuration = { + series: [], + yAxis: {}, + xAxis: {} + }; + }, + priority: 890 + }); - openmct.types.addType('telemetry.plot.stacked', { - key: "telemetry.plot.stacked", - name: "Stacked Plot", - cssClass: "icon-plot-stacked", - description: "Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.", - creatable: true, - initialize: function (domainObject) { - domainObject.composition = []; - domainObject.configuration = { - series: [], - yAxis: {}, - xAxis: {} - }; - }, - priority: 890 - }); + stackedPlotConfigurationInterceptor(openmct); - stackedPlotConfigurationInterceptor(openmct); + openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct)); + openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct)); + openmct.objectViews.addProvider(new PlotViewProvider(openmct)); - openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct)); - openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct)); - openmct.objectViews.addProvider(new PlotViewProvider(openmct)); + openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct)); + openmct.inspectorViews.addProvider(new StackedPlotsInspectorViewProvider(openmct)); - openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct)); - openmct.inspectorViews.addProvider(new StackedPlotsInspectorViewProvider(openmct)); + openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow); + openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow); - openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow); - openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow); - - PlotViewActions.forEach(action => { - openmct.actions.register(action); - }); - }; + PlotViewActions.forEach((action) => { + openmct.actions.register(action); + }); + }; } diff --git a/src/plugins/plot/pluginSpec.js b/src/plugins/plot/pluginSpec.js index 4fec092ff4..f6524b4f5f 100644 --- a/src/plugins/plot/pluginSpec.js +++ b/src/plugins/plot/pluginSpec.js @@ -20,878 +20,921 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing"; -import PlotVuePlugin from "./plugin"; -import Vue from "vue"; -import configStore from "./configuration/ConfigStore"; -import EventEmitter from "EventEmitter"; -import PlotOptions from "./inspector/PlotOptions.vue"; -import PlotConfigurationModel from "./configuration/PlotConfigurationModel"; +import { + createMouseEvent, + createOpenMct, + resetApplicationState, + spyOnBuiltins +} from 'utils/testing'; +import PlotVuePlugin from './plugin'; +import Vue from 'vue'; +import configStore from './configuration/ConfigStore'; +import EventEmitter from 'EventEmitter'; +import PlotOptions from './inspector/PlotOptions.vue'; +import PlotConfigurationModel from './configuration/PlotConfigurationModel'; const TEST_KEY_ID = 'some-other-key'; -describe("the plugin", function () { - let element; - let child; - let openmct; - let telemetryPromise; - let telemetryPromiseResolve; - let mockObjectPath; - let telemetrylimitProvider; +describe('the plugin', function () { + let element; + let child; + let openmct; + let telemetryPromise; + let telemetryPromiseResolve; + let mockObjectPath; + let telemetrylimitProvider; - beforeEach((done) => { - mockObjectPath = [ + beforeEach((done) => { + mockObjectPath = [ + { + name: 'mock folder', + type: 'fake-folder', + identifier: { + key: 'mock-folder', + namespace: '' + } + }, + { + name: 'mock parent folder', + type: 'time-strip', + identifier: { + key: 'mock-parent-folder', + namespace: '' + } + } + ]; + const testTelemetry = [ + { + utc: 1, + 'some-key': 'some-value 1', + 'some-other-key': 'some-other-value 1' + }, + { + utc: 2, + 'some-key': 'some-value 2', + 'some-other-key': 'some-other-value 2' + }, + { + utc: 3, + 'some-key': 'some-value 3', + 'some-other-key': 'some-other-value 3' + } + ]; + + const timeSystem = { + timeSystemKey: 'utc', + bounds: { + start: 0, + end: 4 + } + }; + + openmct = createOpenMct(timeSystem); + + telemetryPromise = new Promise((resolve) => { + telemetryPromiseResolve = resolve; + }); + + spyOn(openmct.telemetry, 'request').and.callFake(() => { + telemetryPromiseResolve(testTelemetry); + + return telemetryPromise; + }); + + telemetrylimitProvider = jasmine.createSpyObj('telemetrylimitProvider', [ + 'supportsLimits', + 'getLimits', + 'getLimitEvaluator' + ]); + telemetrylimitProvider.supportsLimits.and.returnValue(true); + telemetrylimitProvider.getLimits.and.returnValue({ + limits: function () { + return Promise.resolve({ + WARNING: { + low: { + cssClass: 'is-limit--lwr is-limit--yellow', + 'some-key': -0.5 + }, + high: { + cssClass: 'is-limit--upr is-limit--yellow', + 'some-key': 0.5 + } + }, + DISTRESS: { + low: { + cssClass: 'is-limit--lwr is-limit--red', + 'some-key': -0.9 + }, + high: { + cssClass: 'is-limit--upr is-limit--red', + 'some-key': 0.9 + } + } + }); + } + }); + telemetrylimitProvider.getLimitEvaluator.and.returnValue({ + evaluate: function () { + return {}; + } + }); + openmct.telemetry.addProvider(telemetrylimitProvider); + + openmct.install(new PlotVuePlugin()); + + element = document.createElement('div'); + element.style.width = '640px'; + element.style.height = '480px'; + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + element.appendChild(child); + document.body.appendChild(element); + + openmct.types.addType('test-object', { + creatable: true + }); + + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((callBack) => { + callBack(); + }); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach((done) => { + openmct.time.timeSystem('utc', { + start: 0, + end: 2 + }); + + configStore.deleteAll(); + resetApplicationState(openmct).then(done).catch(done); + }); + + describe('the plot views', () => { + it('provides a plot view for objects with telemetry', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'test-object', + telemetry: { + values: [ { - name: 'mock folder', - type: 'fake-folder', - identifier: { - key: 'mock-folder', - namespace: '' - } + key: 'some-key', + hints: { + domain: 1 + } }, { - name: 'mock parent folder', + key: 'other-key', + hints: { + range: 1 + } + }, + { + key: 'yet-another-key', + format: 'string', + hints: { + range: 2 + } + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + const plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single'); + + expect(plotView).toBeDefined(); + }); + + it('does not provide a plot view if the telemetry is entirely non numeric', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'test-object', + telemetry: { + values: [ + { + key: 'some-key', + hints: { + domain: 1 + } + }, + { + key: 'other-key', + format: 'string', + hints: { + range: 1 + } + }, + { + key: 'yet-another-key', + format: 'string', + hints: { + range: 1 + } + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + const plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single'); + + expect(plotView).toBeUndefined(); + }); + + it('provides an overlay plot view for objects with telemetry', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'telemetry.plot.overlay', + telemetry: { + values: [ + { + key: 'some-key' + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + let plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-overlay'); + expect(plotView).toBeDefined(); + }); + + it('provides an inspector view for overlay plots', () => { + let selection = [ + [ + { + context: { + item: { + id: 'test-object', + type: 'telemetry.plot.overlay', + telemetry: { + values: [ + { + key: 'some-key' + } + ] + } + } + } + }, + { + context: { + item: { + type: 'time-strip' + } + } + } + ] + ]; + const applicableInspectorViews = openmct.inspectorViews.get(selection); + const plotInspectorView = applicableInspectorViews.find( + (view) => (view.name = 'Plots Configuration') + ); + + expect(plotInspectorView).toBeDefined(); + }); + + it('provides a stacked plot view for objects with telemetry', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'telemetry.plot.stacked', + telemetry: { + values: [ + { + key: 'some-key' + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + let plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-stacked'); + expect(plotView).toBeDefined(); + }); + }); + + describe('The single plot view', () => { + let testTelemetryObject; + let applicableViews; + let plotViewProvider; + let plotView; + + beforeEach(() => { + openmct.time.timeSystem('utc', { + start: 0, + end: 4 + }); + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key', + name: 'Some attribute', + hints: { + range: 1 + } + }, + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 2 + } + } + ] + } + }; + + openmct.router.path = [testTelemetryObject]; + + applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single'); + plotView = plotViewProvider.view(testTelemetryObject, []); + plotView.show(child, true); + + return Vue.nextTick(); + }); + + afterEach(() => { + openmct.router.path = null; + }); + + it('Makes only one request for telemetry on load', () => { + expect(openmct.telemetry.request).toHaveBeenCalledTimes(1); + }); + + it('Renders a collapsed legend for every telemetry', () => { + let legend = element.querySelectorAll('.plot-wrapper-collapsed-legend .plot-series-name'); + expect(legend.length).toBe(1); + expect(legend[0].innerHTML).toEqual('Test Object'); + }); + + it('Renders an expanded legend for every telemetry', () => { + let legendControl = element.querySelector( + '.c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle' + ); + const clickEvent = createMouseEvent('click'); + + legendControl.dispatchEvent(clickEvent); + + let legend = element.querySelectorAll('.plot-wrapper-expanded-legend .plot-legend-item td'); + expect(legend.length).toBe(6); + }); + + it('Renders X-axis ticks for the telemetry object', (done) => { + const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); + const config = configStore.get(configId); + config.xAxis.set('displayRange', { + min: 0, + max: 4 + }); + + Vue.nextTick(() => { + let xAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper' + ); + expect(xAxisElement.length).toBe(1); + + let ticks = xAxisElement[0].querySelectorAll('.gl-plot-tick'); + expect(ticks.length).toBe(9); + + done(); + }); + }); + + it('Renders Y-axis options for the telemetry object', () => { + let yAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select' + ); + expect(yAxisElement.length).toBe(1); + //Object{name: "Some attribute", key: "some-key"}, Object{name: "Another attribute", key: "some-other-key"} + let options = yAxisElement[0].querySelectorAll('option'); + expect(options.length).toBe(2); + expect(options[0].value).toBe('Some attribute'); + expect(options[1].value).toBe('Another attribute'); + }); + + it('Updates the Y-axis label when changed', () => { + const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); + const config = configStore.get(configId); + const yAxisElement = element.querySelectorAll('.gl-plot-axis-area.gl-plot-y')[0].__vue__; + config.yAxis.seriesCollection.models.forEach((plotSeries) => { + expect(plotSeries.model.yKey).toBe('some-key'); + }); + + yAxisElement.$emit('yKeyChanged', TEST_KEY_ID, 1); + config.yAxis.seriesCollection.models.forEach((plotSeries) => { + expect(plotSeries.model.yKey).toBe(TEST_KEY_ID); + }); + }); + + it('hides the pause and play controls', () => { + let pauseEl = element.querySelectorAll('.c-button-set .icon-pause'); + let playEl = element.querySelectorAll('.c-button-set .icon-arrow-right'); + expect(pauseEl.length).toBe(0); + expect(playEl.length).toBe(0); + }); + + describe('pause and play controls', () => { + beforeEach(() => { + openmct.time.clock('local', { + start: -1000, + end: 100 + }); + + return Vue.nextTick(); + }); + + it('shows the pause controls', (done) => { + Vue.nextTick(() => { + let pauseEl = element.querySelectorAll('.c-button-set .icon-pause'); + expect(pauseEl.length).toBe(1); + done(); + }); + }); + + it('shows the play control if plot is paused', (done) => { + let pauseEl = element.querySelector('.c-button-set .icon-pause'); + const clickEvent = createMouseEvent('click'); + + pauseEl.dispatchEvent(clickEvent); + Vue.nextTick(() => { + let playEl = element.querySelectorAll('.c-button-set .is-paused'); + expect(playEl.length).toBe(1); + done(); + }); + }); + }); + + describe('resume actions on errant click', () => { + beforeEach(() => { + openmct.time.clock('local', { + start: -1000, + end: 100 + }); + + return Vue.nextTick(); + }); + + it('clicking the plot view without movement resumes the plot while active', async () => { + const pauseEl = element.querySelectorAll('.c-button-set .icon-pause'); + // if the pause button is present, the chart is running + expect(pauseEl.length).toBe(1); + + // simulate an errant mouse click + // the second item is the canvas we need to use + const canvas = element.querySelectorAll('canvas')[1]; + const mouseDownEvent = new MouseEvent('mousedown'); + const mouseUpEvent = new MouseEvent('mouseup'); + canvas.dispatchEvent(mouseDownEvent); + // mouseup event is bound to the window + window.dispatchEvent(mouseUpEvent); + await Vue.nextTick(); + + const pauseElAfterClick = element.querySelectorAll('.c-button-set .icon-pause'); + console.log('pauseElAfterClick', pauseElAfterClick); + expect(pauseElAfterClick.length).toBe(1); + }); + + it('clicking the plot view without movement leaves the plot paused', async () => { + const pauseEl = element.querySelector('.c-button-set .icon-pause'); + // pause the plot + pauseEl.dispatchEvent(createMouseEvent('click')); + await Vue.nextTick(); + + const playEl = element.querySelectorAll('.c-button-set .is-paused'); + expect(playEl.length).toBe(1); + + // simulate an errant mouse click + // the second item is the canvas we need to use + const canvas = element.querySelectorAll('canvas')[1]; + const mouseDownEvent = new MouseEvent('mousedown'); + const mouseUpEvent = new MouseEvent('mouseup'); + canvas.dispatchEvent(mouseDownEvent); + // mouseup event is bound to the window + window.dispatchEvent(mouseUpEvent); + await Vue.nextTick(); + + const playElAfterChartClick = element.querySelectorAll('.c-button-set .is-paused'); + expect(playElAfterChartClick.length).toBe(1); + }); + + it('clicking the plot does not request historical data', async () => { + expect(openmct.telemetry.request).toHaveBeenCalledTimes(2); + + // simulate an errant mouse click + // the second item is the canvas we need to use + const canvas = element.querySelectorAll('canvas')[1]; + const mouseDownEvent = new MouseEvent('mousedown'); + const mouseUpEvent = new MouseEvent('mouseup'); + canvas.dispatchEvent(mouseDownEvent); + // mouseup event is bound to the window + window.dispatchEvent(mouseUpEvent); + await Vue.nextTick(); + + expect(openmct.telemetry.request).toHaveBeenCalledTimes(2); + }); + + describe('limits', () => { + it('lines are not displayed by default', () => { + let limitEl = element.querySelectorAll('.js-limit-area .js-limit-line'); + expect(limitEl.length).toBe(0); + }); + + it('lines are displayed when configuration is set to true', (done) => { + const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); + const config = configStore.get(configId); + config.yAxis.set('displayRange', { + min: 0, + max: 4 + }); + config.series.models[0].set('limitLines', true); + + Vue.nextTick(() => { + let limitEl = element.querySelectorAll('.js-limit-area .js-limit-line'); + expect(limitEl.length).toBe(4); + done(); + }); + }); + }); + }); + + describe('controls in time strip view', () => { + it('zoom controls are hidden', () => { + let pauseEl = element.querySelectorAll('.c-button-set .js-zoom'); + expect(pauseEl.length).toBe(0); + }); + + it('pan controls are hidden', () => { + let pauseEl = element.querySelectorAll('.c-button-set .js-pan'); + expect(pauseEl.length).toBe(0); + }); + + it('pause/play controls are hidden', () => { + let pauseEl = element.querySelectorAll('.c-button-set .js-pause'); + expect(pauseEl.length).toBe(0); + }); + }); + }); + + describe('resizing the plot', () => { + let plotContainerResizeObserver; + let resizePromiseResolve; + let testTelemetryObject; + let applicableViews; + let plotViewProvider; + let plotView; + let resizePromise; + + beforeEach(() => { + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key', + name: 'Some attribute', + hints: { + range: 1 + } + }, + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 2 + } + } + ] + } + }; + + openmct.router.path = [testTelemetryObject]; + + applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single'); + plotView = plotViewProvider.view(testTelemetryObject, []); + + plotView.show(child, true); + + resizePromise = new Promise((resolve) => { + resizePromiseResolve = resolve; + }); + + const handlePlotResize = _.debounce(() => { + resizePromiseResolve(true); + }, 600); + + plotContainerResizeObserver = new ResizeObserver(handlePlotResize); + plotContainerResizeObserver.observe( + plotView.getComponent().$children[0].$children[1].$parent.$refs.plotWrapper + ); + + return Vue.nextTick(() => { + plotView.getComponent().$children[0].$children[1].stopFollowingTimeContext(); + spyOn( + plotView.getComponent().$children[0].$children[1], + 'loadSeriesData' + ).and.callThrough(); + }); + }); + + afterEach(() => { + plotContainerResizeObserver.disconnect(); + openmct.router.path = null; + }); + + it('requests historical data when over the threshold', (done) => { + element.style.width = '680px'; + resizePromise.then(() => { + expect( + plotView.getComponent().$children[0].$children[1].loadSeriesData + ).toHaveBeenCalledTimes(1); + done(); + }); + }); + + it('does not request historical data when under the threshold', (done) => { + element.style.width = '644px'; + resizePromise.then(() => { + expect( + plotView.getComponent().$children[0].$children[1].loadSeriesData + ).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('the inspector view', () => { + let component; + let viewComponentObject; + let mockComposition; + let testTelemetryObject; + let selection; + let config; + beforeEach((done) => { + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key', + name: 'Some attribute', + hints: { + range: 1 + } + }, + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 2 + } + } + ] + } + }; + + selection = [ + [ + { + context: { + item: { + id: 'test-object', + identifier: { + key: 'test-object', + namespace: '' + }, + type: 'telemetry.plot.overlay', + configuration: { + series: [ + { + identifier: { + key: 'test-object', + namespace: '' + } + } + ] + }, + composition: [] + } + } + }, + { + context: { + item: { type: 'time-strip', identifier: { - key: 'mock-parent-folder', - namespace: '' + key: 'some-other-key', + namespace: '' } + } } - ]; - const testTelemetry = [ - { - 'utc': 1, - 'some-key': 'some-value 1', - 'some-other-key': 'some-other-value 1' - }, - { - 'utc': 2, - 'some-key': 'some-value 2', - 'some-other-key': 'some-other-value 2' - }, - { - 'utc': 3, - 'some-key': 'some-value 3', - 'some-other-key': 'some-other-value 3' - } - ]; + } + ] + ]; - const timeSystem = { - timeSystemKey: 'utc', - bounds: { - start: 0, - end: 4 - } - }; + openmct.router.path = [testTelemetryObject]; + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testTelemetryObject); - openmct = createOpenMct(timeSystem); + return [testTelemetryObject]; + }; - telemetryPromise = new Promise((resolve) => { - telemetryPromiseResolve = resolve; + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier); + config = new PlotConfigurationModel({ + id: configId, + domainObject: selection[0][0].context.item, + openmct: openmct + }); + configStore.add(configId, config); + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + PlotOptions + }, + provide: { + openmct: openmct, + domainObject: selection[0][0].context.item, + path: [selection[0][0].context.item, selection[0][1].context.item] + }, + template: '' + }); + + Vue.nextTick(() => { + viewComponentObject = component.$root.$children[0]; + done(); + }); + }); + + afterEach(() => { + openmct.router.path = null; + }); + + describe('in view only mode', () => { + let browseOptionsEl; + let editOptionsEl; + beforeEach(() => { + browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); + editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); + }); + + it('does not show the edit options', () => { + expect(editOptionsEl).toBeNull(); + }); + + it('shows the name', () => { + const seriesEl = browseOptionsEl.querySelector('.c-object-label__name'); + expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name); + }); + + it('shows in collapsed mode', () => { + const seriesEl = browseOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); + expect(seriesEl.length).toEqual(0); + }); + + it('shows in expanded mode', () => { + let expandControl = browseOptionsEl.querySelector('.c-disclosure-triangle'); + const clickEvent = createMouseEvent('click'); + expandControl.dispatchEvent(clickEvent); + + const plotOptionsProperties = browseOptionsEl.querySelectorAll( + '.js-plot-options-browse-properties .grid-row' + ); + expect(plotOptionsProperties.length).toEqual(6); + }); + }); + + describe('in edit mode', () => { + let editOptionsEl; + let browseOptionsEl; + + beforeEach((done) => { + viewComponentObject.setEditState(true); + Vue.nextTick(() => { + editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); + browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); + done(); }); + }); - spyOn(openmct.telemetry, 'request').and.callFake(() => { - telemetryPromiseResolve(testTelemetry); + it('does not show the browse options', () => { + expect(browseOptionsEl).toBeNull(); + }); - return telemetryPromise; + it('shows the name', () => { + const seriesEl = editOptionsEl.querySelector('.c-object-label__name'); + expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name); + }); + + it('shows in collapsed mode', () => { + const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); + expect(seriesEl.length).toEqual(0); + }); + + it('shows in collapsed mode', () => { + const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); + expect(seriesEl.length).toEqual(0); + }); + + it('renders expanded', () => { + const expandControl = editOptionsEl.querySelector('.c-disclosure-triangle'); + const clickEvent = createMouseEvent('click'); + expandControl.dispatchEvent(clickEvent); + + const plotOptionsProperties = editOptionsEl.querySelectorAll( + '.js-plot-options-edit-properties .grid-row' + ); + expect(plotOptionsProperties.length).toEqual(8); + }); + + it('shows yKeyOptions', () => { + const expandControl = editOptionsEl.querySelector('.c-disclosure-triangle'); + const clickEvent = createMouseEvent('click'); + expandControl.dispatchEvent(clickEvent); + + const plotOptionsProperties = editOptionsEl.querySelectorAll( + '.js-plot-options-edit-properties .grid-row' + ); + + const yKeySelection = plotOptionsProperties[0].querySelector('select'); + const options = Array.from(yKeySelection.options).map((option) => { + return option.value; }); - - telemetrylimitProvider = jasmine.createSpyObj('telemetrylimitProvider', [ - 'supportsLimits', - 'getLimits', - 'getLimitEvaluator' + expect(options).toEqual([ + testTelemetryObject.telemetry.values[1].key, + testTelemetryObject.telemetry.values[2].key ]); - telemetrylimitProvider.supportsLimits.and.returnValue(true); - telemetrylimitProvider.getLimits.and.returnValue({ - limits: function () { - return Promise.resolve({ - WARNING: { - low: { - cssClass: "is-limit--lwr is-limit--yellow", - 'some-key': -0.5 - }, - high: { - cssClass: "is-limit--upr is-limit--yellow", - 'some-key': 0.5 - } - }, - DISTRESS: { - low: { - cssClass: "is-limit--lwr is-limit--red", - 'some-key': -0.9 - }, - high: { - cssClass: "is-limit--upr is-limit--red", - 'some-key': 0.9 - } - } - }); - } - }); - telemetrylimitProvider.getLimitEvaluator.and.returnValue({ - evaluate: function () { - return {}; - } - }); - openmct.telemetry.addProvider(telemetrylimitProvider); + }); - openmct.install(new PlotVuePlugin()); + it('shows yAxis options', () => { + const expandControl = editOptionsEl.querySelector('.c-disclosure-triangle'); + const clickEvent = createMouseEvent('click'); + expandControl.dispatchEvent(clickEvent); - element = document.createElement("div"); - element.style.width = "640px"; - element.style.height = "480px"; - child = document.createElement("div"); - child.style.width = "640px"; - child.style.height = "480px"; - element.appendChild(child); - document.body.appendChild(element); + const yAxisProperties = editOptionsEl.querySelectorAll( + 'div.grid-properties:first-of-type .l-inspector-part' + ); - openmct.types.addType("test-object", { - creatable: true - }); + // TODO better test + expect(yAxisProperties.length).toEqual(2); + }); - spyOnBuiltins(["requestAnimationFrame"]); - window.requestAnimationFrame.and.callFake((callBack) => { - callBack(); - }); - - openmct.on("start", done); - openmct.startHeadless(); - }); - - afterEach((done) => { - openmct.time.timeSystem('utc', { - start: 0, - end: 2 - }); - - configStore.deleteAll(); - resetApplicationState(openmct).then(done).catch(done); - }); - - describe("the plot views", () => { - - it("provides a plot view for objects with telemetry", () => { - const testTelemetryObject = { - id: "test-object", - type: "test-object", - telemetry: { - values: [{ - key: "some-key", - hints: { - domain: 1 - } - }, - { - key: "other-key", - hints: { - range: 1 - } - }, - { - key: "yet-another-key", - format: "string", - hints: { - range: 2 - } - }] - } - }; - - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); - - expect(plotView).toBeDefined(); - }); - - it("does not provide a plot view if the telemetry is entirely non numeric", () => { - const testTelemetryObject = { - id: "test-object", - type: "test-object", - telemetry: { - values: [{ - key: "some-key", - hints: { - domain: 1 - } - }, - { - key: "other-key", - format: "string", - hints: { - range: 1 - } - }, - { - key: "yet-another-key", - format: "string", - hints: { - range: 1 - } - }] - } - }; - - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); - - expect(plotView).toBeUndefined(); - }); - - it("provides an overlay plot view for objects with telemetry", () => { - const testTelemetryObject = { - id: "test-object", - type: "telemetry.plot.overlay", - telemetry: { - values: [{ - key: "some-key" - }] - } - }; - - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay"); - expect(plotView).toBeDefined(); - }); - - it('provides an inspector view for overlay plots', () => { - let selection = [ - [ - { - context: { - item: { - id: "test-object", - type: "telemetry.plot.overlay", - telemetry: { - values: [{ - key: "some-key" - }] - } - } - } - }, - { - context: { - item: { - type: 'time-strip' - } - } - } - ] - ]; - const applicableInspectorViews = openmct.inspectorViews.get(selection); - const plotInspectorView = applicableInspectorViews.find(view => view.name = 'Plots Configuration'); - - expect(plotInspectorView).toBeDefined(); - }); - - it("provides a stacked plot view for objects with telemetry", () => { - const testTelemetryObject = { - id: "test-object", - type: "telemetry.plot.stacked", - telemetry: { - values: [{ - key: "some-key" - }] - } - }; - - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked"); - expect(plotView).toBeDefined(); - }); - - }); - - describe("The single plot view", () => { - let testTelemetryObject; - let applicableViews; - let plotViewProvider; - let plotView; - - beforeEach(() => { - openmct.time.timeSystem("utc", { - start: 0, - end: 4 - }); - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key", - name: "Some attribute", - hints: { - range: 1 - } - }, { - key: "some-other-key", - name: "Another attribute", - hints: { - range: 2 - } - }] - } - }; - - openmct.router.path = [testTelemetryObject]; - - applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); - plotView = plotViewProvider.view(testTelemetryObject, []); - plotView.show(child, true); - - return Vue.nextTick(); - }); - - afterEach(() => { - openmct.router.path = null; - }); - - it("Makes only one request for telemetry on load", () => { - expect(openmct.telemetry.request).toHaveBeenCalledTimes(1); - }); - - it("Renders a collapsed legend for every telemetry", () => { - let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name"); - expect(legend.length).toBe(1); - expect(legend[0].innerHTML).toEqual("Test Object"); - }); - - it("Renders an expanded legend for every telemetry", () => { - let legendControl = element.querySelector(".c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle"); - const clickEvent = createMouseEvent("click"); - - legendControl.dispatchEvent(clickEvent); - - let legend = element.querySelectorAll(".plot-wrapper-expanded-legend .plot-legend-item td"); - expect(legend.length).toBe(6); - }); - - it("Renders X-axis ticks for the telemetry object", (done) => { - const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); - const config = configStore.get(configId); - config.xAxis.set('displayRange', { - min: 0, - max: 4 - }); - - Vue.nextTick(() => { - let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper"); - expect(xAxisElement.length).toBe(1); - - let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick"); - expect(ticks.length).toBe(9); - - done(); - }); - }); - - it("Renders Y-axis options for the telemetry object", () => { - let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select"); - expect(yAxisElement.length).toBe(1); - //Object{name: "Some attribute", key: "some-key"}, Object{name: "Another attribute", key: "some-other-key"} - let options = yAxisElement[0].querySelectorAll("option"); - expect(options.length).toBe(2); - expect(options[0].value).toBe("Some attribute"); - expect(options[1].value).toBe("Another attribute"); - }); - - it("Updates the Y-axis label when changed", () => { - const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); - const config = configStore.get(configId); - const yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y")[0].__vue__; - config.yAxis.seriesCollection.models.forEach((plotSeries) => { - expect(plotSeries.model.yKey).toBe('some-key'); - }); - - yAxisElement.$emit('yKeyChanged', TEST_KEY_ID, 1); - config.yAxis.seriesCollection.models.forEach((plotSeries) => { - expect(plotSeries.model.yKey).toBe(TEST_KEY_ID); - }); - }); - - it('hides the pause and play controls', () => { - let pauseEl = element.querySelectorAll(".c-button-set .icon-pause"); - let playEl = element.querySelectorAll(".c-button-set .icon-arrow-right"); - expect(pauseEl.length).toBe(0); - expect(playEl.length).toBe(0); - }); - - describe('pause and play controls', () => { - beforeEach(() => { - openmct.time.clock('local', { - start: -1000, - end: 100 - }); - - return Vue.nextTick(); - }); - - it('shows the pause controls', (done) => { - Vue.nextTick(() => { - let pauseEl = element.querySelectorAll(".c-button-set .icon-pause"); - expect(pauseEl.length).toBe(1); - done(); - }); - - }); - - it('shows the play control if plot is paused', (done) => { - let pauseEl = element.querySelector(".c-button-set .icon-pause"); - const clickEvent = createMouseEvent("click"); - - pauseEl.dispatchEvent(clickEvent); - Vue.nextTick(() => { - let playEl = element.querySelectorAll(".c-button-set .is-paused"); - expect(playEl.length).toBe(1); - done(); - }); - - }); - }); - - describe('resume actions on errant click', () => { - beforeEach(() => { - openmct.time.clock('local', { - start: -1000, - end: 100 - }); - - return Vue.nextTick(); - }); - - it("clicking the plot view without movement resumes the plot while active", async () => { - - const pauseEl = element.querySelectorAll(".c-button-set .icon-pause"); - // if the pause button is present, the chart is running - expect(pauseEl.length).toBe(1); - - // simulate an errant mouse click - // the second item is the canvas we need to use - const canvas = element.querySelectorAll("canvas")[1]; - const mouseDownEvent = new MouseEvent('mousedown'); - const mouseUpEvent = new MouseEvent('mouseup'); - canvas.dispatchEvent(mouseDownEvent); - // mouseup event is bound to the window - window.dispatchEvent(mouseUpEvent); - await Vue.nextTick(); - - const pauseElAfterClick = element.querySelectorAll(".c-button-set .icon-pause"); - console.log('pauseElAfterClick', pauseElAfterClick); - expect(pauseElAfterClick.length).toBe(1); - - }); - - it("clicking the plot view without movement leaves the plot paused", async () => { - - const pauseEl = element.querySelector(".c-button-set .icon-pause"); - // pause the plot - pauseEl.dispatchEvent(createMouseEvent('click')); - await Vue.nextTick(); - - const playEl = element.querySelectorAll('.c-button-set .is-paused'); - expect(playEl.length).toBe(1); - - // simulate an errant mouse click - // the second item is the canvas we need to use - const canvas = element.querySelectorAll("canvas")[1]; - const mouseDownEvent = new MouseEvent('mousedown'); - const mouseUpEvent = new MouseEvent('mouseup'); - canvas.dispatchEvent(mouseDownEvent); - // mouseup event is bound to the window - window.dispatchEvent(mouseUpEvent); - await Vue.nextTick(); - - const playElAfterChartClick = element.querySelectorAll(".c-button-set .is-paused"); - expect(playElAfterChartClick.length).toBe(1); - - }); - - it("clicking the plot does not request historical data", async () => { - expect(openmct.telemetry.request).toHaveBeenCalledTimes(2); - - // simulate an errant mouse click - // the second item is the canvas we need to use - const canvas = element.querySelectorAll("canvas")[1]; - const mouseDownEvent = new MouseEvent('mousedown'); - const mouseUpEvent = new MouseEvent('mouseup'); - canvas.dispatchEvent(mouseDownEvent); - // mouseup event is bound to the window - window.dispatchEvent(mouseUpEvent); - await Vue.nextTick(); - - expect(openmct.telemetry.request).toHaveBeenCalledTimes(2); - - }); - - describe('limits', () => { - - it('lines are not displayed by default', () => { - let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line"); - expect(limitEl.length).toBe(0); - }); - - it('lines are displayed when configuration is set to true', (done) => { - const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); - const config = configStore.get(configId); - config.yAxis.set('displayRange', { - min: 0, - max: 4 - }); - config.series.models[0].set('limitLines', true); - - Vue.nextTick(() => { - let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line"); - expect(limitEl.length).toBe(4); - done(); - }); - }); - }); - }); - - describe('controls in time strip view', () => { - - it('zoom controls are hidden', () => { - let pauseEl = element.querySelectorAll(".c-button-set .js-zoom"); - expect(pauseEl.length).toBe(0); - }); - - it('pan controls are hidden', () => { - let pauseEl = element.querySelectorAll(".c-button-set .js-pan"); - expect(pauseEl.length).toBe(0); - }); - - it('pause/play controls are hidden', () => { - let pauseEl = element.querySelectorAll(".c-button-set .js-pause"); - expect(pauseEl.length).toBe(0); - }); - - }); - }); - - describe('resizing the plot', () => { - let plotContainerResizeObserver; - let resizePromiseResolve; - let testTelemetryObject; - let applicableViews; - let plotViewProvider; - let plotView; - let resizePromise; - - beforeEach(() => { - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key", - name: "Some attribute", - hints: { - range: 1 - } - }, { - key: "some-other-key", - name: "Another attribute", - hints: { - range: 2 - } - }] - } - }; - - openmct.router.path = [testTelemetryObject]; - - applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); - plotView = plotViewProvider.view(testTelemetryObject, []); - - plotView.show(child, true); - - resizePromise = new Promise((resolve) => { - resizePromiseResolve = resolve; - }); - - const handlePlotResize = _.debounce(() => { - resizePromiseResolve(true); - }, 600); - - plotContainerResizeObserver = new ResizeObserver(handlePlotResize); - plotContainerResizeObserver.observe(plotView.getComponent().$children[0].$children[1].$parent.$refs.plotWrapper); - - return Vue.nextTick(() => { - plotView.getComponent().$children[0].$children[1].stopFollowingTimeContext(); - spyOn(plotView.getComponent().$children[0].$children[1], 'loadSeriesData').and.callThrough(); - }); - }); - - afterEach(() => { - plotContainerResizeObserver.disconnect(); - openmct.router.path = null; - }); - - it("requests historical data when over the threshold", (done) => { - element.style.width = '680px'; - resizePromise.then(() => { - expect(plotView.getComponent().$children[0].$children[1].loadSeriesData).toHaveBeenCalledTimes(1); - done(); - }); - }); - - it("does not request historical data when under the threshold", (done) => { - element.style.width = '644px'; - resizePromise.then(() => { - expect(plotView.getComponent().$children[0].$children[1].loadSeriesData).not.toHaveBeenCalled(); - done(); - }); - }); - }); - - describe('the inspector view', () => { - let component; - let viewComponentObject; - let mockComposition; - let testTelemetryObject; - let selection; - let config; - beforeEach((done) => { - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key", - name: "Some attribute", - hints: { - range: 1 - } - }, { - key: "some-other-key", - name: "Another attribute", - hints: { - range: 2 - } - }] - } - }; - - selection = [ - [ - { - context: { - item: { - id: "test-object", - identifier: { - key: "test-object", - namespace: '' - }, - type: "telemetry.plot.overlay", - configuration: { - series: [ - { - identifier: { - key: "test-object", - namespace: '' - } - } - ] - }, - composition: [] - } - } - }, - { - context: { - item: { - type: 'time-strip', - identifier: { - key: 'some-other-key', - namespace: '' - } - } - } - } - ] - ]; - - openmct.router.path = [testTelemetryObject]; - mockComposition = new EventEmitter(); - mockComposition.load = () => { - mockComposition.emit('add', testTelemetryObject); - - return [testTelemetryObject]; - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier); - config = new PlotConfigurationModel({ - id: configId, - domainObject: selection[0][0].context.item, - openmct: openmct - }); - configStore.add(configId, config); - - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - PlotOptions - }, - provide: { - openmct: openmct, - domainObject: selection[0][0].context.item, - path: [selection[0][0].context.item, selection[0][1].context.item] - }, - template: '' - }); - - Vue.nextTick(() => { - viewComponentObject = component.$root.$children[0]; - done(); - }); - }); - - afterEach(() => { - openmct.router.path = null; - }); - - describe('in view only mode', () => { - let browseOptionsEl; - let editOptionsEl; - beforeEach(() => { - browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); - editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); - }); - - it('does not show the edit options', () => { - expect(editOptionsEl).toBeNull(); - }); - - it('shows the name', () => { - const seriesEl = browseOptionsEl.querySelector('.c-object-label__name'); - expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name); - }); - - it('shows in collapsed mode', () => { - const seriesEl = browseOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); - expect(seriesEl.length).toEqual(0); - }); - - it('shows in expanded mode', () => { - let expandControl = browseOptionsEl.querySelector(".c-disclosure-triangle"); - const clickEvent = createMouseEvent("click"); - expandControl.dispatchEvent(clickEvent); - - const plotOptionsProperties = browseOptionsEl.querySelectorAll('.js-plot-options-browse-properties .grid-row'); - expect(plotOptionsProperties.length).toEqual(6); - }); - }); - - describe('in edit mode', () => { - let editOptionsEl; - let browseOptionsEl; - - beforeEach((done) => { - viewComponentObject.setEditState(true); - Vue.nextTick(() => { - editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); - browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); - done(); - }); - }); - - it('does not show the browse options', () => { - expect(browseOptionsEl).toBeNull(); - }); - - it('shows the name', () => { - const seriesEl = editOptionsEl.querySelector('.c-object-label__name'); - expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name); - }); - - it('shows in collapsed mode', () => { - const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); - expect(seriesEl.length).toEqual(0); - }); - - it('shows in collapsed mode', () => { - const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); - expect(seriesEl.length).toEqual(0); - }); - - it('renders expanded', () => { - const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle"); - const clickEvent = createMouseEvent("click"); - expandControl.dispatchEvent(clickEvent); - - const plotOptionsProperties = editOptionsEl.querySelectorAll(".js-plot-options-edit-properties .grid-row"); - expect(plotOptionsProperties.length).toEqual(8); - }); - - it('shows yKeyOptions', () => { - const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle"); - const clickEvent = createMouseEvent("click"); - expandControl.dispatchEvent(clickEvent); - - const plotOptionsProperties = editOptionsEl.querySelectorAll(".js-plot-options-edit-properties .grid-row"); - - const yKeySelection = plotOptionsProperties[0].querySelector('select'); - const options = Array.from(yKeySelection.options).map((option) => { - return option.value; - }); - expect(options).toEqual([testTelemetryObject.telemetry.values[1].key, testTelemetryObject.telemetry.values[2].key]); - }); - - it('shows yAxis options', () => { - const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle"); - const clickEvent = createMouseEvent("click"); - expandControl.dispatchEvent(clickEvent); - - const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part"); - - // TODO better test - expect(yAxisProperties.length).toEqual(2); - }); - - it('renders color palette options', () => { - const colorSwatch = editOptionsEl.querySelector(".c-click-swatch"); - expect(colorSwatch).toBeDefined(); - }); - }); + it('renders color palette options', () => { + const colorSwatch = editOptionsEl.querySelector('.c-click-swatch'); + expect(colorSwatch).toBeDefined(); + }); }); + }); }); diff --git a/src/plugins/plot/stackedPlot/StackedPlot.vue b/src/plugins/plot/stackedPlot/StackedPlot.vue index d6e3ba6677..c57cfa91eb 100644 --- a/src/plugins/plot/stackedPlot/StackedPlot.vue +++ b/src/plugins/plot/stackedPlot/StackedPlot.vue @@ -21,301 +21,308 @@ --> diff --git a/src/plugins/plot/stackedPlot/StackedPlotCompositionPolicy.js b/src/plugins/plot/stackedPlot/StackedPlotCompositionPolicy.js index 882b6c2a98..2d16188da8 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotCompositionPolicy.js +++ b/src/plugins/plot/stackedPlot/StackedPlotCompositionPolicy.js @@ -1,30 +1,33 @@ export default function StackedPlotCompositionPolicy(openmct) { - function hasNumericTelemetry(domainObject) { - const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); - if (!hasTelemetry) { - return false; - } - - let metadata = openmct.telemetry.getMetadata(domainObject); - - return metadata.values().length > 0 && hasDomainAndRange(metadata); + function hasNumericTelemetry(domainObject) { + const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); + if (!hasTelemetry) { + return false; } - function hasDomainAndRange(metadata) { - return (metadata.valuesForHints(['range']).length > 0 - && metadata.valuesForHints(['domain']).length > 0); + let metadata = openmct.telemetry.getMetadata(domainObject); + + return metadata.values().length > 0 && hasDomainAndRange(metadata); + } + + function hasDomainAndRange(metadata) { + return ( + metadata.valuesForHints(['range']).length > 0 && + metadata.valuesForHints(['domain']).length > 0 + ); + } + + return { + allow: function (parent, child) { + if ( + parent.type === 'telemetry.plot.stacked' && + child.type !== 'telemetry.plot.overlay' && + hasNumericTelemetry(child) === false + ) { + return false; + } + + return true; } - - return { - allow: function (parent, child) { - - if ((parent.type === 'telemetry.plot.stacked') - && ((child.type !== 'telemetry.plot.overlay') && (hasNumericTelemetry(child) === false)) - ) { - return false; - } - - return true; - } - }; + }; } diff --git a/src/plugins/plot/stackedPlot/StackedPlotItem.vue b/src/plugins/plot/stackedPlot/StackedPlotItem.vue index 491b30f41b..19c6bca6ff 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotItem.vue +++ b/src/plugins/plot/stackedPlot/StackedPlotItem.vue @@ -20,209 +20,206 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js b/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js index 85f7aceaea..cd67d655e6 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js +++ b/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js @@ -24,63 +24,63 @@ import StackedPlot from './StackedPlot.vue'; import Vue from 'vue'; export default function StackedPlotViewProvider(openmct) { - function isCompactView(objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); + function isCompactView(objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip'); - return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); - } + return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); + } - return { - key: 'plot-stacked', - name: 'Stacked Plot', - cssClass: 'icon-telemetry', - canView(domainObject, objectPath) { - return domainObject.type === 'telemetry.plot.stacked'; - }, + return { + key: 'plot-stacked', + name: 'Stacked Plot', + cssClass: 'icon-telemetry', + canView(domainObject, objectPath) { + return domainObject.type === 'telemetry.plot.stacked'; + }, - canEdit(domainObject, objectPath) { - return domainObject.type === 'telemetry.plot.stacked'; - }, + canEdit(domainObject, objectPath) { + return domainObject.type === 'telemetry.plot.stacked'; + }, - view: function (domainObject, objectPath) { - let component; + view: function (domainObject, objectPath) { + let component; - return { - show: function (element) { - let isCompact = isCompactView(objectPath); + return { + show: function (element) { + let isCompact = isCompactView(objectPath); - component = new Vue({ - el: element, - components: { - StackedPlot - }, - provide: { - openmct, - domainObject, - path: objectPath - }, - data() { - return { - options: { - compact: isCompact - } - }; - }, - template: '' - }); - }, - getViewContext() { - if (!component) { - return {}; - } - - return component.$refs.plotComponent.getViewContext(); - }, - destroy: function () { - component.$destroy(); - component = undefined; + component = new Vue({ + el: element, + components: { + StackedPlot + }, + provide: { + openmct, + domainObject, + path: objectPath + }, + data() { + return { + options: { + compact: isCompact } - }; + }; + }, + template: '' + }); + }, + getViewContext() { + if (!component) { + return {}; + } + + return component.$refs.plotComponent.getViewContext(); + }, + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js b/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js index c917a6b500..698312688a 100644 --- a/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js +++ b/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js @@ -20,118 +20,141 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import StyleRuleManager from "@/plugins/condition/StyleRuleManager"; -import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants"; +import StyleRuleManager from '@/plugins/condition/StyleRuleManager'; +import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants'; export default { - inject: ['openmct', 'domainObject', 'path'], - data() { - return { - objectStyle: undefined - }; - }, - mounted() { - this.objectStyles = this.getObjectStyleForItem(this.childObject.configuration); - this.initObjectStyles(); - }, - beforeDestroy() { - if (this.stopListeningStyles) { - this.stopListeningStyles(); - } - - if (this.styleRuleManager) { - this.styleRuleManager.destroy(); - } - }, - methods: { - getObjectStyleForItem(config) { - if (config && config.objectStyles) { - return config.objectStyles ? Object.assign({}, config.objectStyles) : undefined; - } else { - return undefined; - } - }, - initObjectStyles() { - if (!this.styleRuleManager) { - this.styleRuleManager = new StyleRuleManager(this.objectStyles, this.openmct, this.updateStyle.bind(this), true); - } else { - this.styleRuleManager.updateObjectStyleConfig(this.objectStyles); - } - - if (this.stopListeningStyles) { - this.stopListeningStyles(); - } - - this.stopListeningStyles = this.openmct.objects.observe(this.childObject, 'configuration.objectStyles', (newObjectStyle) => { - //Updating styles in the inspector view will trigger this so that the changes are reflected immediately - this.styleRuleManager.updateObjectStyleConfig(newObjectStyle); - }); - - if (this.childObject && this.childObject.configuration && this.childObject.configuration.fontStyle) { - const { fontSize, font } = this.childObject.configuration.fontStyle; - this.setFontSize(fontSize); - this.setFont(font); - } - - this.stopListeningFontStyles = this.openmct.objects.observe(this.childObject, 'configuration.fontStyle', (newFontStyle) => { - this.setFontSize(newFontStyle.fontSize); - this.setFont(newFontStyle.font); - }); - }, - getStyleReceiver() { - let styleReceiver; - - if (this.$el !== undefined) { - styleReceiver = this.$el.querySelector('.js-style-receiver') - || this.$el.querySelector(':first-child'); - - if (styleReceiver === null) { - styleReceiver = undefined; - } - } - - return styleReceiver; - }, - setFontSize(newSize) { - let elemToStyle = this.getStyleReceiver(); - - if (elemToStyle !== undefined) { - elemToStyle.dataset.fontSize = newSize; - } - }, - setFont(newFont) { - let elemToStyle = this.getStyleReceiver(); - - if (elemToStyle !== undefined) { - elemToStyle.dataset.font = newFont; - } - }, - updateStyle(styleObj) { - let elemToStyle = this.getStyleReceiver(); - - if (!styleObj || elemToStyle === undefined) { - return; - } - - let keys = Object.keys(styleObj); - - keys.forEach(key => { - if (elemToStyle) { - if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) { - if (elemToStyle.style[key]) { - elemToStyle.style[key] = ''; - } - } else { - if (!styleObj.isStyleInvisible && elemToStyle.classList.contains(STYLE_CONSTANTS.isStyleInvisible)) { - elemToStyle.classList.remove(STYLE_CONSTANTS.isStyleInvisible); - } else if (styleObj.isStyleInvisible && !elemToStyle.classList.contains(styleObj.isStyleInvisible)) { - elemToStyle.classList.add(styleObj.isStyleInvisible); - } - - elemToStyle.style[key] = styleObj[key]; - } - } - }); - } + inject: ['openmct', 'domainObject', 'path'], + data() { + return { + objectStyle: undefined + }; + }, + mounted() { + this.objectStyles = this.getObjectStyleForItem(this.childObject.configuration); + this.initObjectStyles(); + }, + beforeDestroy() { + if (this.stopListeningStyles) { + this.stopListeningStyles(); } + + if (this.styleRuleManager) { + this.styleRuleManager.destroy(); + } + }, + methods: { + getObjectStyleForItem(config) { + if (config && config.objectStyles) { + return config.objectStyles ? Object.assign({}, config.objectStyles) : undefined; + } else { + return undefined; + } + }, + initObjectStyles() { + if (!this.styleRuleManager) { + this.styleRuleManager = new StyleRuleManager( + this.objectStyles, + this.openmct, + this.updateStyle.bind(this), + true + ); + } else { + this.styleRuleManager.updateObjectStyleConfig(this.objectStyles); + } + + if (this.stopListeningStyles) { + this.stopListeningStyles(); + } + + this.stopListeningStyles = this.openmct.objects.observe( + this.childObject, + 'configuration.objectStyles', + (newObjectStyle) => { + //Updating styles in the inspector view will trigger this so that the changes are reflected immediately + this.styleRuleManager.updateObjectStyleConfig(newObjectStyle); + } + ); + + if ( + this.childObject && + this.childObject.configuration && + this.childObject.configuration.fontStyle + ) { + const { fontSize, font } = this.childObject.configuration.fontStyle; + this.setFontSize(fontSize); + this.setFont(font); + } + + this.stopListeningFontStyles = this.openmct.objects.observe( + this.childObject, + 'configuration.fontStyle', + (newFontStyle) => { + this.setFontSize(newFontStyle.fontSize); + this.setFont(newFontStyle.font); + } + ); + }, + getStyleReceiver() { + let styleReceiver; + + if (this.$el !== undefined) { + styleReceiver = + this.$el.querySelector('.js-style-receiver') || this.$el.querySelector(':first-child'); + + if (styleReceiver === null) { + styleReceiver = undefined; + } + } + + return styleReceiver; + }, + setFontSize(newSize) { + let elemToStyle = this.getStyleReceiver(); + + if (elemToStyle !== undefined) { + elemToStyle.dataset.fontSize = newSize; + } + }, + setFont(newFont) { + let elemToStyle = this.getStyleReceiver(); + + if (elemToStyle !== undefined) { + elemToStyle.dataset.font = newFont; + } + }, + updateStyle(styleObj) { + let elemToStyle = this.getStyleReceiver(); + + if (!styleObj || elemToStyle === undefined) { + return; + } + + let keys = Object.keys(styleObj); + + keys.forEach((key) => { + if (elemToStyle) { + if (typeof styleObj[key] === 'string' && styleObj[key].indexOf('__no_value') > -1) { + if (elemToStyle.style[key]) { + elemToStyle.style[key] = ''; + } + } else { + if ( + !styleObj.isStyleInvisible && + elemToStyle.classList.contains(STYLE_CONSTANTS.isStyleInvisible) + ) { + elemToStyle.classList.remove(STYLE_CONSTANTS.isStyleInvisible); + } else if ( + styleObj.isStyleInvisible && + !elemToStyle.classList.contains(styleObj.isStyleInvisible) + ) { + elemToStyle.classList.add(styleObj.isStyleInvisible); + } + + elemToStyle.style[key] = styleObj[key]; + } + } + }); + } + } }; diff --git a/src/plugins/plot/stackedPlot/pluginSpec.js b/src/plugins/plot/stackedPlot/pluginSpec.js index 4bb9c7f58d..d6e5da4a69 100644 --- a/src/plugins/plot/stackedPlot/pluginSpec.js +++ b/src/plugins/plot/stackedPlot/pluginSpec.js @@ -20,768 +20,799 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing"; -import PlotVuePlugin from "../plugin"; -import Vue from "vue"; -import StackedPlot from "./StackedPlot.vue"; -import configStore from "../configuration/ConfigStore"; -import EventEmitter from "EventEmitter"; -import PlotConfigurationModel from "../configuration/PlotConfigurationModel"; -import PlotOptions from "../inspector/PlotOptions.vue"; +import { + createMouseEvent, + createOpenMct, + resetApplicationState, + spyOnBuiltins +} from 'utils/testing'; +import PlotVuePlugin from '../plugin'; +import Vue from 'vue'; +import StackedPlot from './StackedPlot.vue'; +import configStore from '../configuration/ConfigStore'; +import EventEmitter from 'EventEmitter'; +import PlotConfigurationModel from '../configuration/PlotConfigurationModel'; +import PlotOptions from '../inspector/PlotOptions.vue'; -describe("the plugin", function () { - let element; - let child; - let openmct; - let telemetryPromise; - let telemetryPromiseResolve; - let mockObjectPath; - let stackedPlotObject = { +describe('the plugin', function () { + let element; + let child; + let openmct; + let telemetryPromise; + let telemetryPromiseResolve; + let mockObjectPath; + let stackedPlotObject = { + identifier: { + namespace: '', + key: 'test-plot' + }, + type: 'telemetry.plot.stacked', + name: 'Test Stacked Plot', + configuration: { + series: [] + } + }; + + beforeEach((done) => { + mockObjectPath = [ + { + name: 'mock folder', + type: 'fake-folder', identifier: { - namespace: "", - key: "test-plot" - }, - type: "telemetry.plot.stacked", - name: "Test Stacked Plot", - configuration: { - series: [] + key: 'mock-folder', + namespace: '' } + }, + { + name: 'mock parent folder', + type: 'time-strip', + identifier: { + key: 'mock-parent-folder', + namespace: '' + } + } + ]; + const testTelemetry = [ + { + utc: 1, + 'some-key': 'some-value 1', + 'some-other-key': 'some-other-value 1' + }, + { + utc: 2, + 'some-key': 'some-value 2', + 'some-other-key': 'some-other-value 2' + }, + { + utc: 3, + 'some-key': 'some-value 3', + 'some-other-key': 'some-other-value 3' + } + ]; + + const timeSystem = { + timeSystemKey: 'utc', + bounds: { + start: 0, + end: 4 + } }; - beforeEach((done) => { - mockObjectPath = [ - { - name: 'mock folder', - type: 'fake-folder', - identifier: { - key: 'mock-folder', - namespace: '' - } - }, - { - name: 'mock parent folder', - type: 'time-strip', - identifier: { - key: 'mock-parent-folder', - namespace: '' - } - } - ]; - const testTelemetry = [ - { - 'utc': 1, - 'some-key': 'some-value 1', - 'some-other-key': 'some-other-value 1' - }, - { - 'utc': 2, - 'some-key': 'some-value 2', - 'some-other-key': 'some-other-value 2' - }, - { - 'utc': 3, - 'some-key': 'some-value 3', - 'some-other-key': 'some-other-value 3' - } - ]; + openmct = createOpenMct(timeSystem); - const timeSystem = { - timeSystemKey: 'utc', - bounds: { - start: 0, - end: 4 - } - }; - - openmct = createOpenMct(timeSystem); - - telemetryPromise = new Promise((resolve) => { - telemetryPromiseResolve = resolve; - }); - - spyOn(openmct.telemetry, 'request').and.callFake(() => { - telemetryPromiseResolve(testTelemetry); - - return telemetryPromise; - }); - - openmct.install(new PlotVuePlugin()); - - element = document.createElement("div"); - element.style.width = "640px"; - element.style.height = "480px"; - child = document.createElement("div"); - child.style.width = "640px"; - child.style.height = "480px"; - element.appendChild(child); - document.body.appendChild(element); - - spyOn(window, 'ResizeObserver').and.returnValue({ - observe() {}, - unobserve() {}, - disconnect() {} - }); - - openmct.types.addType("test-object", { - creatable: true - }); - - spyOnBuiltins(["requestAnimationFrame"]); - window.requestAnimationFrame.and.callFake((callBack) => { - callBack(); - }); - - openmct.router.path = [stackedPlotObject]; - openmct.on("start", done); - openmct.startHeadless(); + telemetryPromise = new Promise((resolve) => { + telemetryPromiseResolve = resolve; }); - afterEach((done) => { - openmct.time.timeSystem('utc', { - start: 0, - end: 1 - }); - configStore.deleteAll(); - resetApplicationState(openmct).then(done).catch(done); + spyOn(openmct.telemetry, 'request').and.callFake(() => { + telemetryPromiseResolve(testTelemetry); + + return telemetryPromise; }); + openmct.install(new PlotVuePlugin()); + + element = document.createElement('div'); + element.style.width = '640px'; + element.style.height = '480px'; + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + element.appendChild(child); + document.body.appendChild(element); + + spyOn(window, 'ResizeObserver').and.returnValue({ + observe() {}, + unobserve() {}, + disconnect() {} + }); + + openmct.types.addType('test-object', { + creatable: true + }); + + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((callBack) => { + callBack(); + }); + + openmct.router.path = [stackedPlotObject]; + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach((done) => { + openmct.time.timeSystem('utc', { + start: 0, + end: 1 + }); + configStore.deleteAll(); + resetApplicationState(openmct).then(done).catch(done); + }); + + afterAll(() => { + openmct.router.path = null; + }); + + describe('the plot views', () => { + it('provides a stacked plot view for objects with telemetry', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'telemetry.plot.stacked', + telemetry: { + values: [ + { + key: 'some-key' + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + let plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-stacked'); + expect(plotView).toBeDefined(); + }); + }); + + describe('The stacked plot view', () => { + let testTelemetryObject; + let testTelemetryObject2; + let config; + let component; + let mockCompositionList = []; + let plotViewComponentObject; + afterAll(() => { - openmct.router.path = null; + openmct.router.path = null; }); - describe("the plot views", () => { - it("provides a stacked plot view for objects with telemetry", () => { - const testTelemetryObject = { - id: "test-object", - type: "telemetry.plot.stacked", - telemetry: { - values: [{ - key: "some-key" - }] + beforeEach(() => { + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key', + name: 'Some attribute', + hints: { + range: 1 + } + }, + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 2 + } + } + ] + }, + configuration: { + objectStyles: { + staticStyle: { + style: { + backgroundColor: 'rgb(0, 200, 0)', + color: '', + border: '' + } + }, + conditionSetIdentifier: { + namespace: '', + key: 'testConditionSetId' + }, + selectedConditionId: 'conditionId1', + defaultConditionId: 'conditionId1', + styles: [ + { + conditionId: 'conditionId1', + style: { + backgroundColor: 'rgb(0, 155, 0)', + color: '', + output: '', + border: '' } - }; + } + ] + } + } + }; - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked"); - expect(plotView).toBeDefined(); - }); + testTelemetryObject2 = { + identifier: { + namespace: '', + key: 'test-object2' + }, + type: 'test-object', + name: 'Test Object2', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key2', + name: 'Some attribute2', + hints: { + range: 1 + } + }, + { + key: 'some-other-key2', + name: 'Another attribute2', + hints: { + range: 2 + } + } + ] + } + }; + stackedPlotObject.composition = [ + { + identifier: testTelemetryObject.identifier + } + ]; + + mockCompositionList = []; + spyOn(openmct.composition, 'get').and.callFake((domainObject) => { + //We need unique compositions here - one for the StackedPlot view and one for the PlotLegend view + const numObjects = domainObject.composition.length; + const mockComposition = new EventEmitter(); + mockComposition.load = () => { + if (numObjects === 1) { + mockComposition.emit('add', testTelemetryObject); + + return [testTelemetryObject]; + } else if (numObjects === 2) { + mockComposition.emit('add', testTelemetryObject); + mockComposition.emit('add', testTelemetryObject2); + + return [testTelemetryObject, testTelemetryObject2]; + } else { + return []; + } + }; + + mockCompositionList.push(mockComposition); + + return mockComposition; + }); + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + StackedPlot + }, + provide: { + openmct: openmct, + domainObject: stackedPlotObject, + path: [stackedPlotObject] + }, + template: '' + }); + + return telemetryPromise.then(Vue.nextTick()).then(() => { + plotViewComponentObject = component.$root.$children[0]; + const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); + config = configStore.get(configId); + }); }); - describe("The stacked plot view", () => { - let testTelemetryObject; - let testTelemetryObject2; - let config; - let component; - let mockCompositionList = []; - let plotViewComponentObject; + it('Renders a collapsed legend for every telemetry', () => { + let legend = element.querySelectorAll('.plot-wrapper-collapsed-legend .plot-series-name'); + expect(legend.length).toBe(1); + expect(legend[0].innerHTML).toEqual('Test Object'); + }); - afterAll(() => { - openmct.router.path = null; + it('Renders an expanded legend for every telemetry', () => { + let legendControl = element.querySelector( + '.c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle' + ); + const clickEvent = createMouseEvent('click'); + + legendControl.dispatchEvent(clickEvent); + + let legend = element.querySelectorAll('.plot-wrapper-expanded-legend .plot-legend-item td'); + expect(legend.length).toBe(6); + }); + + // disable due to flakiness + xit('Renders X-axis ticks for the telemetry object', () => { + let xAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper' + ); + expect(xAxisElement.length).toBe(1); + + config.xAxis.set('displayRange', { + min: 0, + max: 4 + }); + let ticks = xAxisElement[0].querySelectorAll('.gl-plot-tick'); + expect(ticks.length).toBe(9); + }); + + it('Renders Y-axis ticks for the telemetry object', (done) => { + config.yAxis.set('displayRange', { + min: 10, + max: 20 + }); + Vue.nextTick(() => { + let yAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper' + ); + expect(yAxisElement.length).toBe(1); + let ticks = yAxisElement[0].querySelectorAll('.gl-plot-tick'); + expect(ticks.length).toBe(6); + done(); + }); + }); + + it('Renders Y-axis options for the telemetry object', () => { + let yAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select' + ); + expect(yAxisElement.length).toBe(1); + let options = yAxisElement[0].querySelectorAll('option'); + expect(options.length).toBe(2); + expect(options[0].value).toBe('Some attribute'); + expect(options[1].value).toBe('Another attribute'); + }); + + it('turns on cursor Guides all telemetry objects', (done) => { + expect(plotViewComponentObject.cursorGuide).toBeFalse(); + plotViewComponentObject.cursorGuide = true; + Vue.nextTick(() => { + let childCursorGuides = element.querySelectorAll('.c-cursor-guide--v'); + expect(childCursorGuides.length).toBe(1); + done(); + }); + }); + + it('shows grid lines for all telemetry objects', () => { + expect(plotViewComponentObject.gridLines).toBeTrue(); + let gridLinesContainer = element.querySelectorAll('.gl-plot-display-area .js-ticks'); + let visible = 0; + gridLinesContainer.forEach((el) => { + if (el.style.display !== 'none') { + visible++; + } + }); + expect(visible).toBe(2); + }); + + it('hides grid lines for all telemetry objects', (done) => { + expect(plotViewComponentObject.gridLines).toBeTrue(); + plotViewComponentObject.gridLines = false; + Vue.nextTick(() => { + expect(plotViewComponentObject.gridLines).toBeFalse(); + let gridLinesContainer = element.querySelectorAll('.gl-plot-display-area .js-ticks'); + let visible = 0; + gridLinesContainer.forEach((el) => { + if (el.style.display !== 'none') { + visible++; + } }); + expect(visible).toBe(0); + done(); + }); + }); - beforeEach(() => { - testTelemetryObject = { + it('plots a new series when a new telemetry object is added', (done) => { + //setting composition here so that any new triggers to composition.load with correctly load the mockComposition in the beforeEach + stackedPlotObject.composition = [testTelemetryObject, testTelemetryObject2]; + mockCompositionList[0].emit('add', testTelemetryObject2); + + Vue.nextTick(() => { + let legend = element.querySelectorAll('.plot-wrapper-collapsed-legend .plot-series-name'); + expect(legend.length).toBe(2); + expect(legend[1].innerHTML).toEqual('Test Object2'); + done(); + }); + }); + + it('removes plots from series when a telemetry object is removed', (done) => { + stackedPlotObject.composition = []; + mockCompositionList[0].emit('remove', testTelemetryObject.identifier); + Vue.nextTick(() => { + expect(plotViewComponentObject.compositionObjects.length).toBe(0); + done(); + }); + }); + + it('Changes the label of the y axis when the option changes', (done) => { + let selectEl = element.querySelector('.gl-plot-y-label__select'); + selectEl.value = 'Another attribute'; + selectEl.dispatchEvent(new Event('change')); + + Vue.nextTick(() => { + expect(config.yAxis.get('label')).toEqual('Another attribute'); + done(); + }); + }); + + it('Adds a new point to the plot', (done) => { + let originalLength = config.series.models[0].getSeriesData().length; + config.series.models[0].add({ + utc: 2, + 'some-key': 1, + 'some-other-key': 2 + }); + Vue.nextTick(() => { + const seriesData = config.series.models[0].getSeriesData(); + expect(seriesData.length).toEqual(originalLength + 1); + done(); + }); + }); + + it('updates the xscale', (done) => { + config.xAxis.set('displayRange', { + min: 0, + max: 10 + }); + Vue.nextTick(() => { + expect(plotViewComponentObject.$children[0].component.$children[1].xScale.domain()).toEqual( + { + min: 0, + max: 10 + } + ); + done(); + }); + }); + + it('updates the yscale', (done) => { + const yAxisList = [config.yAxis, ...config.additionalYAxes]; + yAxisList.forEach((yAxis) => { + yAxis.set('displayRange', { + min: 10, + max: 20 + }); + }); + Vue.nextTick(() => { + const yAxesScales = plotViewComponentObject.$children[0].component.$children[1].yScale; + yAxesScales.forEach((yAxisScale) => { + expect(yAxisScale.scale.domain()).toEqual({ + min: 10, + max: 20 + }); + }); + done(); + }); + }); + + it('shows styles for telemetry objects if available', (done) => { + Vue.nextTick(() => { + let conditionalStylesContainer = element.querySelectorAll( + '.c-plot--stacked-container .js-style-receiver' + ); + let hasStyles = 0; + conditionalStylesContainer.forEach((el) => { + if (el.style.backgroundColor !== '') { + hasStyles++; + } + }); + expect(hasStyles).toBe(1); + done(); + }); + }); + }); + + describe('the stacked plot inspector view', () => { + let component; + let viewComponentObject; + let mockComposition; + let testTelemetryObject; + let selection; + let config; + beforeEach((done) => { + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key', + name: 'Some attribute', + hints: { + range: 1 + } + }, + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 2 + } + } + ] + } + }; + + selection = [ + [ + { + context: { + item: { + type: 'telemetry.plot.stacked', identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key", - name: "Some attribute", - hints: { - range: 1 - } - }, { - key: "some-other-key", - name: "Another attribute", - hints: { - range: 2 - } - }] + key: 'some-stacked-plot', + namespace: '' }, configuration: { - objectStyles: { - staticStyle: { - style: { - backgroundColor: 'rgb(0, 200, 0)', - color: '', - border: '' - } - }, - conditionSetIdentifier: { - namespace: '', - key: 'testConditionSetId' - }, - selectedConditionId: 'conditionId1', - defaultConditionId: 'conditionId1', - styles: [ - { - conditionId: 'conditionId1', - style: { - backgroundColor: 'rgb(0, 155, 0)', - color: '', - output: '', - border: '' - } - } - ] - } + series: [] } - }; + } + } + } + ] + ]; - testTelemetryObject2 = { - identifier: { - namespace: "", - key: "test-object2" - }, - type: "test-object", - name: "Test Object2", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key2", - name: "Some attribute2", - hints: { - range: 1 - } - }, { - key: "some-other-key2", - name: "Another attribute2", - hints: { - range: 2 - } - }] - } - }; + openmct.router.path = [testTelemetryObject]; + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testTelemetryObject); - stackedPlotObject.composition = [{ - identifier: testTelemetryObject.identifier - }]; + return [testTelemetryObject]; + }; - mockCompositionList = []; - spyOn(openmct.composition, 'get').and.callFake((domainObject) => { - //We need unique compositions here - one for the StackedPlot view and one for the PlotLegend view - const numObjects = domainObject.composition.length; - const mockComposition = new EventEmitter(); - mockComposition.load = () => { - if (numObjects === 1) { - mockComposition.emit('add', testTelemetryObject); + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - return [testTelemetryObject]; - } else if (numObjects === 2) { - mockComposition.emit('add', testTelemetryObject); - mockComposition.emit('add', testTelemetryObject2); + const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier); + config = new PlotConfigurationModel({ + id: configId, + domainObject: selection[0][0].context.item, + openmct: openmct + }); + configStore.add(configId, config); - return [testTelemetryObject, testTelemetryObject2]; - } else { - return []; - } - }; + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + PlotOptions + }, + provide: { + openmct: openmct, + domainObject: selection[0][0].context.item, + path: [selection[0][0].context.item] + }, + template: '' + }); - mockCompositionList.push(mockComposition); - - return mockComposition; - }); - - let viewContainer = document.createElement("div"); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - StackedPlot - }, - provide: { - openmct: openmct, - domainObject: stackedPlotObject, - path: [stackedPlotObject] - }, - template: "" - }); - - return telemetryPromise - .then(Vue.nextTick()) - .then(() => { - plotViewComponentObject = component.$root.$children[0]; - const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); - config = configStore.get(configId); - }); - }); - - it("Renders a collapsed legend for every telemetry", () => { - let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name"); - expect(legend.length).toBe(1); - expect(legend[0].innerHTML).toEqual("Test Object"); - }); - - it("Renders an expanded legend for every telemetry", () => { - let legendControl = element.querySelector(".c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle"); - const clickEvent = createMouseEvent("click"); - - legendControl.dispatchEvent(clickEvent); - - let legend = element.querySelectorAll(".plot-wrapper-expanded-legend .plot-legend-item td"); - expect(legend.length).toBe(6); - }); - - // disable due to flakiness - xit("Renders X-axis ticks for the telemetry object", () => { - let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper"); - expect(xAxisElement.length).toBe(1); - - config.xAxis.set('displayRange', { - min: 0, - max: 4 - }); - let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick"); - expect(ticks.length).toBe(9); - }); - - it("Renders Y-axis ticks for the telemetry object", (done) => { - config.yAxis.set('displayRange', { - min: 10, - max: 20 - }); - Vue.nextTick(() => { - let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper"); - expect(yAxisElement.length).toBe(1); - let ticks = yAxisElement[0].querySelectorAll(".gl-plot-tick"); - expect(ticks.length).toBe(6); - done(); - }); - }); - - it("Renders Y-axis options for the telemetry object", () => { - let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select"); - expect(yAxisElement.length).toBe(1); - let options = yAxisElement[0].querySelectorAll("option"); - expect(options.length).toBe(2); - expect(options[0].value).toBe("Some attribute"); - expect(options[1].value).toBe("Another attribute"); - }); - - it("turns on cursor Guides all telemetry objects", (done) => { - expect(plotViewComponentObject.cursorGuide).toBeFalse(); - plotViewComponentObject.cursorGuide = true; - Vue.nextTick(() => { - let childCursorGuides = element.querySelectorAll(".c-cursor-guide--v"); - expect(childCursorGuides.length).toBe(1); - done(); - }); - }); - - it("shows grid lines for all telemetry objects", () => { - expect(plotViewComponentObject.gridLines).toBeTrue(); - let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks"); - let visible = 0; - gridLinesContainer.forEach(el => { - if (el.style.display !== "none") { - visible++; - } - }); - expect(visible).toBe(2); - }); - - it("hides grid lines for all telemetry objects", (done) => { - expect(plotViewComponentObject.gridLines).toBeTrue(); - plotViewComponentObject.gridLines = false; - Vue.nextTick(() => { - expect(plotViewComponentObject.gridLines).toBeFalse(); - let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks"); - let visible = 0; - gridLinesContainer.forEach(el => { - if (el.style.display !== "none") { - visible++; - } - }); - expect(visible).toBe(0); - done(); - }); - }); - - it('plots a new series when a new telemetry object is added', (done) => { - //setting composition here so that any new triggers to composition.load with correctly load the mockComposition in the beforeEach - stackedPlotObject.composition = [testTelemetryObject, testTelemetryObject2]; - mockCompositionList[0].emit('add', testTelemetryObject2); - - Vue.nextTick(() => { - let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name"); - expect(legend.length).toBe(2); - expect(legend[1].innerHTML).toEqual("Test Object2"); - done(); - }); - - }); - - it('removes plots from series when a telemetry object is removed', (done) => { - stackedPlotObject.composition = []; - mockCompositionList[0].emit('remove', testTelemetryObject.identifier); - Vue.nextTick(() => { - expect(plotViewComponentObject.compositionObjects.length).toBe(0); - done(); - }); - }); - - it("Changes the label of the y axis when the option changes", (done) => { - let selectEl = element.querySelector('.gl-plot-y-label__select'); - selectEl.value = 'Another attribute'; - selectEl.dispatchEvent(new Event("change")); - - Vue.nextTick(() => { - expect(config.yAxis.get('label')).toEqual('Another attribute'); - done(); - }); - }); - - it("Adds a new point to the plot", (done) => { - let originalLength = config.series.models[0].getSeriesData().length; - config.series.models[0].add({ - utc: 2, - 'some-key': 1, - 'some-other-key': 2 - }); - Vue.nextTick(() => { - const seriesData = config.series.models[0].getSeriesData(); - expect(seriesData.length).toEqual(originalLength + 1); - done(); - }); - }); - - it("updates the xscale", (done) => { - config.xAxis.set('displayRange', { - min: 0, - max: 10 - }); - Vue.nextTick(() => { - expect(plotViewComponentObject.$children[0].component.$children[1].xScale.domain()).toEqual({ - min: 0, - max: 10 - }); - done(); - }); - }); - - it("updates the yscale", (done) => { - const yAxisList = [config.yAxis, ...config.additionalYAxes]; - yAxisList.forEach((yAxis) => { - yAxis.set('displayRange', { - min: 10, - max: 20 - }); - }); - Vue.nextTick(() => { - const yAxesScales = plotViewComponentObject.$children[0].component.$children[1].yScale; - yAxesScales.forEach((yAxisScale) => { - expect(yAxisScale.scale.domain()).toEqual({ - min: 10, - max: 20 - }); - }); - done(); - }); - }); - - it("shows styles for telemetry objects if available", (done) => { - Vue.nextTick(() => { - let conditionalStylesContainer = element.querySelectorAll(".c-plot--stacked-container .js-style-receiver"); - let hasStyles = 0; - conditionalStylesContainer.forEach(el => { - if (el.style.backgroundColor !== '') { - hasStyles++; - } - }); - expect(hasStyles).toBe(1); - done(); - }); - }); + Vue.nextTick(() => { + viewComponentObject = component.$root.$children[0]; + done(); + }); }); - describe('the stacked plot inspector view', () => { - let component; - let viewComponentObject; - let mockComposition; - let testTelemetryObject; - let selection; - let config; - beforeEach((done) => { - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key", - name: "Some attribute", - hints: { - range: 1 - } - }, { - key: "some-other-key", - name: "Another attribute", - hints: { - range: 2 - } - }] - } - }; - - selection = [ - [ - { - context: { - item: { - type: 'telemetry.plot.stacked', - identifier: { - key: 'some-stacked-plot', - namespace: '' - }, - configuration: { - series: [] - } - } - } - } - ] - ]; - - openmct.router.path = [testTelemetryObject]; - mockComposition = new EventEmitter(); - mockComposition.load = () => { - mockComposition.emit('add', testTelemetryObject); - - return [testTelemetryObject]; - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier); - config = new PlotConfigurationModel({ - id: configId, - domainObject: selection[0][0].context.item, - openmct: openmct - }); - configStore.add(configId, config); - - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - PlotOptions - }, - provide: { - openmct: openmct, - domainObject: selection[0][0].context.item, - path: [selection[0][0].context.item] - }, - template: '' - }); - - Vue.nextTick(() => { - viewComponentObject = component.$root.$children[0]; - done(); - }); - }); - - afterEach(() => { - openmct.router.path = null; - }); - - describe('in view only mode', () => { - let browseOptionsEl; - beforeEach(() => { - browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); - }); - - it('shows legend properties', () => { - const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties'); - expect(legendPropertiesEl).not.toBeNull(); - }); - - it('does not show series properties', () => { - const seriesPropertiesEl = browseOptionsEl.querySelector('.c-tree'); - expect(seriesPropertiesEl).toBeNull(); - }); - - it('does not show yaxis properties', () => { - const yAxisPropertiesEl = browseOptionsEl.querySelector('.js-yaxis-properties'); - expect(yAxisPropertiesEl).toBeNull(); - }); - }); - + afterEach(() => { + openmct.router.path = null; }); - describe('inspector view of stacked plot child', () => { - let component; - let viewComponentObject; - let mockComposition; - let testTelemetryObject; - let selection; - let config; - beforeEach((done) => { - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key", - name: "Some attribute", - hints: { - range: 1 - } - }, { - key: "some-other-key", - name: "Another attribute", - hints: { - range: 2 - } - }] - } - }; + describe('in view only mode', () => { + let browseOptionsEl; + beforeEach(() => { + browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); + }); - selection = [ - [ - { - context: { - item: { - id: "test-object", - identifier: { - key: "test-object", - namespace: '' - }, - type: "telemetry.plot.overlay", - configuration: { - series: [ - { - identifier: { - key: "test-object", - namespace: '' - } - } - ] - }, - composition: [] - } - } - }, - { - context: { - item: { - type: 'telemetry.plot.stacked', - identifier: { - key: 'some-stacked-plot', - namespace: '' - }, - configuration: { - series: [] - } - } - } - } - ] - ]; + it('shows legend properties', () => { + const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties'); + expect(legendPropertiesEl).not.toBeNull(); + }); - openmct.router.path = [testTelemetryObject]; - mockComposition = new EventEmitter(); - mockComposition.load = () => { - mockComposition.emit('add', testTelemetryObject); - - return [testTelemetryObject]; - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier); - config = new PlotConfigurationModel({ - id: configId, - domainObject: selection[0][0].context.item, - openmct: openmct - }); - configStore.add(configId, config); - - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - PlotOptions - }, - provide: { - openmct: openmct, - domainObject: selection[0][0].context.item, - path: [selection[0][0].context.item, selection[0][1].context.item] - }, - template: '' - }); - - Vue.nextTick(() => { - viewComponentObject = component.$root.$children[0]; - done(); - }); - }); - - afterEach(() => { - openmct.router.path = null; - }); - - describe('in view only mode', () => { - let browseOptionsEl; - beforeEach(() => { - browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); - }); - - it('hides legend properties', () => { - const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties'); - expect(legendPropertiesEl).toBeNull(); - }); - - it('shows series properties', () => { - const seriesPropertiesEl = browseOptionsEl.querySelector('.c-tree'); - expect(seriesPropertiesEl).not.toBeNull(); - }); - - it('shows yaxis properties', () => { - const yAxisPropertiesEl = browseOptionsEl.querySelector('.js-yaxis-properties'); - expect(yAxisPropertiesEl).not.toBeNull(); - }); - }); + it('does not show series properties', () => { + const seriesPropertiesEl = browseOptionsEl.querySelector('.c-tree'); + expect(seriesPropertiesEl).toBeNull(); + }); + it('does not show yaxis properties', () => { + const yAxisPropertiesEl = browseOptionsEl.querySelector('.js-yaxis-properties'); + expect(yAxisPropertiesEl).toBeNull(); + }); }); + }); + + describe('inspector view of stacked plot child', () => { + let component; + let viewComponentObject; + let mockComposition; + let testTelemetryObject; + let selection; + let config; + beforeEach((done) => { + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key', + name: 'Some attribute', + hints: { + range: 1 + } + }, + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 2 + } + } + ] + } + }; + + selection = [ + [ + { + context: { + item: { + id: 'test-object', + identifier: { + key: 'test-object', + namespace: '' + }, + type: 'telemetry.plot.overlay', + configuration: { + series: [ + { + identifier: { + key: 'test-object', + namespace: '' + } + } + ] + }, + composition: [] + } + } + }, + { + context: { + item: { + type: 'telemetry.plot.stacked', + identifier: { + key: 'some-stacked-plot', + namespace: '' + }, + configuration: { + series: [] + } + } + } + } + ] + ]; + + openmct.router.path = [testTelemetryObject]; + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testTelemetryObject); + + return [testTelemetryObject]; + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier); + config = new PlotConfigurationModel({ + id: configId, + domainObject: selection[0][0].context.item, + openmct: openmct + }); + configStore.add(configId, config); + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + PlotOptions + }, + provide: { + openmct: openmct, + domainObject: selection[0][0].context.item, + path: [selection[0][0].context.item, selection[0][1].context.item] + }, + template: '' + }); + + Vue.nextTick(() => { + viewComponentObject = component.$root.$children[0]; + done(); + }); + }); + + afterEach(() => { + openmct.router.path = null; + }); + + describe('in view only mode', () => { + let browseOptionsEl; + beforeEach(() => { + browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); + }); + + it('hides legend properties', () => { + const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties'); + expect(legendPropertiesEl).toBeNull(); + }); + + it('shows series properties', () => { + const seriesPropertiesEl = browseOptionsEl.querySelector('.c-tree'); + expect(seriesPropertiesEl).not.toBeNull(); + }); + + it('shows yaxis properties', () => { + const yAxisPropertiesEl = browseOptionsEl.querySelector('.js-yaxis-properties'); + expect(yAxisPropertiesEl).not.toBeNull(); + }); + }); + }); }); diff --git a/src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js b/src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js index b8296197e0..e9a7bd2fd3 100644 --- a/src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js +++ b/src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js @@ -21,18 +21,16 @@ *****************************************************************************/ export default function stackedPlotConfigurationInterceptor(openmct) { + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return domainObject && domainObject.type === 'telemetry.plot.stacked'; + }, + invoke: (identifier, object) => { + if (object && object.configuration && object.configuration.series === undefined) { + object.configuration.series = []; + } - openmct.objects.addGetInterceptor({ - appliesTo: (identifier, domainObject) => { - return domainObject && domainObject.type === 'telemetry.plot.stacked'; - }, - invoke: (identifier, object) => { - - if (object && object.configuration && object.configuration.series === undefined) { - object.configuration.series = []; - } - - return object; - } - }); + return object; + } + }); } diff --git a/src/plugins/plot/tickUtils.js b/src/plugins/plot/tickUtils.js index a83a779891..98b3ed8515 100644 --- a/src/plugins/plot/tickUtils.js +++ b/src/plugins/plot/tickUtils.js @@ -1,4 +1,4 @@ -import { antisymlog, symlog } from "./mathUtils"; +import { antisymlog, symlog } from './mathUtils'; const e10 = Math.sqrt(50); const e5 = Math.sqrt(10); @@ -8,18 +8,18 @@ const e2 = Math.sqrt(2); * Nicely formatted tick steps from d3-array. */ function tickStep(start, stop, count) { - const step0 = Math.abs(stop - start) / Math.max(0, count); - let step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)); - const error = step0 / step1; - if (error >= e10) { - step1 *= 10; - } else if (error >= e5) { - step1 *= 5; - } else if (error >= e2) { - step1 *= 2; - } + const step0 = Math.abs(stop - start) / Math.max(0, count); + let step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)); + const error = step0 / step1; + if (error >= e10) { + step1 *= 10; + } else if (error >= e5) { + step1 *= 5; + } else if (error >= e2) { + step1 *= 2; + } - return stop < start ? -step1 : step1; + return stop < start ? -step1 : step1; } /** @@ -27,132 +27,130 @@ function tickStep(start, stop, count) { * ticks to precise values. */ function getPrecision(step) { - const exponential = step.toExponential(); - const i = exponential.indexOf('e'); - if (i === -1) { - return 0; - } + const exponential = step.toExponential(); + const i = exponential.indexOf('e'); + if (i === -1) { + return 0; + } - let precision = Math.max(0, -(Number(exponential.slice(i + 1)))); + let precision = Math.max(0, -Number(exponential.slice(i + 1))); - if (precision > 20) { - precision = 20; - } + if (precision > 20) { + precision = 20; + } - return precision; + return precision; } export function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount = 6) { - // log()'ed values - const mainLogTicks = ticks(start, stop, mainTickCount); + // log()'ed values + const mainLogTicks = ticks(start, stop, mainTickCount); - // original values - const mainTicks = mainLogTicks.map(n => antisymlog(n, 10)); + // original values + const mainTicks = mainLogTicks.map((n) => antisymlog(n, 10)); - const result = []; + const result = []; - let i = 0; - for (const logTick of mainLogTicks) { - result.push(logTick); + let i = 0; + for (const logTick of mainLogTicks) { + result.push(logTick); - if (i === mainLogTicks.length - 1) { - break; - } - - const tick = mainTicks[i]; - const nextTick = mainTicks[i + 1]; - const rangeBetweenMainTicks = nextTick - tick; - - const secondaryLogTicks = ticks( - tick + rangeBetweenMainTicks / (secondaryTickCount + 1), - nextTick - rangeBetweenMainTicks / (secondaryTickCount + 1), - secondaryTickCount - 2 - ) - .map(n => symlog(n, 10)); - - result.push(...secondaryLogTicks); - - i++; + if (i === mainLogTicks.length - 1) { + break; } - return result; + const tick = mainTicks[i]; + const nextTick = mainTicks[i + 1]; + const rangeBetweenMainTicks = nextTick - tick; + + const secondaryLogTicks = ticks( + tick + rangeBetweenMainTicks / (secondaryTickCount + 1), + nextTick - rangeBetweenMainTicks / (secondaryTickCount + 1), + secondaryTickCount - 2 + ).map((n) => symlog(n, 10)); + + result.push(...secondaryLogTicks); + + i++; + } + + return result; } /** * Linear tick generation from d3-array. */ export function ticks(start, stop, count) { - const step = tickStep(start, stop, count); - const precision = getPrecision(step); + const step = tickStep(start, stop, count); + const precision = getPrecision(step); - return _.range( - Math.ceil(start / step) * step, - Math.floor(stop / step) * step + step / 2, // inclusive - step - ).map(function round(tick) { - return Number(tick.toFixed(precision)); - }); + return _.range( + Math.ceil(start / step) * step, + Math.floor(stop / step) * step + step / 2, // inclusive + step + ).map(function round(tick) { + return Number(tick.toFixed(precision)); + }); } export function commonPrefix(a, b) { - const maxLen = Math.min(a.length, b.length); - let breakpoint = 0; - for (let i = 0; i < maxLen; i++) { - if (a[i] !== b[i]) { - break; - } - - if (a[i] === ' ') { - breakpoint = i + 1; - } + const maxLen = Math.min(a.length, b.length); + let breakpoint = 0; + for (let i = 0; i < maxLen; i++) { + if (a[i] !== b[i]) { + break; } - return a.slice(0, breakpoint); + if (a[i] === ' ') { + breakpoint = i + 1; + } + } + + return a.slice(0, breakpoint); } export function commonSuffix(a, b) { - const maxLen = Math.min(a.length, b.length); - let breakpoint = 0; - for (let i = 0; i <= maxLen; i++) { - if (a[a.length - i] !== b[b.length - i]) { - break; - } - - if ('. '.indexOf(a[a.length - i]) !== -1) { - breakpoint = i; - } + const maxLen = Math.min(a.length, b.length); + let breakpoint = 0; + for (let i = 0; i <= maxLen; i++) { + if (a[a.length - i] !== b[b.length - i]) { + break; } - return a.slice(a.length - breakpoint); + if ('. '.indexOf(a[a.length - i]) !== -1) { + breakpoint = i; + } + } + + return a.slice(a.length - breakpoint); } export function getFormattedTicks(newTicks, format) { - newTicks = newTicks - .map(function (tickValue) { - return { - value: tickValue, - text: format(tickValue) - }; - }); + newTicks = newTicks.map(function (tickValue) { + return { + value: tickValue, + text: format(tickValue) + }; + }); - if (newTicks.length && typeof newTicks[0].text === 'string') { - const tickText = newTicks.map(function (t) { - return t.text; - }); - const prefix = tickText.reduce(commonPrefix); - const suffix = tickText.reduce(commonSuffix); - newTicks.forEach(function (t) { - t.fullText = t.text; + if (newTicks.length && typeof newTicks[0].text === 'string') { + const tickText = newTicks.map(function (t) { + return t.text; + }); + const prefix = tickText.reduce(commonPrefix); + const suffix = tickText.reduce(commonSuffix); + newTicks.forEach(function (t) { + t.fullText = t.text; - if (typeof t.text === 'string') { - if (suffix.length) { - t.text = t.text.slice(prefix.length, -suffix.length); - } else { - t.text = t.text.slice(prefix.length); - } - } - }); - } + if (typeof t.text === 'string') { + if (suffix.length) { + t.text = t.text.slice(prefix.length, -suffix.length); + } else { + t.text = t.text.slice(prefix.length); + } + } + }); + } - return newTicks; + return newTicks; } diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index a095e91a37..bcf2c04586 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -21,217 +21,217 @@ *****************************************************************************/ define([ - 'lodash', - './utcTimeSystem/plugin', - './remoteClock/plugin', - './localTimeSystem/plugin', - './ISOTimeFormat/plugin', - './myItems/plugin', - '../../example/generator/plugin', - '../../example/eventGenerator/plugin', - './autoflow/AutoflowTabularPlugin', - './timeConductor/plugin', - '../../example/imagery/plugin', - '../../example/faultManagement/exampleFaultSource', - './imagery/plugin', - './summaryWidget/plugin', - './URLIndicatorPlugin/URLIndicatorPlugin', - './telemetryMean/plugin', - './plot/plugin', - './charts/bar/plugin', - './charts/scatter/plugin', - './telemetryTable/plugin', - './staticRootPlugin/plugin', - './notebook/plugin', - './displayLayout/plugin', - './formActions/plugin', - './folderView/plugin', - './flexibleLayout/plugin', - './tabs/plugin', - './LADTable/plugin', - './filters/plugin', - './objectMigration/plugin', - './goToOriginalAction/plugin', - './openInNewTabAction/plugin', - './clearData/plugin', - './webPage/plugin', - './condition/plugin', - './conditionWidget/plugin', - './themes/espresso', - './themes/snow', - './URLTimeSettingsSynchronizer/plugin', - './notificationIndicator/plugin', - './newFolderAction/plugin', - './persistence/couch/plugin', - './defaultRootName/plugin', - './plan/plugin', - './viewDatumAction/plugin', - './viewLargeAction/plugin', - './interceptors/plugin', - './performanceIndicator/plugin', - './CouchDBSearchFolder/plugin', - './timeline/plugin', - './hyperlink/plugin', - './clock/plugin', - './DeviceClassifier/plugin', - './timer/plugin', - './userIndicator/plugin', - '../../example/exampleUser/plugin', - './localStorage/plugin', - './operatorStatus/plugin', - './gauge/GaugePlugin', - './timelist/plugin', - './faultManagement/FaultManagementPlugin', - '../../example/exampleTags/plugin', - './inspectorViews/plugin' + 'lodash', + './utcTimeSystem/plugin', + './remoteClock/plugin', + './localTimeSystem/plugin', + './ISOTimeFormat/plugin', + './myItems/plugin', + '../../example/generator/plugin', + '../../example/eventGenerator/plugin', + './autoflow/AutoflowTabularPlugin', + './timeConductor/plugin', + '../../example/imagery/plugin', + '../../example/faultManagement/exampleFaultSource', + './imagery/plugin', + './summaryWidget/plugin', + './URLIndicatorPlugin/URLIndicatorPlugin', + './telemetryMean/plugin', + './plot/plugin', + './charts/bar/plugin', + './charts/scatter/plugin', + './telemetryTable/plugin', + './staticRootPlugin/plugin', + './notebook/plugin', + './displayLayout/plugin', + './formActions/plugin', + './folderView/plugin', + './flexibleLayout/plugin', + './tabs/plugin', + './LADTable/plugin', + './filters/plugin', + './objectMigration/plugin', + './goToOriginalAction/plugin', + './openInNewTabAction/plugin', + './clearData/plugin', + './webPage/plugin', + './condition/plugin', + './conditionWidget/plugin', + './themes/espresso', + './themes/snow', + './URLTimeSettingsSynchronizer/plugin', + './notificationIndicator/plugin', + './newFolderAction/plugin', + './persistence/couch/plugin', + './defaultRootName/plugin', + './plan/plugin', + './viewDatumAction/plugin', + './viewLargeAction/plugin', + './interceptors/plugin', + './performanceIndicator/plugin', + './CouchDBSearchFolder/plugin', + './timeline/plugin', + './hyperlink/plugin', + './clock/plugin', + './DeviceClassifier/plugin', + './timer/plugin', + './userIndicator/plugin', + '../../example/exampleUser/plugin', + './localStorage/plugin', + './operatorStatus/plugin', + './gauge/GaugePlugin', + './timelist/plugin', + './faultManagement/FaultManagementPlugin', + '../../example/exampleTags/plugin', + './inspectorViews/plugin' ], function ( - _, - UTCTimeSystem, - RemoteClock, - LocalTimeSystem, - ISOTimeFormat, - MyItems, - GeneratorPlugin, - EventGeneratorPlugin, - AutoflowPlugin, - TimeConductorPlugin, - ExampleImagery, - ExampleFaultSource, - ImageryPlugin, - SummaryWidget, - URLIndicatorPlugin, - TelemetryMean, - PlotPlugin, - BarChartPlugin, - ScatterPlotPlugin, - TelemetryTablePlugin, - StaticRootPlugin, - Notebook, - DisplayLayoutPlugin, - FormActions, - FolderView, - FlexibleLayout, - Tabs, - LADTable, - Filters, - ObjectMigration, - GoToOriginalAction, - OpenInNewTabAction, - ClearData, - WebPagePlugin, - ConditionPlugin, - ConditionWidgetPlugin, - Espresso, - Snow, - URLTimeSettingsSynchronizer, - NotificationIndicator, - NewFolderAction, - CouchDBPlugin, - DefaultRootName, - PlanLayout, - ViewDatumAction, - ViewLargeAction, - ObjectInterceptors, - PerformanceIndicator, - CouchDBSearchFolder, - Timeline, - Hyperlink, - Clock, - DeviceClassifier, - Timer, - UserIndicator, - ExampleUser, - LocalStorage, - OperatorStatus, - GaugePlugin, - TimeList, - FaultManagementPlugin, - ExampleTags, - InspectorViews + _, + UTCTimeSystem, + RemoteClock, + LocalTimeSystem, + ISOTimeFormat, + MyItems, + GeneratorPlugin, + EventGeneratorPlugin, + AutoflowPlugin, + TimeConductorPlugin, + ExampleImagery, + ExampleFaultSource, + ImageryPlugin, + SummaryWidget, + URLIndicatorPlugin, + TelemetryMean, + PlotPlugin, + BarChartPlugin, + ScatterPlotPlugin, + TelemetryTablePlugin, + StaticRootPlugin, + Notebook, + DisplayLayoutPlugin, + FormActions, + FolderView, + FlexibleLayout, + Tabs, + LADTable, + Filters, + ObjectMigration, + GoToOriginalAction, + OpenInNewTabAction, + ClearData, + WebPagePlugin, + ConditionPlugin, + ConditionWidgetPlugin, + Espresso, + Snow, + URLTimeSettingsSynchronizer, + NotificationIndicator, + NewFolderAction, + CouchDBPlugin, + DefaultRootName, + PlanLayout, + ViewDatumAction, + ViewLargeAction, + ObjectInterceptors, + PerformanceIndicator, + CouchDBSearchFolder, + Timeline, + Hyperlink, + Clock, + DeviceClassifier, + Timer, + UserIndicator, + ExampleUser, + LocalStorage, + OperatorStatus, + GaugePlugin, + TimeList, + FaultManagementPlugin, + ExampleTags, + InspectorViews ) { - const plugins = {}; + const plugins = {}; - plugins.example = {}; - plugins.example.ExampleUser = ExampleUser.default; - plugins.example.ExampleImagery = ExampleImagery.default; - plugins.example.ExampleFaultSource = ExampleFaultSource.default; - plugins.example.EventGeneratorPlugin = EventGeneratorPlugin.default; - plugins.example.ExampleTags = ExampleTags.default; - plugins.example.Generator = () => GeneratorPlugin.default; + plugins.example = {}; + plugins.example.ExampleUser = ExampleUser.default; + plugins.example.ExampleImagery = ExampleImagery.default; + plugins.example.ExampleFaultSource = ExampleFaultSource.default; + plugins.example.EventGeneratorPlugin = EventGeneratorPlugin.default; + plugins.example.ExampleTags = ExampleTags.default; + plugins.example.Generator = () => GeneratorPlugin.default; - plugins.UTCTimeSystem = UTCTimeSystem.default; - plugins.LocalTimeSystem = LocalTimeSystem; - plugins.RemoteClock = RemoteClock.default; + plugins.UTCTimeSystem = UTCTimeSystem.default; + plugins.LocalTimeSystem = LocalTimeSystem; + plugins.RemoteClock = RemoteClock.default; - plugins.MyItems = MyItems.default; + plugins.MyItems = MyItems.default; - plugins.StaticRootPlugin = StaticRootPlugin.default; + plugins.StaticRootPlugin = StaticRootPlugin.default; - /** - * A tabular view showing the latest values of multiple telemetry points at - * once. Formatted so that labels and values are aligned. - * - * @param {Object} [options] Optional settings to apply to the autoflow - * tabular view. Currently supports one option, 'type'. - * @param {string} [options.type] The key of an object type to apply this view - * to exclusively. - */ - plugins.AutoflowView = AutoflowPlugin; + /** + * A tabular view showing the latest values of multiple telemetry points at + * once. Formatted so that labels and values are aligned. + * + * @param {Object} [options] Optional settings to apply to the autoflow + * tabular view. Currently supports one option, 'type'. + * @param {string} [options.type] The key of an object type to apply this view + * to exclusively. + */ + plugins.AutoflowView = AutoflowPlugin; - plugins.Conductor = TimeConductorPlugin.default; + plugins.Conductor = TimeConductorPlugin.default; - plugins.CouchDB = CouchDBPlugin.default; + plugins.CouchDB = CouchDBPlugin.default; - plugins.ImageryPlugin = ImageryPlugin; - plugins.Plot = PlotPlugin.default; - plugins.BarChart = BarChartPlugin.default; - plugins.ScatterPlot = ScatterPlotPlugin.default; - plugins.TelemetryTable = TelemetryTablePlugin; + plugins.ImageryPlugin = ImageryPlugin; + plugins.Plot = PlotPlugin.default; + plugins.BarChart = BarChartPlugin.default; + plugins.ScatterPlot = ScatterPlotPlugin.default; + plugins.TelemetryTable = TelemetryTablePlugin; - plugins.SummaryWidget = SummaryWidget; - plugins.TelemetryMean = TelemetryMean; - plugins.URLIndicator = URLIndicatorPlugin; - plugins.Notebook = Notebook.NotebookPlugin; - plugins.RestrictedNotebook = Notebook.RestrictedNotebookPlugin; - plugins.DisplayLayout = DisplayLayoutPlugin.default; - plugins.FaultManagement = FaultManagementPlugin.default; - plugins.FormActions = FormActions; - plugins.FolderView = FolderView; - plugins.Tabs = Tabs; - plugins.FlexibleLayout = FlexibleLayout; - plugins.LADTable = LADTable.default; - plugins.Filters = Filters; - plugins.ObjectMigration = ObjectMigration.default; - plugins.GoToOriginalAction = GoToOriginalAction.default; - plugins.OpenInNewTabAction = OpenInNewTabAction.default; - plugins.ClearData = ClearData; - plugins.WebPage = WebPagePlugin.default; - plugins.Espresso = Espresso.default; - plugins.Snow = Snow.default; - plugins.Condition = ConditionPlugin.default; - plugins.ConditionWidget = ConditionWidgetPlugin.default; - plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default; - plugins.NotificationIndicator = NotificationIndicator.default; - plugins.NewFolderAction = NewFolderAction.default; - plugins.ISOTimeFormat = ISOTimeFormat.default; - plugins.DefaultRootName = DefaultRootName.default; - plugins.PlanLayout = PlanLayout.default; - plugins.ViewDatumAction = ViewDatumAction.default; - plugins.ViewLargeAction = ViewLargeAction.default; - plugins.ObjectInterceptors = ObjectInterceptors.default; - plugins.PerformanceIndicator = PerformanceIndicator.default; - plugins.CouchDBSearchFolder = CouchDBSearchFolder.default; - plugins.Timeline = Timeline.default; - plugins.Hyperlink = Hyperlink.default; - plugins.Clock = Clock.default; - plugins.Timer = Timer.default; - plugins.DeviceClassifier = DeviceClassifier.default; - plugins.UserIndicator = UserIndicator.default; - plugins.LocalStorage = LocalStorage.default; - plugins.OperatorStatus = OperatorStatus.default; - plugins.Gauge = GaugePlugin.default; - plugins.Timelist = TimeList.default; - plugins.InspectorViews = InspectorViews.default; + plugins.SummaryWidget = SummaryWidget; + plugins.TelemetryMean = TelemetryMean; + plugins.URLIndicator = URLIndicatorPlugin; + plugins.Notebook = Notebook.NotebookPlugin; + plugins.RestrictedNotebook = Notebook.RestrictedNotebookPlugin; + plugins.DisplayLayout = DisplayLayoutPlugin.default; + plugins.FaultManagement = FaultManagementPlugin.default; + plugins.FormActions = FormActions; + plugins.FolderView = FolderView; + plugins.Tabs = Tabs; + plugins.FlexibleLayout = FlexibleLayout; + plugins.LADTable = LADTable.default; + plugins.Filters = Filters; + plugins.ObjectMigration = ObjectMigration.default; + plugins.GoToOriginalAction = GoToOriginalAction.default; + plugins.OpenInNewTabAction = OpenInNewTabAction.default; + plugins.ClearData = ClearData; + plugins.WebPage = WebPagePlugin.default; + plugins.Espresso = Espresso.default; + plugins.Snow = Snow.default; + plugins.Condition = ConditionPlugin.default; + plugins.ConditionWidget = ConditionWidgetPlugin.default; + plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default; + plugins.NotificationIndicator = NotificationIndicator.default; + plugins.NewFolderAction = NewFolderAction.default; + plugins.ISOTimeFormat = ISOTimeFormat.default; + plugins.DefaultRootName = DefaultRootName.default; + plugins.PlanLayout = PlanLayout.default; + plugins.ViewDatumAction = ViewDatumAction.default; + plugins.ViewLargeAction = ViewLargeAction.default; + plugins.ObjectInterceptors = ObjectInterceptors.default; + plugins.PerformanceIndicator = PerformanceIndicator.default; + plugins.CouchDBSearchFolder = CouchDBSearchFolder.default; + plugins.Timeline = Timeline.default; + plugins.Hyperlink = Hyperlink.default; + plugins.Clock = Clock.default; + plugins.Timer = Timer.default; + plugins.DeviceClassifier = DeviceClassifier.default; + plugins.UserIndicator = UserIndicator.default; + plugins.LocalStorage = LocalStorage.default; + plugins.OperatorStatus = OperatorStatus.default; + plugins.Gauge = GaugePlugin.default; + plugins.Timelist = TimeList.default; + plugins.InspectorViews = InspectorViews.default; - return plugins; + return plugins; }); diff --git a/src/plugins/remoteClock/RemoteClock.js b/src/plugins/remoteClock/RemoteClock.js index bd77f15aad..7faa3b9290 100644 --- a/src/plugins/remoteClock/RemoteClock.js +++ b/src/plugins/remoteClock/RemoteClock.js @@ -33,135 +33,136 @@ import remoteClockRequestInterceptor from './requestInterceptor'; */ export default class RemoteClock extends DefaultClock { - constructor(openmct, identifier) { - super(); + constructor(openmct, identifier) { + super(); - this.key = 'remote-clock'; + this.key = 'remote-clock'; - this.openmct = openmct; - this.identifier = identifier; + this.openmct = openmct; + this.identifier = identifier; - this.name = 'Remote Clock'; - this.description = "Provides telemetry based timestamps from a configurable source."; + this.name = 'Remote Clock'; + this.description = 'Provides telemetry based timestamps from a configurable source.'; - this.timeTelemetryObject = undefined; - this.parseTime = undefined; - this.formatTime = undefined; - this.metadata = undefined; + this.timeTelemetryObject = undefined; + this.parseTime = undefined; + this.formatTime = undefined; + this.metadata = undefined; - this.lastTick = 0; + this.lastTick = 0; - this.openmct.telemetry.addRequestInterceptor( - remoteClockRequestInterceptor( - this.openmct, - this.identifier, - this.#waitForReady.bind(this) - ) - ); + this.openmct.telemetry.addRequestInterceptor( + remoteClockRequestInterceptor(this.openmct, this.identifier, this.#waitForReady.bind(this)) + ); - this._processDatum = this._processDatum.bind(this); + this._processDatum = this._processDatum.bind(this); + } + + start() { + this.openmct.objects + .get(this.identifier) + .then((domainObject) => { + this.openmct.time.on('timeSystem', this._timeSystemChange); + this.timeTelemetryObject = domainObject; + this.metadata = this.openmct.telemetry.getMetadata(domainObject); + this._timeSystemChange(); + this._requestLatest(); + this._subscribe(); + }) + .catch((error) => { + throw new Error(error); + }); + } + + stop() { + this.openmct.time.off('timeSystem', this._timeSystemChange); + if (this._unsubscribe) { + this._unsubscribe(); } - start() { - this.openmct.objects.get(this.identifier).then((domainObject) => { - this.openmct.time.on('timeSystem', this._timeSystemChange); - this.timeTelemetryObject = domainObject; - this.metadata = this.openmct.telemetry.getMetadata(domainObject); - this._timeSystemChange(); - this._requestLatest(); - this._subscribe(); - }).catch((error) => { - throw new Error(error); + this.removeAllListeners(); + } + + /** + * Will start a subscription to the timeTelemetryObject as well + * handle the unsubscribe callback + * + * @private + */ + _subscribe() { + this._unsubscribe = this.openmct.telemetry.subscribe( + this.timeTelemetryObject, + this._processDatum + ); + } + + /** + * Will request the latest data for the timeTelemetryObject + * + * @private + */ + _requestLatest() { + this.openmct.telemetry + .request(this.timeTelemetryObject, { + size: 1, + strategy: 'latest' + }) + .then((data) => { + this._processDatum(data[data.length - 1]); + }); + } + + /** + * Function to parse the datum from the timeTelemetryObject as well + * as check if it's valid, calls "tick" + * + * @private + */ + _processDatum(datum) { + let time = this.parseTime(datum); + + if (time > this.lastTick) { + this.tick(time); + } + } + + /** + * Callback function for timeSystem change events + * + * @private + */ + _timeSystemChange() { + let timeSystem = this.openmct.time.timeSystem(); + let timeKey = timeSystem.key; + let metadataValue = this.metadata.value(timeKey); + let timeFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); + this.parseTime = (datum) => { + return timeFormatter.parse(datum); + }; + + this.formatTime = (datum) => { + return timeFormatter.format(datum); + }; + } + + /** + * Waits for the clock to have a non-default tick value. + * + * @private + */ + #waitForReady() { + const waitForInitialTick = (resolve) => { + if (this.lastTick > 0) { + const offsets = this.openmct.time.clockOffsets(); + resolve({ + start: this.lastTick + offsets.start, + end: this.lastTick + offsets.end }); - } + } else { + setTimeout(() => waitForInitialTick(resolve), 100); + } + }; - stop() { - this.openmct.time.off('timeSystem', this._timeSystemChange); - if (this._unsubscribe) { - this._unsubscribe(); - } - - this.removeAllListeners(); - } - - /** - * Will start a subscription to the timeTelemetryObject as well - * handle the unsubscribe callback - * - * @private - */ - _subscribe() { - this._unsubscribe = this.openmct.telemetry.subscribe( - this.timeTelemetryObject, - this._processDatum - ); - } - - /** - * Will request the latest data for the timeTelemetryObject - * - * @private - */ - _requestLatest() { - this.openmct.telemetry.request(this.timeTelemetryObject, { - size: 1, - strategy: 'latest' - }).then(data => { - this._processDatum(data[data.length - 1]); - }); - } - - /** - * Function to parse the datum from the timeTelemetryObject as well - * as check if it's valid, calls "tick" - * - * @private - */ - _processDatum(datum) { - let time = this.parseTime(datum); - - if (time > this.lastTick) { - this.tick(time); - } - } - - /** - * Callback function for timeSystem change events - * - * @private - */ - _timeSystemChange() { - let timeSystem = this.openmct.time.timeSystem(); - let timeKey = timeSystem.key; - let metadataValue = this.metadata.value(timeKey); - let timeFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); - this.parseTime = (datum) => { - return timeFormatter.parse(datum); - }; - - this.formatTime = (datum) => { - return timeFormatter.format(datum); - }; - } - - /** - * Waits for the clock to have a non-default tick value. - * - * @private - */ - #waitForReady() { - const waitForInitialTick = (resolve) => { - if (this.lastTick > 0) { - const offsets = this.openmct.time.clockOffsets(); - resolve({ - start: this.lastTick + offsets.start, - end: this.lastTick + offsets.end - }); - } else { - setTimeout(() => waitForInitialTick(resolve), 100); - } - }; - - return new Promise(waitForInitialTick); - } + return new Promise(waitForInitialTick); + } } diff --git a/src/plugins/remoteClock/RemoteClockSpec.js b/src/plugins/remoteClock/RemoteClockSpec.js index bfbb843856..e41ea872ac 100644 --- a/src/plugins/remoteClock/RemoteClockSpec.js +++ b/src/plugins/remoteClock/RemoteClockSpec.js @@ -20,144 +20,148 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; const REMOTE_CLOCK_KEY = 'remote-clock'; const TIME_TELEMETRY_ID = { - namespace: 'remote', - key: 'telemetry' + namespace: 'remote', + key: 'telemetry' }; const TIME_VALUE = 12345; const REQ_OPTIONS = { - size: 1, - strategy: 'latest' + size: 1, + strategy: 'latest' }; const OFFSET_START = -10; const OFFSET_END = 1; -describe("the RemoteClock plugin", () => { - let openmct; - let object = { - name: 'remote-telemetry', - identifier: TIME_TELEMETRY_ID +describe('the RemoteClock plugin', () => { + let openmct; + let object = { + name: 'remote-telemetry', + identifier: TIME_TELEMETRY_ID + }; + + beforeEach((done) => { + openmct = createOpenMct(); + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('once installed', () => { + let remoteClock; + let boundsCallback; + let metadataValue = { some: 'value' }; + let timeSystem = { key: 'utc' }; + let metadata = { + value: () => metadataValue + }; + let reqDatum = { + key: TIME_VALUE }; - beforeEach((done) => { - openmct = createOpenMct(); - openmct.on('start', done); - openmct.startHeadless(); + let formatter = { + parse: (datum) => datum.key + }; + + let objectPromise; + let requestPromise; + + beforeEach(() => { + openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID)); + + let clocks = openmct.time.getAllClocks(); + remoteClock = clocks.filter((clock) => clock.key === REMOTE_CLOCK_KEY)[0]; + + boundsCallback = jasmine.createSpy('boundsCallback'); + openmct.time.on('bounds', boundsCallback); + + spyOn(remoteClock, '_timeSystemChange').and.callThrough(); + spyOn(openmct.telemetry, 'getMetadata').and.returnValue(metadata); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue(formatter); + spyOn(openmct.telemetry, 'subscribe').and.callThrough(); + spyOn(openmct.time, 'on').and.callThrough(); + spyOn(openmct.time, 'timeSystem').and.returnValue(timeSystem); + spyOn(metadata, 'value').and.callThrough(); + + let requestPromiseResolve; + let objectPromiseResolve; + + requestPromise = new Promise((resolve) => { + requestPromiseResolve = resolve; + }); + spyOn(openmct.telemetry, 'request').and.callFake(() => { + requestPromiseResolve([reqDatum]); + + return requestPromise; + }); + + objectPromise = new Promise((resolve) => { + objectPromiseResolve = resolve; + }); + spyOn(openmct.objects, 'get').and.callFake(() => { + objectPromiseResolve(object); + + return objectPromise; + }); + + openmct.time.clock(REMOTE_CLOCK_KEY, { + start: OFFSET_START, + end: OFFSET_END + }); }); - afterEach(() => { - return resetApplicationState(openmct); + it('Does not throw error if time system is changed before remote clock initialized', () => { + expect(() => openmct.time.timeSystem('utc')).not.toThrow(); }); - describe('once installed', () => { - let remoteClock; - let boundsCallback; - let metadataValue = { some: 'value' }; - let timeSystem = { key: 'utc' }; - let metadata = { - value: () => metadataValue - }; - let reqDatum = { - key: TIME_VALUE - }; + describe('once resolved', () => { + beforeEach(async () => { + await Promise.all([objectPromise, requestPromise]); + }); - let formatter = { - parse: (datum) => datum.key - }; + it('is available and sets up initial values and listeners', () => { + expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY); + expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID); + expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange); + expect(remoteClock._timeSystemChange).toHaveBeenCalled(); + }); - let objectPromise; - let requestPromise; + it('will request/store the object based on the identifier passed in', () => { + expect(remoteClock.timeTelemetryObject).toEqual(object); + }); - beforeEach(() => { - openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID)); + it('will request metadata and set up formatters', () => { + expect(remoteClock.metadata).toEqual(metadata); + expect(metadata.value).toHaveBeenCalled(); + expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue); + }); - let clocks = openmct.time.getAllClocks(); - remoteClock = clocks.filter(clock => clock.key === REMOTE_CLOCK_KEY)[0]; - - boundsCallback = jasmine.createSpy("boundsCallback"); - openmct.time.on('bounds', boundsCallback); - - spyOn(remoteClock, '_timeSystemChange').and.callThrough(); - spyOn(openmct.telemetry, 'getMetadata').and.returnValue(metadata); - spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue(formatter); - spyOn(openmct.telemetry, 'subscribe').and.callThrough(); - spyOn(openmct.time, 'on').and.callThrough(); - spyOn(openmct.time, 'timeSystem').and.returnValue(timeSystem); - spyOn(metadata, 'value').and.callThrough(); - - let requestPromiseResolve; - let objectPromiseResolve; - - requestPromise = new Promise((resolve) => { - requestPromiseResolve = resolve; - }); - spyOn(openmct.telemetry, 'request').and.callFake(() => { - requestPromiseResolve([reqDatum]); - - return requestPromise; - }); - - objectPromise = new Promise((resolve) => { - objectPromiseResolve = resolve; - }); - spyOn(openmct.objects, 'get').and.callFake(() => { - objectPromiseResolve(object); - - return objectPromise; - }); - - openmct.time.clock(REMOTE_CLOCK_KEY, { - start: OFFSET_START, - end: OFFSET_END - }); - }); - - it("Does not throw error if time system is changed before remote clock initialized", () => { - expect(() => openmct.time.timeSystem('utc')).not.toThrow(); - }); - - describe('once resolved', () => { - beforeEach(async () => { - await Promise.all([objectPromise, requestPromise]); - }); - - it('is available and sets up initial values and listeners', () => { - expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY); - expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID); - expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange); - expect(remoteClock._timeSystemChange).toHaveBeenCalled(); - }); - - it('will request/store the object based on the identifier passed in', () => { - expect(remoteClock.timeTelemetryObject).toEqual(object); - }); - - it('will request metadata and set up formatters', () => { - expect(remoteClock.metadata).toEqual(metadata); - expect(metadata.value).toHaveBeenCalled(); - expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue); - }); - - it('will request the latest datum for the object it received and process the datum returned', () => { - expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS); - expect(boundsCallback).toHaveBeenCalledWith({ - start: TIME_VALUE + OFFSET_START, - end: TIME_VALUE + OFFSET_END - }, true); - }); - - it('will set up subscriptions correctly', () => { - expect(remoteClock._unsubscribe).toBeDefined(); - expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum); - }); - }); + it('will request the latest datum for the object it received and process the datum returned', () => { + expect(openmct.telemetry.request).toHaveBeenCalledWith( + remoteClock.timeTelemetryObject, + REQ_OPTIONS + ); + expect(boundsCallback).toHaveBeenCalledWith( + { + start: TIME_VALUE + OFFSET_START, + end: TIME_VALUE + OFFSET_END + }, + true + ); + }); + it('will set up subscriptions correctly', () => { + expect(remoteClock._unsubscribe).toBeDefined(); + expect(openmct.telemetry.subscribe).toHaveBeenCalledWith( + remoteClock.timeTelemetryObject, + remoteClock._processDatum + ); + }); }); - + }); }); diff --git a/src/plugins/remoteClock/plugin.js b/src/plugins/remoteClock/plugin.js index 93b2640d37..749fee9abe 100644 --- a/src/plugins/remoteClock/plugin.js +++ b/src/plugins/remoteClock/plugin.js @@ -20,13 +20,13 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import RemoteClock from "./RemoteClock"; +import RemoteClock from './RemoteClock'; /** * Install a clock that uses a configurable telemetry endpoint. */ export default function (identifier) { - return function (openmct) { - openmct.time.addClock(new RemoteClock(openmct, identifier)); - }; + return function (openmct) { + openmct.time.addClock(new RemoteClock(openmct, identifier)); + }; } diff --git a/src/plugins/remoteClock/requestInterceptor.js b/src/plugins/remoteClock/requestInterceptor.js index 4102f87bdd..a963249e8b 100644 --- a/src/plugins/remoteClock/requestInterceptor.js +++ b/src/plugins/remoteClock/requestInterceptor.js @@ -21,24 +21,24 @@ *****************************************************************************/ function remoteClockRequestInterceptor(openmct, _remoteClockIdentifier, waitForBounds) { - let remoteClockLoaded = false; + let remoteClockLoaded = false; - return { - appliesTo: () => { - // Get the activeClock from the Global Time Context - const { activeClock } = openmct.time; + return { + appliesTo: () => { + // Get the activeClock from the Global Time Context + const { activeClock } = openmct.time; - return activeClock?.key === 'remote-clock' && !remoteClockLoaded; - }, - invoke: async (request) => { - const { start, end } = await waitForBounds(); - remoteClockLoaded = true; - request.start = start; - request.end = end; + return activeClock?.key === 'remote-clock' && !remoteClockLoaded; + }, + invoke: async (request) => { + const { start, end } = await waitForBounds(); + remoteClockLoaded = true; + request.start = start; + request.end = end; - return request; - } - }; + return request; + } + }; } export default remoteClockRequestInterceptor; diff --git a/src/plugins/remove/RemoveAction.js b/src/plugins/remove/RemoveAction.js index 68cee6ef59..0671dfd75e 100644 --- a/src/plugins/remove/RemoveAction.js +++ b/src/plugins/remove/RemoveAction.js @@ -23,141 +23,142 @@ const SPECIAL_MESSAGE_TYPES = ['layout', 'flexible-layout']; export default class RemoveAction { - #transaction; + #transaction; - constructor(openmct) { + constructor(openmct) { + this.name = 'Remove'; + this.key = 'remove'; + this.description = 'Remove this object from its containing object.'; + this.cssClass = 'icon-trash'; + this.group = 'action'; + this.priority = 1; - this.name = 'Remove'; - this.key = 'remove'; - this.description = 'Remove this object from its containing object.'; - this.cssClass = "icon-trash"; - this.group = "action"; - this.priority = 1; + this.openmct = openmct; - this.openmct = openmct; + this.removeFromComposition = this.removeFromComposition.bind(this); // for access to private transaction variable + this.#transaction = null; + } - this.removeFromComposition = this.removeFromComposition.bind(this); // for access to private transaction variable - this.#transaction = null; + async invoke(objectPath) { + const child = objectPath[0]; + const parent = objectPath[1]; + + try { + await this.showConfirmDialog(child, parent); + } catch (error) { + return; // form canceled, exit invoke } - async invoke(objectPath) { - const child = objectPath[0]; - const parent = objectPath[1]; + await this.removeFromComposition(parent, child, objectPath); - try { - await this.showConfirmDialog(child, parent); - } catch (error) { - return; // form canceled, exit invoke - } + if (this.inNavigationPath(child)) { + this.navigateTo(objectPath.slice(1)); + } + } - await this.removeFromComposition(parent, child, objectPath); + showConfirmDialog(child, parent) { + let message = + 'Warning! This action will remove this object. Are you sure you want to continue?'; - if (this.inNavigationPath(child)) { - this.navigateTo(objectPath.slice(1)); - } + if (SPECIAL_MESSAGE_TYPES.includes(parent.type)) { + const type = this.openmct.types.get(parent.type); + const typeName = type.definition.name; + + message = `Warning! This action will remove this item from the ${typeName}. Are you sure you want to continue?`; } - showConfirmDialog(child, parent) { - let message = 'Warning! This action will remove this object. Are you sure you want to continue?'; - - if (SPECIAL_MESSAGE_TYPES.includes(parent.type)) { - const type = this.openmct.types.get(parent.type); - const typeName = type.definition.name; - - message = `Warning! This action will remove this item from the ${typeName}. Are you sure you want to continue?`; - } - - return new Promise((resolve, reject) => { - const dialog = this.openmct.overlays.dialog({ - title: `Remove ${child.name}`, - iconClass: 'alert', - message, - buttons: [ - { - label: 'OK', - callback: () => { - dialog.dismiss(); - resolve(); - } - }, - { - label: 'Cancel', - callback: () => { - dialog.dismiss(); - reject(); - } - } - ] - }); - }); - } - - inNavigationPath(object) { - return this.openmct.router.path - .some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier)); - } - - navigateTo(objectPath) { - let urlPath = objectPath.reverse() - .map(object => this.openmct.objects.makeKeyString(object.identifier)) - .join("/"); - - this.openmct.router.navigate('#/browse/' + urlPath); - } - - async removeFromComposition(parent, child, objectPath) { - this.startTransaction(); - - const composition = this.openmct.composition.get(parent); - composition.remove(child); - - if (!this.openmct.objects.isObjectPathToALink(child, objectPath)) { - this.openmct.objects.mutate(child, 'location', null); - } - - if (this.inNavigationPath(child) && this.openmct.editor.isEditing()) { - this.openmct.editor.save(); - } - - await this.saveTransaction(); - } - - appliesTo(objectPath) { - const parent = objectPath[1]; - const parentType = parent && this.openmct.types.get(parent.type); - const child = objectPath[0]; - const locked = child.locked ? child.locked : parent && parent.locked; - const isEditing = this.openmct.editor.isEditing(); - const isPersistable = this.openmct.objects.isPersistable(child.identifier); - const isLink = this.openmct.objects.isObjectPathToALink(child, objectPath); - - if (!isLink && (locked || !isPersistable)) { - return false; - } - - if (isEditing) { - if (this.openmct.router.isNavigatedObject(objectPath)) { - return false; + return new Promise((resolve, reject) => { + const dialog = this.openmct.overlays.dialog({ + title: `Remove ${child.name}`, + iconClass: 'alert', + message, + buttons: [ + { + label: 'OK', + callback: () => { + dialog.dismiss(); + resolve(); } - } + }, + { + label: 'Cancel', + callback: () => { + dialog.dismiss(); + reject(); + } + } + ] + }); + }); + } - return parentType?.definition.creatable - && Array.isArray(parent?.composition); + inNavigationPath(object) { + return this.openmct.router.path.some((objectInPath) => + this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier) + ); + } + + navigateTo(objectPath) { + let urlPath = objectPath + .reverse() + .map((object) => this.openmct.objects.makeKeyString(object.identifier)) + .join('/'); + + this.openmct.router.navigate('#/browse/' + urlPath); + } + + async removeFromComposition(parent, child, objectPath) { + this.startTransaction(); + + const composition = this.openmct.composition.get(parent); + composition.remove(child); + + if (!this.openmct.objects.isObjectPathToALink(child, objectPath)) { + this.openmct.objects.mutate(child, 'location', null); } - startTransaction() { - if (!this.openmct.objects.isTransactionActive()) { - this.#transaction = this.openmct.objects.startTransaction(); - } + if (this.inNavigationPath(child) && this.openmct.editor.isEditing()) { + this.openmct.editor.save(); } - async saveTransaction() { - if (!this.#transaction) { - return; - } + await this.saveTransaction(); + } - await this.#transaction.commit(); - this.openmct.objects.endTransaction(); - this.#transaction = null; + appliesTo(objectPath) { + const parent = objectPath[1]; + const parentType = parent && this.openmct.types.get(parent.type); + const child = objectPath[0]; + const locked = child.locked ? child.locked : parent && parent.locked; + const isEditing = this.openmct.editor.isEditing(); + const isPersistable = this.openmct.objects.isPersistable(child.identifier); + const isLink = this.openmct.objects.isObjectPathToALink(child, objectPath); + + if (!isLink && (locked || !isPersistable)) { + return false; } + + if (isEditing) { + if (this.openmct.router.isNavigatedObject(objectPath)) { + return false; + } + } + + return parentType?.definition.creatable && Array.isArray(parent?.composition); + } + + startTransaction() { + if (!this.openmct.objects.isTransactionActive()) { + this.#transaction = this.openmct.objects.startTransaction(); + } + } + + async saveTransaction() { + if (!this.#transaction) { + return; + } + + await this.#transaction.commit(); + this.openmct.objects.endTransaction(); + this.#transaction = null; + } } diff --git a/src/plugins/remove/plugin.js b/src/plugins/remove/plugin.js index 60ff4eb055..03cd1e878a 100644 --- a/src/plugins/remove/plugin.js +++ b/src/plugins/remove/plugin.js @@ -19,10 +19,10 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import RemoveAction from "./RemoveAction"; +import RemoveAction from './RemoveAction'; export default function () { - return function (openmct) { - openmct.actions.register(new RemoveAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new RemoveAction(openmct)); + }; } diff --git a/src/plugins/remove/pluginSpec.js b/src/plugins/remove/pluginSpec.js index 0c592be8c7..404546610f 100644 --- a/src/plugins/remove/pluginSpec.js +++ b/src/plugins/remove/pluginSpec.js @@ -19,119 +19,112 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState, - getMockObjects -} from 'utils/testing'; +import { createOpenMct, resetApplicationState, getMockObjects } from 'utils/testing'; -describe("The Remove Action plugin", () => { +describe('The Remove Action plugin', () => { + let openmct; + let removeAction; + let childObject; + let parentObject; - let openmct; - let removeAction; - let childObject; - let parentObject; + // this setups up the app + beforeEach((done) => { + openmct = createOpenMct(); - // this setups up the app - beforeEach((done) => { - openmct = createOpenMct(); + childObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Child Folder', + identifier: { + namespace: '', + key: 'child-folder-object' + } + } + } + }).folder; + parentObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + identifier: { + namespace: '', + key: 'parent-folder-object' + }, + name: 'Parent Folder', + composition: [childObject.identifier] + } + } + }).folder; - childObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Child Folder", - identifier: { - namespace: "", - key: "child-folder-object" - } - } - } - }).folder; - parentObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - identifier: { - namespace: "", - key: "parent-folder-object" - }, - name: "Parent Folder", - composition: [childObject.identifier] - } - } - }).folder; + openmct.on('start', done); + openmct.startHeadless(); - openmct.on('start', done); - openmct.startHeadless(); + removeAction = openmct.actions._allActions.remove; + }); - removeAction = openmct.actions._allActions.remove; + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('should be defined', () => { + expect(removeAction).toBeDefined(); + }); + + describe('when removing an object from a parent composition', () => { + beforeEach(() => { + spyOn(removeAction, 'removeFromComposition').and.callThrough(); + spyOn(removeAction, 'inNavigationPath').and.returnValue(false); + spyOn(openmct.objects, 'mutate').and.callThrough(); + spyOn(openmct.objects, 'startTransaction').and.callThrough(); + spyOn(openmct.objects, 'endTransaction').and.callThrough(); + removeAction.removeFromComposition(parentObject, childObject); }); - afterEach(() => { - return resetApplicationState(openmct); + it('removeFromComposition should be called with the parent and child', () => { + expect(removeAction.removeFromComposition).toHaveBeenCalled(); + expect(removeAction.removeFromComposition).toHaveBeenCalledWith(parentObject, childObject); }); - it("should be defined", () => { - expect(removeAction).toBeDefined(); + it('it should mutate the parent object', () => { + expect(openmct.objects.mutate).toHaveBeenCalled(); + expect(openmct.objects.mutate.calls.argsFor(0)[0]).toEqual(parentObject); }); - describe("when removing an object from a parent composition", () => { - - beforeEach(() => { - spyOn(removeAction, 'removeFromComposition').and.callThrough(); - spyOn(removeAction, 'inNavigationPath').and.returnValue(false); - spyOn(openmct.objects, 'mutate').and.callThrough(); - spyOn(openmct.objects, 'startTransaction').and.callThrough(); - spyOn(openmct.objects, 'endTransaction').and.callThrough(); - removeAction.removeFromComposition(parentObject, childObject); - }); - - it("removeFromComposition should be called with the parent and child", () => { - expect(removeAction.removeFromComposition).toHaveBeenCalled(); - expect(removeAction.removeFromComposition).toHaveBeenCalledWith(parentObject, childObject); - }); - - it("it should mutate the parent object", () => { - expect(openmct.objects.mutate).toHaveBeenCalled(); - expect(openmct.objects.mutate.calls.argsFor(0)[0]).toEqual(parentObject); - }); - - it("it should start a transaction", () => { - expect(openmct.objects.startTransaction).toHaveBeenCalled(); - }); - - it("it should end the transaction", (done) => { - setTimeout(() => { - expect(openmct.objects.endTransaction).toHaveBeenCalled(); - done(); - }, 100); - }); + it('it should start a transaction', () => { + expect(openmct.objects.startTransaction).toHaveBeenCalled(); }); - describe("when determining the object is applicable", () => { - - beforeEach(() => { - spyOn(removeAction, 'appliesTo').and.callThrough(); - }); - - it("should be true when the parent is creatable and has composition", () => { - let applies = removeAction.appliesTo([childObject, parentObject]); - expect(applies).toBe(true); - }); - - it("should be false when the child is locked and not an alias", () => { - childObject.locked = true; - childObject.location = 'parent-folder-object'; - let applies = removeAction.appliesTo([childObject, parentObject]); - expect(applies).toBe(false); - }); - - it("should be true when the child is locked and IS an alias", () => { - childObject.locked = true; - childObject.location = 'other-folder-object'; - let applies = removeAction.appliesTo([childObject, parentObject]); - expect(applies).toBe(true); - }); + it('it should end the transaction', (done) => { + setTimeout(() => { + expect(openmct.objects.endTransaction).toHaveBeenCalled(); + done(); + }, 100); }); + }); + + describe('when determining the object is applicable', () => { + beforeEach(() => { + spyOn(removeAction, 'appliesTo').and.callThrough(); + }); + + it('should be true when the parent is creatable and has composition', () => { + let applies = removeAction.appliesTo([childObject, parentObject]); + expect(applies).toBe(true); + }); + + it('should be false when the child is locked and not an alias', () => { + childObject.locked = true; + childObject.location = 'parent-folder-object'; + let applies = removeAction.appliesTo([childObject, parentObject]); + expect(applies).toBe(false); + }); + + it('should be true when the child is locked and IS an alias', () => { + childObject.locked = true; + childObject.location = 'other-folder-object'; + let applies = removeAction.appliesTo([childObject, parentObject]); + expect(applies).toBe(true); + }); + }); }); diff --git a/src/plugins/staticRootPlugin/StaticModelProvider.js b/src/plugins/staticRootPlugin/StaticModelProvider.js index 4bf9ffbca3..9d2248d7a5 100644 --- a/src/plugins/staticRootPlugin/StaticModelProvider.js +++ b/src/plugins/staticRootPlugin/StaticModelProvider.js @@ -29,159 +29,171 @@ import objectUtils from 'objectUtils'; class StaticModelProvider { - constructor(importData, rootIdentifier) { - this.objectMap = {}; - this.rewriteModel(importData, rootIdentifier); + constructor(importData, rootIdentifier) { + this.objectMap = {}; + this.rewriteModel(importData, rootIdentifier); + } + + /** + * Standard "Get". + */ + get(identifier) { + const keyString = objectUtils.makeKeyString(identifier); + if (this.objectMap[keyString]) { + return this.objectMap[keyString]; } - /** - * Standard "Get". - */ - get(identifier) { - const keyString = objectUtils.makeKeyString(identifier); - if (this.objectMap[keyString]) { - return this.objectMap[keyString]; - } + throw new Error(keyString + ' not found in import models.'); + } - throw new Error(keyString + ' not found in import models.'); + parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) { + Object.keys(objectLeaf).forEach((nodeKey) => { + if (idMap.get(nodeKey)) { + const newIdentifier = objectUtils.makeKeyString({ + namespace: newRootNamespace, + key: idMap.get(nodeKey) + }); + objectLeaf[newIdentifier] = { ...objectLeaf[nodeKey] }; + delete objectLeaf[nodeKey]; + objectLeaf[newIdentifier] = this.parseTreeLeaf( + newIdentifier, + objectLeaf[newIdentifier], + idMap, + newRootNamespace, + oldRootNamespace + ); + } else { + objectLeaf[nodeKey] = this.parseTreeLeaf( + nodeKey, + objectLeaf[nodeKey], + idMap, + newRootNamespace, + oldRootNamespace + ); + } + }); + + return objectLeaf; + } + + parseArrayLeaf(arrayLeaf, idMap, newRootNamespace, oldRootNamespace) { + return arrayLeaf.map((leafValue, index) => + this.parseTreeLeaf(null, leafValue, idMap, newRootNamespace, oldRootNamespace) + ); + } + + parseBranchedLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace) { + if (Array.isArray(branchedLeafValue)) { + return this.parseArrayLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace); + } else { + return this.parseObjectLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace); + } + } + + parseTreeLeaf(leafKey, leafValue, idMap, newRootNamespace, oldRootNamespace) { + if (leafValue === null || leafValue === undefined) { + return leafValue; } - parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) { - Object.keys(objectLeaf).forEach((nodeKey) => { - if (idMap.get(nodeKey)) { - const newIdentifier = objectUtils.makeKeyString({ - namespace: newRootNamespace, - key: idMap.get(nodeKey) - }); - objectLeaf[newIdentifier] = { ...objectLeaf[nodeKey] }; - delete objectLeaf[nodeKey]; - objectLeaf[newIdentifier] = this.parseTreeLeaf(newIdentifier, objectLeaf[newIdentifier], idMap, newRootNamespace, oldRootNamespace); - } else { - objectLeaf[nodeKey] = this.parseTreeLeaf(nodeKey, objectLeaf[nodeKey], idMap, newRootNamespace, oldRootNamespace); - } + const hasChild = typeof leafValue === 'object'; + if (hasChild) { + return this.parseBranchedLeaf(leafValue, idMap, newRootNamespace, oldRootNamespace); + } + + if (leafKey === 'key') { + let mappedLeafValue; + if (oldRootNamespace) { + mappedLeafValue = idMap.get( + objectUtils.makeKeyString({ + namespace: oldRootNamespace, + key: leafValue + }) + ); + } else { + mappedLeafValue = idMap.get(leafValue); + } + + return mappedLeafValue ?? leafValue; + } else if (leafKey === 'namespace') { + // Only rewrite the namespace if it matches the old root namespace. + // This is to prevent rewriting namespaces of objects that are not + // children of the root object (e.g.: objects from a telemetry dictionary) + return leafValue === oldRootNamespace ? newRootNamespace : leafValue; + } else if (leafKey === 'location') { + const mappedLeafValue = idMap.get(leafValue); + if (!mappedLeafValue) { + return null; + } + + const newLocationIdentifier = objectUtils.makeKeyString({ + namespace: newRootNamespace, + key: mappedLeafValue + }); + + return newLocationIdentifier; + } else { + const mappedLeafValue = idMap.get(leafValue); + if (mappedLeafValue) { + const newIdentifier = objectUtils.makeKeyString({ + namespace: newRootNamespace, + key: mappedLeafValue }); - return objectLeaf; + return newIdentifier; + } else { + return leafValue; + } } + } - parseArrayLeaf(arrayLeaf, idMap, newRootNamespace, oldRootNamespace) { - return arrayLeaf.map((leafValue, index) => this.parseTreeLeaf( - null, leafValue, idMap, newRootNamespace, oldRootNamespace)); - } + rewriteObjectIdentifiers(importData, rootIdentifier) { + const { namespace: oldRootNamespace } = objectUtils.parseKeyString(importData.rootId); + const { namespace: newRootNamespace } = rootIdentifier; + const idMap = new Map(); + const objectTree = importData.openmct; - parseBranchedLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace) { - if (Array.isArray(branchedLeafValue)) { - return this.parseArrayLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace); - } else { - return this.parseObjectLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace); - } - } + Object.keys(objectTree).forEach((originalId, index) => { + let newId = index.toString(); + if (originalId === importData.rootId) { + newId = rootIdentifier.key; + } - parseTreeLeaf(leafKey, leafValue, idMap, newRootNamespace, oldRootNamespace) { - if (leafValue === null || leafValue === undefined) { - return leafValue; - } + idMap.set(originalId, newId); + }); - const hasChild = typeof leafValue === 'object'; - if (hasChild) { - return this.parseBranchedLeaf(leafValue, idMap, newRootNamespace, oldRootNamespace); - } + const newTree = this.parseTreeLeaf(null, objectTree, idMap, newRootNamespace, oldRootNamespace); - if (leafKey === 'key') { - let mappedLeafValue; - if (oldRootNamespace) { - mappedLeafValue = idMap.get(objectUtils.makeKeyString({ - namespace: oldRootNamespace, - key: leafValue - })); - } else { - mappedLeafValue = idMap.get(leafValue); - } + return newTree; + } - return mappedLeafValue ?? leafValue; - } else if (leafKey === 'namespace') { - // Only rewrite the namespace if it matches the old root namespace. - // This is to prevent rewriting namespaces of objects that are not - // children of the root object (e.g.: objects from a telemetry dictionary) - return leafValue === oldRootNamespace - ? newRootNamespace - : leafValue; - } else if (leafKey === 'location') { - const mappedLeafValue = idMap.get(leafValue); - if (!mappedLeafValue) { - return null; - } + /** + * Converts all objects in an object make from old format objects to new + * format objects. + */ + convertToNewObjects(oldObjectMap) { + return Object.keys(oldObjectMap).reduce(function (newObjectMap, key) { + newObjectMap[key] = objectUtils.toNewFormat(oldObjectMap[key], key); - const newLocationIdentifier = objectUtils.makeKeyString({ - namespace: newRootNamespace, - key: mappedLeafValue - }); + return newObjectMap; + }, {}); + } - return newLocationIdentifier; - } else { - const mappedLeafValue = idMap.get(leafValue); - if (mappedLeafValue) { - const newIdentifier = objectUtils.makeKeyString({ - namespace: newRootNamespace, - key: mappedLeafValue - }); + /* Set the root location correctly for a top-level object */ + setRootLocation(objectMap, rootIdentifier) { + objectMap[objectUtils.makeKeyString(rootIdentifier)].location = 'ROOT'; - return newIdentifier; - } else { - return leafValue; - } - } - } + return objectMap; + } - rewriteObjectIdentifiers(importData, rootIdentifier) { - const { namespace: oldRootNamespace } = objectUtils.parseKeyString(importData.rootId); - const { namespace: newRootNamespace } = rootIdentifier; - const idMap = new Map(); - const objectTree = importData.openmct; - - Object.keys(objectTree).forEach((originalId, index) => { - let newId = index.toString(); - if (originalId === importData.rootId) { - newId = rootIdentifier.key; - } - - idMap.set(originalId, newId); - }); - - const newTree = this.parseTreeLeaf(null, objectTree, idMap, newRootNamespace, oldRootNamespace); - - return newTree; - } - - /** - * Converts all objects in an object make from old format objects to new - * format objects. - */ - convertToNewObjects(oldObjectMap) { - return Object.keys(oldObjectMap) - .reduce(function (newObjectMap, key) { - newObjectMap[key] = objectUtils.toNewFormat(oldObjectMap[key], key); - - return newObjectMap; - }, {}); - } - - /* Set the root location correctly for a top-level object */ - setRootLocation(objectMap, rootIdentifier) { - objectMap[objectUtils.makeKeyString(rootIdentifier)].location = 'ROOT'; - - return objectMap; - } - - /** - * Takes importData (as provided by the ImportExport plugin) and exposes - * an object provider to fetch those objects. - */ - rewriteModel(importData, rootIdentifier) { - const oldFormatObjectMap = this.rewriteObjectIdentifiers(importData, rootIdentifier); - const newFormatObjectMap = this.convertToNewObjects(oldFormatObjectMap); - this.objectMap = this.setRootLocation(newFormatObjectMap, rootIdentifier); - } + /** + * Takes importData (as provided by the ImportExport plugin) and exposes + * an object provider to fetch those objects. + */ + rewriteModel(importData, rootIdentifier) { + const oldFormatObjectMap = this.rewriteObjectIdentifiers(importData, rootIdentifier); + const newFormatObjectMap = this.convertToNewObjects(oldFormatObjectMap); + this.objectMap = this.setRootLocation(newFormatObjectMap, rootIdentifier); + } } export default StaticModelProvider; diff --git a/src/plugins/staticRootPlugin/StaticModelProviderSpec.js b/src/plugins/staticRootPlugin/StaticModelProviderSpec.js index 347a160dd9..e8a70a4ffe 100644 --- a/src/plugins/staticRootPlugin/StaticModelProviderSpec.js +++ b/src/plugins/staticRootPlugin/StaticModelProviderSpec.js @@ -25,260 +25,255 @@ import testStaticDataFooNamespace from './test-data/static-provider-test-foo-nam import StaticModelProvider from './StaticModelProvider'; describe('StaticModelProvider', function () { - describe('with empty namespace', function () { + describe('with empty namespace', function () { + let staticProvider; - let staticProvider; - - beforeEach(function () { - const staticData = JSON.parse(JSON.stringify(testStaticDataEmptyNamespace)); - staticProvider = new StaticModelProvider(staticData, { - namespace: 'my-import', - key: 'root' - }); - }); - - describe('rootObject', function () { - let rootModel; - - beforeEach(function () { - rootModel = staticProvider.get({ - namespace: 'my-import', - key: 'root' - }); - }); - - it('is located at top level', function () { - expect(rootModel.location).toBe('ROOT'); - }); - - it('has remapped identifier', function () { - expect(rootModel.identifier).toEqual({ - namespace: 'my-import', - key: 'root' - }); - }); - - it('has remapped identifiers in composition', function () { - expect(rootModel.composition).toContain({ - namespace: 'my-import', - key: '1' - }); - expect(rootModel.composition).toContain({ - namespace: 'my-import', - key: '2' - }); - }); - }); - - describe('childObjects', function () { - let swg; - let layout; - let fixed; - - beforeEach(function () { - swg = staticProvider.get({ - namespace: 'my-import', - key: '1' - }); - layout = staticProvider.get({ - namespace: 'my-import', - key: '2' - }); - fixed = staticProvider.get({ - namespace: 'my-import', - key: '3' - }); - }); - - it('match expected ordering', function () { - // this is a sanity check to make sure the identifiers map in - // the correct order. - expect(swg.type).toBe('generator'); - expect(layout.type).toBe('layout'); - expect(fixed.type).toBe('telemetry.fixed'); - }); - - it('have remapped identifiers', function () { - expect(swg.identifier).toEqual({ - namespace: 'my-import', - key: '1' - }); - expect(layout.identifier).toEqual({ - namespace: 'my-import', - key: '2' - }); - expect(fixed.identifier).toEqual({ - namespace: 'my-import', - key: '3' - }); - }); - - it('have remapped composition', function () { - expect(layout.composition).toContain({ - namespace: 'my-import', - key: '1' - }); - expect(layout.composition).toContain({ - namespace: 'my-import', - key: '3' - }); - expect(fixed.composition).toContain({ - namespace: 'my-import', - key: '1' - }); - }); - - it('rewrites locations', function () { - expect(swg.location).toBe('my-import:root'); - expect(layout.location).toBe('my-import:root'); - expect(fixed.location).toBe('my-import:2'); - }); - - it('rewrites matched identifiers in objects', function () { - expect(layout.configuration.layout.panels['my-import:1']) - .toBeDefined(); - expect(layout.configuration.layout.panels['my-import:3']) - .toBeDefined(); - expect(layout.configuration.layout.panels['483c00d4-bb1d-4b42-b29a-c58e06b322a0']) - .not.toBeDefined(); - expect(layout.configuration.layout.panels['20273193-f069-49e9-b4f7-b97a87ed755d']) - .not.toBeDefined(); - expect(fixed.configuration['fixed-display'].elements[0].id) - .toBe('my-import:1'); - }); - - }); + beforeEach(function () { + const staticData = JSON.parse(JSON.stringify(testStaticDataEmptyNamespace)); + staticProvider = new StaticModelProvider(staticData, { + namespace: 'my-import', + key: 'root' + }); }); - describe('with namespace "foo"', function () { - let staticProvider; + describe('rootObject', function () { + let rootModel; - beforeEach(function () { - const staticData = JSON.parse(JSON.stringify(testStaticDataFooNamespace)); - staticProvider = new StaticModelProvider(staticData, { - namespace: 'my-import', - key: 'root' - }); + beforeEach(function () { + rootModel = staticProvider.get({ + namespace: 'my-import', + key: 'root' }); + }); - describe('rootObject', function () { - let rootModel; + it('is located at top level', function () { + expect(rootModel.location).toBe('ROOT'); + }); - beforeEach(function () { - rootModel = staticProvider.get({ - namespace: 'my-import', - key: 'root' - }); - }); - - it('is located at top level', function () { - expect(rootModel.location).toBe('ROOT'); - }); - - it('has remapped identifier', function () { - expect(rootModel.identifier).toEqual({ - namespace: 'my-import', - key: 'root' - }); - }); - - it('has remapped composition', function () { - expect(rootModel.composition).toContain({ - namespace: 'my-import', - key: '1' - }); - expect(rootModel.composition).toContain({ - namespace: 'my-import', - key: '2' - }); - }); + it('has remapped identifier', function () { + expect(rootModel.identifier).toEqual({ + namespace: 'my-import', + key: 'root' }); + }); - describe('childObjects', function () { - let clock; - let layout; - let swg; - let folder; - - beforeEach(function () { - folder = staticProvider.get({ - namespace: 'my-import', - key: 'root' - }); - layout = staticProvider.get({ - namespace: 'my-import', - key: '1' - }); - swg = staticProvider.get({ - namespace: 'my-import', - key: '2' - }); - clock = staticProvider.get({ - namespace: 'my-import', - key: '3' - }); - }); - - it('match expected ordering', function () { - // this is a sanity check to make sure the identifiers map in - // the correct order. - expect(folder.type).toBe('folder'); - expect(swg.type).toBe('generator'); - expect(layout.type).toBe('layout'); - expect(clock.type).toBe('clock'); - }); - - it('have remapped identifiers', function () { - expect(folder.identifier).toEqual({ - namespace: 'my-import', - key: 'root' - }); - expect(layout.identifier).toEqual({ - namespace: 'my-import', - key: '1' - }); - expect(swg.identifier).toEqual({ - namespace: 'my-import', - key: '2' - }); - expect(clock.identifier).toEqual({ - namespace: 'my-import', - key: '3' - }); - }); - - it('have remapped identifiers in composition', function () { - expect(layout.composition).toContain({ - namespace: 'my-import', - key: '2' - }); - expect(layout.composition).toContain({ - namespace: 'my-import', - key: '3' - }); - }); - - it('layout has remapped identifiers in configuration', function () { - const identifiers = layout.configuration.items - .map(item => item.identifier) - .filter(identifier => identifier !== undefined); - expect(identifiers).toContain({ - namespace: 'my-import', - key: '2' - }); - expect(identifiers).toContain({ - namespace: 'my-import', - key: '3' - }); - }); - - it('rewrites locations', function () { - expect(folder.location).toBe('ROOT'); - expect(swg.location).toBe('my-import:root'); - expect(layout.location).toBe('my-import:root'); - expect(clock.location).toBe('my-import:root'); - }); + it('has remapped identifiers in composition', function () { + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '1' }); + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '2' + }); + }); }); + + describe('childObjects', function () { + let swg; + let layout; + let fixed; + + beforeEach(function () { + swg = staticProvider.get({ + namespace: 'my-import', + key: '1' + }); + layout = staticProvider.get({ + namespace: 'my-import', + key: '2' + }); + fixed = staticProvider.get({ + namespace: 'my-import', + key: '3' + }); + }); + + it('match expected ordering', function () { + // this is a sanity check to make sure the identifiers map in + // the correct order. + expect(swg.type).toBe('generator'); + expect(layout.type).toBe('layout'); + expect(fixed.type).toBe('telemetry.fixed'); + }); + + it('have remapped identifiers', function () { + expect(swg.identifier).toEqual({ + namespace: 'my-import', + key: '1' + }); + expect(layout.identifier).toEqual({ + namespace: 'my-import', + key: '2' + }); + expect(fixed.identifier).toEqual({ + namespace: 'my-import', + key: '3' + }); + }); + + it('have remapped composition', function () { + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '3' + }); + expect(fixed.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + }); + + it('rewrites locations', function () { + expect(swg.location).toBe('my-import:root'); + expect(layout.location).toBe('my-import:root'); + expect(fixed.location).toBe('my-import:2'); + }); + + it('rewrites matched identifiers in objects', function () { + expect(layout.configuration.layout.panels['my-import:1']).toBeDefined(); + expect(layout.configuration.layout.panels['my-import:3']).toBeDefined(); + expect( + layout.configuration.layout.panels['483c00d4-bb1d-4b42-b29a-c58e06b322a0'] + ).not.toBeDefined(); + expect( + layout.configuration.layout.panels['20273193-f069-49e9-b4f7-b97a87ed755d'] + ).not.toBeDefined(); + expect(fixed.configuration['fixed-display'].elements[0].id).toBe('my-import:1'); + }); + }); + }); + describe('with namespace "foo"', function () { + let staticProvider; + + beforeEach(function () { + const staticData = JSON.parse(JSON.stringify(testStaticDataFooNamespace)); + staticProvider = new StaticModelProvider(staticData, { + namespace: 'my-import', + key: 'root' + }); + }); + + describe('rootObject', function () { + let rootModel; + + beforeEach(function () { + rootModel = staticProvider.get({ + namespace: 'my-import', + key: 'root' + }); + }); + + it('is located at top level', function () { + expect(rootModel.location).toBe('ROOT'); + }); + + it('has remapped identifier', function () { + expect(rootModel.identifier).toEqual({ + namespace: 'my-import', + key: 'root' + }); + }); + + it('has remapped composition', function () { + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '2' + }); + }); + }); + + describe('childObjects', function () { + let clock; + let layout; + let swg; + let folder; + + beforeEach(function () { + folder = staticProvider.get({ + namespace: 'my-import', + key: 'root' + }); + layout = staticProvider.get({ + namespace: 'my-import', + key: '1' + }); + swg = staticProvider.get({ + namespace: 'my-import', + key: '2' + }); + clock = staticProvider.get({ + namespace: 'my-import', + key: '3' + }); + }); + + it('match expected ordering', function () { + // this is a sanity check to make sure the identifiers map in + // the correct order. + expect(folder.type).toBe('folder'); + expect(swg.type).toBe('generator'); + expect(layout.type).toBe('layout'); + expect(clock.type).toBe('clock'); + }); + + it('have remapped identifiers', function () { + expect(folder.identifier).toEqual({ + namespace: 'my-import', + key: 'root' + }); + expect(layout.identifier).toEqual({ + namespace: 'my-import', + key: '1' + }); + expect(swg.identifier).toEqual({ + namespace: 'my-import', + key: '2' + }); + expect(clock.identifier).toEqual({ + namespace: 'my-import', + key: '3' + }); + }); + + it('have remapped identifiers in composition', function () { + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '2' + }); + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '3' + }); + }); + + it('layout has remapped identifiers in configuration', function () { + const identifiers = layout.configuration.items + .map((item) => item.identifier) + .filter((identifier) => identifier !== undefined); + expect(identifiers).toContain({ + namespace: 'my-import', + key: '2' + }); + expect(identifiers).toContain({ + namespace: 'my-import', + key: '3' + }); + }); + + it('rewrites locations', function () { + expect(folder.location).toBe('ROOT'); + expect(swg.location).toBe('my-import:root'); + expect(layout.location).toBe('my-import:root'); + expect(clock.location).toBe('my-import:root'); + }); + }); + }); }); - diff --git a/src/plugins/staticRootPlugin/plugin.js b/src/plugins/staticRootPlugin/plugin.js index d87251bd0e..fb6822ac8b 100644 --- a/src/plugins/staticRootPlugin/plugin.js +++ b/src/plugins/staticRootPlugin/plugin.js @@ -23,41 +23,41 @@ import StaticModelProvider from './StaticModelProvider'; export default function StaticRootPlugin(options) { - const rootIdentifier = { - namespace: options.namespace, - key: 'root' - }; + const rootIdentifier = { + namespace: options.namespace, + key: 'root' + }; - let cachedProvider; + let cachedProvider; - function loadProvider() { - return fetch(options.exportUrl) - .then(function (response) { - return response.json(); - }) - .then(function (importData) { - cachedProvider = new StaticModelProvider(importData, rootIdentifier); + function loadProvider() { + return fetch(options.exportUrl) + .then(function (response) { + return response.json(); + }) + .then(function (importData) { + cachedProvider = new StaticModelProvider(importData, rootIdentifier); - return cachedProvider; - }); + return cachedProvider; + }); + } + + function getProvider() { + if (!cachedProvider) { + cachedProvider = loadProvider(); } - function getProvider() { - if (!cachedProvider) { - cachedProvider = loadProvider(); - } + return Promise.resolve(cachedProvider); + } - return Promise.resolve(cachedProvider); - } - - return function install(openmct) { - openmct.objects.addRoot(rootIdentifier); - openmct.objects.addProvider(options.namespace, { - get: function (identifier) { - return getProvider().then(function (provider) { - return provider.get(identifier); - }); - } + return function install(openmct) { + openmct.objects.addRoot(rootIdentifier); + openmct.objects.addProvider(options.namespace, { + get: function (identifier) { + return getProvider().then(function (provider) { + return provider.get(identifier); }); - }; + } + }); + }; } diff --git a/src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json b/src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json index 8c523de4a7..0011a32ed3 100644 --- a/src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json +++ b/src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json @@ -1 +1,104 @@ -{"openmct":{"a9122832-4b6e-43ea-8219-5359c14c5de8":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","d2ac3ae4-0af2-49fe-81af-adac09936215"],"name":"import-provider-test","type":"folder","notes":null,"modified":1508522673278,"location":"mine","persisted":1508522673278},"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"telemetry":{"period":10,"amplitude":1,"offset":0,"dataRateInHz":1,"values":[{"key":"utc","name":"Time","format":"utc","hints":{"domain":1,"priority":0},"source":"utc"},{"key":"yesterday","name":"Yesterday","format":"utc","hints":{"domain":2,"priority":1},"source":"yesterday"},{"key":"sin","name":"Sine","hints":{"range":1,"priority":2},"source":"sin"},{"key":"cos","name":"Cosine","hints":{"range":2,"priority":3},"source":"cos"}]},"name":"SWG-10","type":"generator","modified":1508522652874,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522652874},"d2ac3ae4-0af2-49fe-81af-adac09936215":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","20273193-f069-49e9-b4f7-b97a87ed755d"],"name":"Layout","type":"layout","configuration":{"layout":{"panels":{"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"position":[0,0],"dimensions":[17,8]},"20273193-f069-49e9-b4f7-b97a87ed755d":{"position":[0,8],"dimensions":[17,1],"hasFrame":false}}}},"modified":1508522745580,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522745580},"20273193-f069-49e9-b4f7-b97a87ed755d":{"layoutGrid":[64,16],"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0"],"name":"FP Test","type":"telemetry.fixed","configuration":{"fixed-display":{"elements":[{"type":"fixed.telemetry","x":0,"y":0,"id":"483c00d4-bb1d-4b42-b29a-c58e06b322a0","stroke":"transparent","color":"","titled":true,"width":8,"height":2,"useGrid":true,"size":"24px"}]}},"modified":1508522717619,"location":"d2ac3ae4-0af2-49fe-81af-adac09936215","persisted":1508522717619}},"rootId":"a9122832-4b6e-43ea-8219-5359c14c5de8"} \ No newline at end of file +{ + "openmct": { + "a9122832-4b6e-43ea-8219-5359c14c5de8": { + "composition": [ + "483c00d4-bb1d-4b42-b29a-c58e06b322a0", + "d2ac3ae4-0af2-49fe-81af-adac09936215" + ], + "name": "import-provider-test", + "type": "folder", + "notes": null, + "modified": 1508522673278, + "location": "mine", + "persisted": 1508522673278 + }, + "483c00d4-bb1d-4b42-b29a-c58e06b322a0": { + "telemetry": { + "period": 10, + "amplitude": 1, + "offset": 0, + "dataRateInHz": 1, + "values": [ + { + "key": "utc", + "name": "Time", + "format": "utc", + "hints": { "domain": 1, "priority": 0 }, + "source": "utc" + }, + { + "key": "yesterday", + "name": "Yesterday", + "format": "utc", + "hints": { "domain": 2, "priority": 1 }, + "source": "yesterday" + }, + { "key": "sin", "name": "Sine", "hints": { "range": 1, "priority": 2 }, "source": "sin" }, + { + "key": "cos", + "name": "Cosine", + "hints": { "range": 2, "priority": 3 }, + "source": "cos" + } + ] + }, + "name": "SWG-10", + "type": "generator", + "modified": 1508522652874, + "location": "a9122832-4b6e-43ea-8219-5359c14c5de8", + "persisted": 1508522652874 + }, + "d2ac3ae4-0af2-49fe-81af-adac09936215": { + "composition": [ + "483c00d4-bb1d-4b42-b29a-c58e06b322a0", + "20273193-f069-49e9-b4f7-b97a87ed755d" + ], + "name": "Layout", + "type": "layout", + "configuration": { + "layout": { + "panels": { + "483c00d4-bb1d-4b42-b29a-c58e06b322a0": { "position": [0, 0], "dimensions": [17, 8] }, + "20273193-f069-49e9-b4f7-b97a87ed755d": { + "position": [0, 8], + "dimensions": [17, 1], + "hasFrame": false + } + } + } + }, + "modified": 1508522745580, + "location": "a9122832-4b6e-43ea-8219-5359c14c5de8", + "persisted": 1508522745580 + }, + "20273193-f069-49e9-b4f7-b97a87ed755d": { + "layoutGrid": [64, 16], + "composition": ["483c00d4-bb1d-4b42-b29a-c58e06b322a0"], + "name": "FP Test", + "type": "telemetry.fixed", + "configuration": { + "fixed-display": { + "elements": [ + { + "type": "fixed.telemetry", + "x": 0, + "y": 0, + "id": "483c00d4-bb1d-4b42-b29a-c58e06b322a0", + "stroke": "transparent", + "color": "", + "titled": true, + "width": 8, + "height": 2, + "useGrid": true, + "size": "24px" + } + ] + } + }, + "modified": 1508522717619, + "location": "d2ac3ae4-0af2-49fe-81af-adac09936215", + "persisted": 1508522717619 + } + }, + "rootId": "a9122832-4b6e-43ea-8219-5359c14c5de8" +} diff --git a/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json b/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json index 49dd9d5926..6ec6cd52f7 100644 --- a/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json +++ b/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json @@ -1 +1,120 @@ -{"openmct":{"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1":{"identifier":{"key":"a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","namespace":"foo"},"name":"Folder Foo","type":"folder","composition":[{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"modified":1681164966705,"location":"foo:mine","created":1681164829371,"persisted":1681164966706},"foo:95729018-86ed-4484-867d-10c63c41c5a1":{"identifier":{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},"name":"Display Layout Bar","type":"layout","composition":[{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"configuration":{"items":[{"fill":"#666666","stroke":"","x":42,"y":42,"width":20,"height":4,"type":"box-view","id":"14505a5d-b846-4504-961f-8c9bcdf19f39"},{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"x":0,"y":0,"width":40,"height":15,"displayMode":"all","value":"sin","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"05baa95f-2064-4cb0-ad9f-575758491220"},{"width":40,"height":15,"x":0,"y":15,"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"70e1b8b7-cd59-4a52-b796-d68fb0c48fc5"}],"layoutGrid":[10,10],"objectStyles":{"05baa95f-2064-4cb0-ad9f-575758491220":{"staticStyle":{"style":{"border":"1px solid #00ff00","backgroundColor":"#0000ff","color":"#ff00ff"}}}}},"modified":1681165037189,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164838178,"persisted":1681165037190},"foo:22c438f0-953b-42c5-8fb2-9d5dbeb88a0c":{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"name":"SWG Baz","type":"generator","telemetry":{"period":"20","amplitude":"2","offset":"5","dataRateInHz":1,"phase":0,"randomness":0,"loadDelay":0,"infinityValues":false,"staleness":false},"modified":1681164910719,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164903684,"persisted":1681164910719},"foo:3545554b-53c8-467d-a70d-e90d1a120e4a":{"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"name":"Clock Qux","type":"clock","configuration":{"baseFormat":"YYYY/MM/DD hh:mm:ss","use24":"clock12","timezone":"UTC"},"modified":1681164989837,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164966702,"persisted":1681164989837}},"rootId":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1"} \ No newline at end of file +{ + "openmct": { + "foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1": { + "identifier": { "key": "a6c9e745-89e5-4c3a-8f54-7a34711aaeb1", "namespace": "foo" }, + "name": "Folder Foo", + "type": "folder", + "composition": [ + { "key": "95729018-86ed-4484-867d-10c63c41c5a1", "namespace": "foo" }, + { "key": "22c438f0-953b-42c5-8fb2-9d5dbeb88a0c", "namespace": "foo" }, + { "key": "3545554b-53c8-467d-a70d-e90d1a120e4a", "namespace": "foo" } + ], + "modified": 1681164966705, + "location": "foo:mine", + "created": 1681164829371, + "persisted": 1681164966706 + }, + "foo:95729018-86ed-4484-867d-10c63c41c5a1": { + "identifier": { "key": "95729018-86ed-4484-867d-10c63c41c5a1", "namespace": "foo" }, + "name": "Display Layout Bar", + "type": "layout", + "composition": [ + { "key": "22c438f0-953b-42c5-8fb2-9d5dbeb88a0c", "namespace": "foo" }, + { "key": "3545554b-53c8-467d-a70d-e90d1a120e4a", "namespace": "foo" } + ], + "configuration": { + "items": [ + { + "fill": "#666666", + "stroke": "", + "x": 42, + "y": 42, + "width": 20, + "height": 4, + "type": "box-view", + "id": "14505a5d-b846-4504-961f-8c9bcdf19f39" + }, + { + "identifier": { "key": "22c438f0-953b-42c5-8fb2-9d5dbeb88a0c", "namespace": "foo" }, + "x": 0, + "y": 0, + "width": 40, + "height": 15, + "displayMode": "all", + "value": "sin", + "stroke": "", + "fill": "", + "color": "", + "fontSize": "default", + "font": "default", + "type": "telemetry-view", + "id": "05baa95f-2064-4cb0-ad9f-575758491220" + }, + { + "width": 40, + "height": 15, + "x": 0, + "y": 15, + "identifier": { "key": "3545554b-53c8-467d-a70d-e90d1a120e4a", "namespace": "foo" }, + "hasFrame": true, + "fontSize": "default", + "font": "default", + "type": "subobject-view", + "id": "70e1b8b7-cd59-4a52-b796-d68fb0c48fc5" + } + ], + "layoutGrid": [10, 10], + "objectStyles": { + "05baa95f-2064-4cb0-ad9f-575758491220": { + "staticStyle": { + "style": { + "border": "1px solid #00ff00", + "backgroundColor": "#0000ff", + "color": "#ff00ff" + } + } + } + } + }, + "modified": 1681165037189, + "location": "foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1", + "created": 1681164838178, + "persisted": 1681165037190 + }, + "foo:22c438f0-953b-42c5-8fb2-9d5dbeb88a0c": { + "identifier": { "key": "22c438f0-953b-42c5-8fb2-9d5dbeb88a0c", "namespace": "foo" }, + "name": "SWG Baz", + "type": "generator", + "telemetry": { + "period": "20", + "amplitude": "2", + "offset": "5", + "dataRateInHz": 1, + "phase": 0, + "randomness": 0, + "loadDelay": 0, + "infinityValues": false, + "staleness": false + }, + "modified": 1681164910719, + "location": "foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1", + "created": 1681164903684, + "persisted": 1681164910719 + }, + "foo:3545554b-53c8-467d-a70d-e90d1a120e4a": { + "identifier": { "key": "3545554b-53c8-467d-a70d-e90d1a120e4a", "namespace": "foo" }, + "name": "Clock Qux", + "type": "clock", + "configuration": { + "baseFormat": "YYYY/MM/DD hh:mm:ss", + "use24": "clock12", + "timezone": "UTC" + }, + "modified": 1681164989837, + "location": "foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1", + "created": 1681164966702, + "persisted": 1681164989837 + } + }, + "rootId": "foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1" +} diff --git a/src/plugins/summaryWidget/SummaryWidgetViewPolicy.js b/src/plugins/summaryWidget/SummaryWidgetViewPolicy.js index 0521d0a728..fcca9d21e0 100644 --- a/src/plugins/summaryWidget/SummaryWidgetViewPolicy.js +++ b/src/plugins/summaryWidget/SummaryWidgetViewPolicy.js @@ -20,27 +20,20 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ +define([], function () { + /** + * Policy determining which views can apply to summary widget. Disables + * any view other than normal summary widget view. + */ + function SummaryWidgetViewPolicy() {} -], function ( - -) { - - /** - * Policy determining which views can apply to summary widget. Disables - * any view other than normal summary widget view. - */ - function SummaryWidgetViewPolicy() { + SummaryWidgetViewPolicy.prototype.allow = function (view, domainObject) { + if (domainObject.getModel().type === 'summary-widget') { + return view.key === 'summary-widget-viewer'; } - SummaryWidgetViewPolicy.prototype.allow = function (view, domainObject) { - if (domainObject.getModel().type === 'summary-widget') { - return view.key === 'summary-widget-viewer'; - } + return true; + }; - return true; - - }; - - return SummaryWidgetViewPolicy; + return SummaryWidgetViewPolicy; }); diff --git a/src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js b/src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js index 9fc9a4528c..30342cb9fc 100644 --- a/src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js +++ b/src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js @@ -20,24 +20,20 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define( - [], - function () { +define([], function () { + function SummaryWidgetsCompositionPolicy(openmct) { + this.openmct = openmct; + } - function SummaryWidgetsCompositionPolicy(openmct) { - this.openmct = openmct; - } + SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) { + const parentType = parent.type; - SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) { - const parentType = parent.type; - - if (parentType === 'summary-widget' && !this.openmct.telemetry.isTelemetryObject(child)) { - return false; - } - - return true; - }; - - return SummaryWidgetsCompositionPolicy; + if (parentType === 'summary-widget' && !this.openmct.telemetry.isTelemetryObject(child)) { + return false; } -); + + return true; + }; + + return SummaryWidgetsCompositionPolicy; +}); diff --git a/src/plugins/summaryWidget/plugin.js b/src/plugins/summaryWidget/plugin.js index 3513cbe04f..e14993ad57 100755 --- a/src/plugins/summaryWidget/plugin.js +++ b/src/plugins/summaryWidget/plugin.js @@ -1,96 +1,98 @@ -define([ - './SummaryWidgetsCompositionPolicy', - './src/telemetry/SummaryWidgetMetadataProvider', - './src/telemetry/SummaryWidgetTelemetryProvider', - './src/views/SummaryWidgetViewProvider', - './SummaryWidgetViewPolicy' -], function ( - SummaryWidgetsCompositionPolicy, - SummaryWidgetMetadataProvider, - SummaryWidgetTelemetryProvider, - SummaryWidgetViewProvider, - SummaryWidgetViewPolicy -) { - - function plugin() { - - const widgetType = { - name: 'Summary Widget', - description: 'A compact status update for collections of telemetry-producing items', - cssClass: 'icon-summary-widget', - initialize: function (domainObject) { - domainObject.composition = []; - domainObject.configuration = { - ruleOrder: ['default'], - ruleConfigById: { - default: { - name: 'Default', - label: 'Unnamed Rule', - message: '', - id: 'default', - icon: ' ', - style: { - 'color': '#ffffff', - 'background-color': '#38761d', - 'border-color': 'rgba(0,0,0,0)' - }, - description: 'Default appearance for the widget', - conditions: [{ - object: '', - key: '', - operation: '', - values: [] - }], - jsCondition: '', - trigger: 'any', - expanded: 'true' - } - }, - testDataConfig: [{ - object: '', - key: '', - value: '' - }] - }; - domainObject.openNewTab = 'thisTab'; - domainObject.telemetry = {}; - }, - form: [ - { - "key": "url", - "name": "URL", - "control": "textfield", - "required": false, - "cssClass": "l-input-lg" - }, - { - "key": "openNewTab", - "name": "Tab to Open Hyperlink", - "control": "select", - "options": [ - { - "value": "thisTab", - "name": "Open in this tab" - }, - { - "value": "newTab", - "name": "Open in a new tab" - } - ], - "cssClass": "l-inline" - } - ] - }; - - return function install(openmct) { - openmct.types.addType('summary-widget', widgetType); - let compositionPolicy = new SummaryWidgetsCompositionPolicy(openmct); - openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy)); - openmct.telemetry.addProvider(new SummaryWidgetMetadataProvider(openmct)); - openmct.telemetry.addProvider(new SummaryWidgetTelemetryProvider(openmct)); - openmct.objectViews.addProvider(new SummaryWidgetViewProvider(openmct)); - }; - } - - return plugin; -}); +define([ + './SummaryWidgetsCompositionPolicy', + './src/telemetry/SummaryWidgetMetadataProvider', + './src/telemetry/SummaryWidgetTelemetryProvider', + './src/views/SummaryWidgetViewProvider', + './SummaryWidgetViewPolicy' +], function ( + SummaryWidgetsCompositionPolicy, + SummaryWidgetMetadataProvider, + SummaryWidgetTelemetryProvider, + SummaryWidgetViewProvider, + SummaryWidgetViewPolicy +) { + function plugin() { + const widgetType = { + name: 'Summary Widget', + description: 'A compact status update for collections of telemetry-producing items', + cssClass: 'icon-summary-widget', + initialize: function (domainObject) { + domainObject.composition = []; + domainObject.configuration = { + ruleOrder: ['default'], + ruleConfigById: { + default: { + name: 'Default', + label: 'Unnamed Rule', + message: '', + id: 'default', + icon: ' ', + style: { + color: '#ffffff', + 'background-color': '#38761d', + 'border-color': 'rgba(0,0,0,0)' + }, + description: 'Default appearance for the widget', + conditions: [ + { + object: '', + key: '', + operation: '', + values: [] + } + ], + jsCondition: '', + trigger: 'any', + expanded: 'true' + } + }, + testDataConfig: [ + { + object: '', + key: '', + value: '' + } + ] + }; + domainObject.openNewTab = 'thisTab'; + domainObject.telemetry = {}; + }, + form: [ + { + key: 'url', + name: 'URL', + control: 'textfield', + required: false, + cssClass: 'l-input-lg' + }, + { + key: 'openNewTab', + name: 'Tab to Open Hyperlink', + control: 'select', + options: [ + { + value: 'thisTab', + name: 'Open in this tab' + }, + { + value: 'newTab', + name: 'Open in a new tab' + } + ], + cssClass: 'l-inline' + } + ] + }; + + return function install(openmct) { + openmct.types.addType('summary-widget', widgetType); + let compositionPolicy = new SummaryWidgetsCompositionPolicy(openmct); + openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy)); + openmct.telemetry.addProvider(new SummaryWidgetMetadataProvider(openmct)); + openmct.telemetry.addProvider(new SummaryWidgetTelemetryProvider(openmct)); + openmct.objectViews.addProvider(new SummaryWidgetViewProvider(openmct)); + }; + } + + return plugin; +}); diff --git a/src/plugins/summaryWidget/res/conditionTemplate.html b/src/plugins/summaryWidget/res/conditionTemplate.html index ac5af862e6..aa0ec6cf73 100644 --- a/src/plugins/summaryWidget/res/conditionTemplate.html +++ b/src/plugins/summaryWidget/res/conditionTemplate.html @@ -1,11 +1,11 @@
    • - - - - - - - - - + + + + + + + + +
    • diff --git a/src/plugins/summaryWidget/res/input/paletteTemplate.html b/src/plugins/summaryWidget/res/input/paletteTemplate.html index 4a086bf703..5547e3724d 100644 --- a/src/plugins/summaryWidget/res/input/paletteTemplate.html +++ b/src/plugins/summaryWidget/res/input/paletteTemplate.html @@ -9,13 +9,13 @@
- -
-
-
-
-
+ +
+
+
+
+
diff --git a/src/plugins/summaryWidget/res/input/selectTemplate.html b/src/plugins/summaryWidget/res/input/selectTemplate.html index e49c69830e..830d7f728d 100644 --- a/src/plugins/summaryWidget/res/input/selectTemplate.html +++ b/src/plugins/summaryWidget/res/input/selectTemplate.html @@ -1,4 +1,3 @@ - - + + diff --git a/src/plugins/summaryWidget/res/ruleImageTemplate.html b/src/plugins/summaryWidget/res/ruleImageTemplate.html index fec0f79b00..9c06621476 100644 --- a/src/plugins/summaryWidget/res/ruleImageTemplate.html +++ b/src/plugins/summaryWidget/res/ruleImageTemplate.html @@ -1,3 +1,3 @@
-
+
diff --git a/src/plugins/summaryWidget/res/ruleTemplate.html b/src/plugins/summaryWidget/res/ruleTemplate.html index c05276a63d..8a528dc602 100644 --- a/src/plugins/summaryWidget/res/ruleTemplate.html +++ b/src/plugins/summaryWidget/res/ruleTemplate.html @@ -1,67 +1,72 @@
-
-
-
-
-
-
-
-
-
-
-
Default Title
-
Rule description goes here
-
- - -
-
-
-
    -
  • - - - - -
  • -
  • - - - - -
  • -
  • - - - - -
  • -
  • - - -
  • -
-
    -
  • - - - - -
  • -
  • - - - - -
  • -
-
+
+
+
+
+
+
+
+
+
+
+
Default Title
+
Rule description goes here
+
+ + +
- +
+
    +
  • + + + + +
  • +
  • + + + + +
  • +
  • + + + + +
  • +
  • + + +
  • +
+
    +
  • + + + + +
  • +
  • + + + + +
  • +
+
+
+
diff --git a/src/plugins/summaryWidget/res/testDataItemTemplate.html b/src/plugins/summaryWidget/res/testDataItemTemplate.html index b9b0ef2a87..c5206afc6f 100644 --- a/src/plugins/summaryWidget/res/testDataItemTemplate.html +++ b/src/plugins/summaryWidget/res/testDataItemTemplate.html @@ -1,16 +1,20 @@ -
-
    -
  • - - - - - - - - - - -
  • -
+
+
    +
  • + + + + + + + + + + +
  • +
diff --git a/src/plugins/summaryWidget/res/testDataTemplate.html b/src/plugins/summaryWidget/res/testDataTemplate.html index 6470589c93..53cc6ec909 100644 --- a/src/plugins/summaryWidget/res/testDataTemplate.html +++ b/src/plugins/summaryWidget/res/testDataTemplate.html @@ -1,17 +1,18 @@
-
-
- -
-
-
- -
-
+
+
+
+
+
+ +
+
+
diff --git a/src/plugins/summaryWidget/res/widgetTemplate.html b/src/plugins/summaryWidget/res/widgetTemplate.html index 3d26dfe586..ad72aad208 100755 --- a/src/plugins/summaryWidget/res/widgetTemplate.html +++ b/src/plugins/summaryWidget/res/widgetTemplate.html @@ -1,30 +1,40 @@ -
- -
-
Default Static Name
-
-
-
- You must add at least one telemetry object to edit this widget. -
-
-
-
- - Test Data Values -
-
-
- - Rules -
-
-
-
- -
-
-
-
\ No newline at end of file +
+ +
+
+ Default Static Name +
+
+
+
+ You must add at least one telemetry object to edit this widget. +
+
+
+
+ + Test Data Values +
+
+
+ + Rules +
+
+
+
+ +
+
+
+
diff --git a/src/plugins/summaryWidget/src/Condition.js b/src/plugins/summaryWidget/src/Condition.js index 66f9ecbeb2..972530e4fb 100644 --- a/src/plugins/summaryWidget/src/Condition.js +++ b/src/plugins/summaryWidget/src/Condition.js @@ -1,236 +1,237 @@ define([ - '../res/conditionTemplate.html', - './input/ObjectSelect', - './input/KeySelect', - './input/OperationSelect', - './eventHelpers', - '../../../utils/template/templateHelpers', - 'EventEmitter' + '../res/conditionTemplate.html', + './input/ObjectSelect', + './input/KeySelect', + './input/OperationSelect', + './eventHelpers', + '../../../utils/template/templateHelpers', + 'EventEmitter' ], function ( - conditionTemplate, - ObjectSelect, - KeySelect, - OperationSelect, - eventHelpers, - templateHelpers, - EventEmitter + conditionTemplate, + ObjectSelect, + KeySelect, + OperationSelect, + eventHelpers, + templateHelpers, + EventEmitter ) { + /** + * Represents an individual condition for a summary widget rule. Manages the + * associated inputs and view. + * @param {Object} conditionConfig The configurration for this condition, consisting + * of object, key, operation, and values fields + * @param {number} index the index of this Condition object in it's parent Rule's data model, + * to be injected into callbacks for removes + * @param {ConditionManager} conditionManager A ConditionManager instance for populating + * selects with configuration data + */ + function Condition(conditionConfig, index, conditionManager) { + eventHelpers.extend(this); + this.config = conditionConfig; + this.index = index; + this.conditionManager = conditionManager; + + this.domElement = templateHelpers.convertTemplateToHTML(conditionTemplate)[0]; + + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['remove', 'duplicate', 'change']; + + this.deleteButton = this.domElement.querySelector('.t-delete'); + this.duplicateButton = this.domElement.querySelector('.t-duplicate'); + + this.selects = {}; + this.valueInputs = []; + + this.remove = this.remove.bind(this); + this.duplicate = this.duplicate.bind(this); + + const self = this; + /** - * Represents an individual condition for a summary widget rule. Manages the - * associated inputs and view. - * @param {Object} conditionConfig The configurration for this condition, consisting - * of object, key, operation, and values fields - * @param {number} index the index of this Condition object in it's parent Rule's data model, - * to be injected into callbacks for removes - * @param {ConditionManager} conditionManager A ConditionManager instance for populating - * selects with configuration data + * Event handler for a change in one of this conditions' custom selects + * @param {string} value The new value of this selects + * @param {string} property The property of this condition to modify + * @private */ - function Condition(conditionConfig, index, conditionManager) { - eventHelpers.extend(this); - this.config = conditionConfig; - this.index = index; - this.conditionManager = conditionManager; + function onSelectChange(value, property) { + if (property === 'operation') { + self.generateValueInputs(value); + } - this.domElement = templateHelpers.convertTemplateToHTML(conditionTemplate)[0]; - - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['remove', 'duplicate', 'change']; - - this.deleteButton = this.domElement.querySelector('.t-delete'); - this.duplicateButton = this.domElement.querySelector('.t-duplicate'); - - this.selects = {}; - this.valueInputs = []; - - this.remove = this.remove.bind(this); - this.duplicate = this.duplicate.bind(this); - - const self = this; - - /** - * Event handler for a change in one of this conditions' custom selects - * @param {string} value The new value of this selects - * @param {string} property The property of this condition to modify - * @private - */ - function onSelectChange(value, property) { - if (property === 'operation') { - self.generateValueInputs(value); - } - - self.eventEmitter.emit('change', { - value: value, - property: property, - index: self.index - }); - } - - /** - * Event handler for this conditions value inputs - * @param {Event} event The oninput event that triggered this callback - * @private - */ - function onValueInput(event) { - const elem = event.target; - const value = isNaN(Number(elem.value)) ? elem.value : Number(elem.value); - const inputIndex = self.valueInputs.indexOf(elem); - - self.eventEmitter.emit('change', { - value: value, - property: 'values[' + inputIndex + ']', - index: self.index - }); - } - - this.listenTo(this.deleteButton, 'click', this.remove, this); - this.listenTo(this.duplicateButton, 'click', this.duplicate, this); - - this.selects.object = new ObjectSelect(this.config, this.conditionManager, [ - ['any', 'any telemetry'], - ['all', 'all telemetry'] - ]); - this.selects.key = new KeySelect(this.config, this.selects.object, this.conditionManager); - this.selects.operation = new OperationSelect( - this.config, - this.selects.key, - this.conditionManager, - function (value) { - onSelectChange(value, 'operation'); - }); - - this.selects.object.on('change', function (value) { - onSelectChange(value, 'object'); - }); - this.selects.key.on('change', function (value) { - onSelectChange(value, 'key'); - }); - - Object.values(this.selects).forEach(function (select) { - self.domElement.querySelector('.t-configuration').append(select.getDOM()); - }); - - this.listenTo(this.domElement.querySelector('.t-value-inputs'), 'input', onValueInput); + self.eventEmitter.emit('change', { + value: value, + property: property, + index: self.index + }); } - Condition.prototype.getDOM = function (container) { - return this.domElement; - }; - /** - * Register a callback with this condition: supported callbacks are remove, change, - * duplicate - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function + * Event handler for this conditions value inputs + * @param {Event} event The oninput event that triggered this callback + * @private */ - Condition.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); + function onValueInput(event) { + const elem = event.target; + const value = isNaN(Number(elem.value)) ? elem.value : Number(elem.value); + const inputIndex = self.valueInputs.indexOf(elem); + + self.eventEmitter.emit('change', { + value: value, + property: 'values[' + inputIndex + ']', + index: self.index + }); + } + + this.listenTo(this.deleteButton, 'click', this.remove, this); + this.listenTo(this.duplicateButton, 'click', this.duplicate, this); + + this.selects.object = new ObjectSelect(this.config, this.conditionManager, [ + ['any', 'any telemetry'], + ['all', 'all telemetry'] + ]); + this.selects.key = new KeySelect(this.config, this.selects.object, this.conditionManager); + this.selects.operation = new OperationSelect( + this.config, + this.selects.key, + this.conditionManager, + function (value) { + onSelectChange(value, 'operation'); + } + ); + + this.selects.object.on('change', function (value) { + onSelectChange(value, 'object'); + }); + this.selects.key.on('change', function (value) { + onSelectChange(value, 'key'); + }); + + Object.values(this.selects).forEach(function (select) { + self.domElement.querySelector('.t-configuration').append(select.getDOM()); + }); + + this.listenTo(this.domElement.querySelector('.t-value-inputs'), 'input', onValueInput); + } + + Condition.prototype.getDOM = function (container) { + return this.domElement; + }; + + /** + * Register a callback with this condition: supported callbacks are remove, change, + * duplicate + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + Condition.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } + }; + + /** + * Hide the appropriate inputs when this is the only condition + */ + Condition.prototype.hideButtons = function () { + this.deleteButton.style.display = 'none'; + }; + + /** + * Remove this condition from the configuration. Invokes any registered + * remove callbacks + */ + Condition.prototype.remove = function () { + this.eventEmitter.emit('remove', this.index); + this.destroy(); + }; + + Condition.prototype.destroy = function () { + this.stopListening(); + Object.values(this.selects).forEach(function (select) { + select.destroy(); + }); + }; + + /** + * Make a deep clone of this condition's configuration and invoke any duplicate + * callbacks with the cloned configuration and this rule's index + */ + Condition.prototype.duplicate = function () { + const sourceCondition = JSON.parse(JSON.stringify(this.config)); + this.eventEmitter.emit('duplicate', { + sourceCondition: sourceCondition, + index: this.index + }); + }; + + /** + * When an operation is selected, create the appropriate value inputs + * and add them to the view. If an operation is of type enum, create + * a drop-down menu instead. + * + * @param {string} operation The key of currently selected operation + */ + Condition.prototype.generateValueInputs = function (operation) { + const evaluator = this.conditionManager.getEvaluator(); + const inputArea = this.domElement.querySelector('.t-value-inputs'); + let inputCount; + let inputType; + let newInput; + let index = 0; + let emitChange = false; + + inputArea.innerHTML = ''; + this.valueInputs = []; + this.config.values = this.config.values || []; + + if (evaluator.getInputCount(operation)) { + inputCount = evaluator.getInputCount(operation); + inputType = evaluator.getInputType(operation); + + while (index < inputCount) { + if (inputType === 'select') { + const options = this.generateSelectOptions(); + + newInput = document.createElement('select'); + newInput.innerHTML = options; + + emitChange = true; + } else { + const defaultValue = inputType === 'number' ? 0 : ''; + const value = this.config.values[index] || defaultValue; + this.config.values[index] = value; + + newInput = document.createElement('input'); + newInput.type = `${inputType}`; + newInput.value = `${value}`; } - }; - /** - * Hide the appropriate inputs when this is the only condition - */ - Condition.prototype.hideButtons = function () { - this.deleteButton.style.display = 'none'; - }; + this.valueInputs.push(newInput); + inputArea.appendChild(newInput); + index += 1; + } - /** - * Remove this condition from the configuration. Invokes any registered - * remove callbacks - */ - Condition.prototype.remove = function () { - this.eventEmitter.emit('remove', this.index); - this.destroy(); - }; - - Condition.prototype.destroy = function () { - this.stopListening(); - Object.values(this.selects).forEach(function (select) { - select.destroy(); + if (emitChange) { + this.eventEmitter.emit('change', { + value: Number(newInput[0].options[0].value), + property: 'values[0]', + index: this.index }); - }; + } + } + }; - /** - * Make a deep clone of this condition's configuration and invoke any duplicate - * callbacks with the cloned configuration and this rule's index - */ - Condition.prototype.duplicate = function () { - const sourceCondition = JSON.parse(JSON.stringify(this.config)); - this.eventEmitter.emit('duplicate', { - sourceCondition: sourceCondition, - index: this.index - }); - }; + Condition.prototype.generateSelectOptions = function () { + let telemetryMetadata = this.conditionManager.getTelemetryMetadata(this.config.object); + let options = ''; + telemetryMetadata[this.config.key].enumerations.forEach((enumeration) => { + options += ''; + }); - /** - * When an operation is selected, create the appropriate value inputs - * and add them to the view. If an operation is of type enum, create - * a drop-down menu instead. - * - * @param {string} operation The key of currently selected operation - */ - Condition.prototype.generateValueInputs = function (operation) { - const evaluator = this.conditionManager.getEvaluator(); - const inputArea = this.domElement.querySelector('.t-value-inputs'); - let inputCount; - let inputType; - let newInput; - let index = 0; - let emitChange = false; + return options; + }; - inputArea.innerHTML = ''; - this.valueInputs = []; - this.config.values = this.config.values || []; - - if (evaluator.getInputCount(operation)) { - inputCount = evaluator.getInputCount(operation); - inputType = evaluator.getInputType(operation); - - while (index < inputCount) { - if (inputType === 'select') { - const options = this.generateSelectOptions(); - - newInput = document.createElement("select"); - newInput.innerHTML = options; - - emitChange = true; - } else { - const defaultValue = inputType === 'number' ? 0 : ''; - const value = this.config.values[index] || defaultValue; - this.config.values[index] = value; - - newInput = document.createElement("input"); - newInput.type = `${inputType}`; - newInput.value = `${value}`; - } - - this.valueInputs.push(newInput); - inputArea.appendChild(newInput); - index += 1; - } - - if (emitChange) { - this.eventEmitter.emit('change', { - value: Number(newInput[0].options[0].value), - property: 'values[0]', - index: this.index - }); - } - } - }; - - Condition.prototype.generateSelectOptions = function () { - let telemetryMetadata = this.conditionManager.getTelemetryMetadata(this.config.object); - let options = ''; - telemetryMetadata[this.config.key].enumerations.forEach(enumeration => { - options += ''; - }); - - return options; - }; - - return Condition; + return Condition; }); diff --git a/src/plugins/summaryWidget/src/ConditionEvaluator.js b/src/plugins/summaryWidget/src/ConditionEvaluator.js index 6431690a93..1bef29b5c0 100644 --- a/src/plugins/summaryWidget/src/ConditionEvaluator.js +++ b/src/plugins/summaryWidget/src/ConditionEvaluator.js @@ -1,483 +1,486 @@ define([], function () { + /** + * Responsible for maintaining the possible operations for conditions + * in this widget, and evaluating the boolean value of conditions passed as + * input. + * @constructor + * @param {Object} subscriptionCache A cache consisting of the latest available + * data for any telemetry sources in the widget's + * composition. + * @param {Object} compositionObjs The current set of composition objects to + * evaluate for 'any' and 'all' conditions + */ + function ConditionEvaluator(subscriptionCache, compositionObjs) { + this.subscriptionCache = subscriptionCache; + this.compositionObjs = compositionObjs; + + this.testCache = {}; + this.useTestCache = false; /** - * Responsible for maintaining the possible operations for conditions - * in this widget, and evaluating the boolean value of conditions passed as - * input. - * @constructor - * @param {Object} subscriptionCache A cache consisting of the latest available - * data for any telemetry sources in the widget's - * composition. - * @param {Object} compositionObjs The current set of composition objects to - * evaluate for 'any' and 'all' conditions + * Maps value types to HTML input field types. These + * type of inputs will be generated by conditions expecting this data type */ - function ConditionEvaluator(subscriptionCache, compositionObjs) { - this.subscriptionCache = subscriptionCache; - this.compositionObjs = compositionObjs; + this.inputTypes = { + number: 'number', + string: 'text', + enum: 'select' + }; - this.testCache = {}; - this.useTestCache = false; + /** + * Functions to validate that the input to an operation is of the type + * that it expects, in order to prevent unexpected behavior. Will be + * invoked before the corresponding operation is executed + */ + this.inputValidators = { + number: this.validateNumberInput, + string: this.validateStringInput, + enum: this.validateNumberInput + }; - /** - * Maps value types to HTML input field types. These - * type of inputs will be generated by conditions expecting this data type - */ - this.inputTypes = { - number: 'number', - string: 'text', - enum: 'select' - }; + /** + * A library of operations supported by this rule evaluator. Each operation + * consists of the following fields: + * operation: a function with boolean return type to be invoked when this + * operation is used. Will be called with an array of inputs + * where input [0] is the telemetry value and input [1..n] are + * any comparison values + * text: a human-readable description of this operation to populate selects + * appliesTo: an array of identifiers for types that operation may be used on + * inputCount: the number of inputs required to get any necessary comparison + * values for the operation + * getDescription: A function returning a human-readable shorthand description of + * this operation to populate the 'description' field in the rule header. + * Will be invoked with an array of a condition's comparison values. + */ + this.operations = { + equalTo: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'is equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' == ' + values[0]; + } + }, + notEqualTo: { + operation: function (input) { + return input[0] !== input[1]; + }, + text: 'is not equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' != ' + values[0]; + } + }, + greaterThan: { + operation: function (input) { + return input[0] > input[1]; + }, + text: 'is greater than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' > ' + values[0]; + } + }, + lessThan: { + operation: function (input) { + return input[0] < input[1]; + }, + text: 'is less than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' < ' + values[0]; + } + }, + greaterThanOrEq: { + operation: function (input) { + return input[0] >= input[1]; + }, + text: 'is greater than or equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' >= ' + values[0]; + } + }, + lessThanOrEq: { + operation: function (input) { + return input[0] <= input[1]; + }, + text: 'is less than or equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' <= ' + values[0]; + } + }, + between: { + operation: function (input) { + return input[0] > input[1] && input[0] < input[2]; + }, + text: 'is between', + appliesTo: ['number'], + inputCount: 2, + getDescription: function (values) { + return ' between ' + values[0] + ' and ' + values[1]; + } + }, + notBetween: { + operation: function (input) { + return input[0] < input[1] || input[0] > input[2]; + }, + text: 'is not between', + appliesTo: ['number'], + inputCount: 2, + getDescription: function (values) { + return ' not between ' + values[0] + ' and ' + values[1]; + } + }, + textContains: { + operation: function (input) { + return input[0] && input[1] && input[0].includes(input[1]); + }, + text: 'text contains', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' contains ' + values[0]; + } + }, + textDoesNotContain: { + operation: function (input) { + return input[0] && input[1] && !input[0].includes(input[1]); + }, + text: 'text does not contain', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' does not contain ' + values[0]; + } + }, + textStartsWith: { + operation: function (input) { + return input[0].startsWith(input[1]); + }, + text: 'text starts with', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' starts with ' + values[0]; + } + }, + textEndsWith: { + operation: function (input) { + return input[0].endsWith(input[1]); + }, + text: 'text ends with', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' ends with ' + values[0]; + } + }, + textIsExactly: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'text is exactly', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' is exactly ' + values[0]; + } + }, + isUndefined: { + operation: function (input) { + return typeof input[0] === 'undefined'; + }, + text: 'is undefined', + appliesTo: ['string', 'number', 'enum'], + inputCount: 0, + getDescription: function () { + return ' is undefined'; + } + }, + isDefined: { + operation: function (input) { + return typeof input[0] !== 'undefined'; + }, + text: 'is defined', + appliesTo: ['string', 'number', 'enum'], + inputCount: 0, + getDescription: function () { + return ' is defined'; + } + }, + enumValueIs: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'is', + appliesTo: ['enum'], + inputCount: 1, + getDescription: function (values) { + return ' == ' + values[0]; + } + }, + enumValueIsNot: { + operation: function (input) { + return input[0] !== input[1]; + }, + text: 'is not', + appliesTo: ['enum'], + inputCount: 1, + getDescription: function (values) { + return ' != ' + values[0]; + } + } + }; + } - /** - * Functions to validate that the input to an operation is of the type - * that it expects, in order to prevent unexpected behavior. Will be - * invoked before the corresponding operation is executed - */ - this.inputValidators = { - number: this.validateNumberInput, - string: this.validateStringInput, - enum: this.validateNumberInput - }; + /** + * Evaluate the conditions passed in as an argument, and return the boolean + * value of these conditions. Available evaluation modes are 'any', which will + * return true if any of the conditions evaluates to true (i.e. logical OR); 'all', + * which returns true only if all conditions evalute to true (i.e. logical AND); + * or 'js', which returns the boolean value of a custom JavaScript conditional. + * @param {} conditions Either an array of objects with object, key, operation, + * and value fields, or a string representing a JavaScript + * condition. + * @param {string} mode The key of the mode to use when evaluating the conditions. + * @return {boolean} The boolean value of the conditions + */ + ConditionEvaluator.prototype.execute = function (conditions, mode) { + let active = false; + let conditionValue; + let conditionDefined = false; + const self = this; + let firstRuleEvaluated = false; + const compositionObjs = this.compositionObjs; - /** - * A library of operations supported by this rule evaluator. Each operation - * consists of the following fields: - * operation: a function with boolean return type to be invoked when this - * operation is used. Will be called with an array of inputs - * where input [0] is the telemetry value and input [1..n] are - * any comparison values - * text: a human-readable description of this operation to populate selects - * appliesTo: an array of identifiers for types that operation may be used on - * inputCount: the number of inputs required to get any necessary comparison - * values for the operation - * getDescription: A function returning a human-readable shorthand description of - * this operation to populate the 'description' field in the rule header. - * Will be invoked with an array of a condition's comparison values. - */ - this.operations = { - equalTo: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'is equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' == ' + values[0]; - } - }, - notEqualTo: { - operation: function (input) { - return input[0] !== input[1]; - }, - text: 'is not equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' != ' + values[0]; - } - }, - greaterThan: { - operation: function (input) { - return input[0] > input[1]; - }, - text: 'is greater than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' > ' + values[0]; - } - }, - lessThan: { - operation: function (input) { - return input[0] < input[1]; - }, - text: 'is less than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' < ' + values[0]; - } - }, - greaterThanOrEq: { - operation: function (input) { - return input[0] >= input[1]; - }, - text: 'is greater than or equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' >= ' + values[0]; - } - }, - lessThanOrEq: { - operation: function (input) { - return input[0] <= input[1]; - }, - text: 'is less than or equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' <= ' + values[0]; - } - }, - between: { - operation: function (input) { - return input[0] > input[1] && input[0] < input[2]; - }, - text: 'is between', - appliesTo: ['number'], - inputCount: 2, - getDescription: function (values) { - return ' between ' + values[0] + ' and ' + values[1]; - } - }, - notBetween: { - operation: function (input) { - return input[0] < input[1] || input[0] > input[2]; - }, - text: 'is not between', - appliesTo: ['number'], - inputCount: 2, - getDescription: function (values) { - return ' not between ' + values[0] + ' and ' + values[1]; - } - }, - textContains: { - operation: function (input) { - return input[0] && input[1] && input[0].includes(input[1]); - }, - text: 'text contains', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' contains ' + values[0]; - } - }, - textDoesNotContain: { - operation: function (input) { - return input[0] && input[1] && !input[0].includes(input[1]); - }, - text: 'text does not contain', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' does not contain ' + values[0]; - } - }, - textStartsWith: { - operation: function (input) { - return input[0].startsWith(input[1]); - }, - text: 'text starts with', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' starts with ' + values[0]; - } - }, - textEndsWith: { - operation: function (input) { - return input[0].endsWith(input[1]); - }, - text: 'text ends with', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' ends with ' + values[0]; - } - }, - textIsExactly: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'text is exactly', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' is exactly ' + values[0]; - } - }, - isUndefined: { - operation: function (input) { - return typeof input[0] === 'undefined'; - }, - text: 'is undefined', - appliesTo: ['string', 'number', 'enum'], - inputCount: 0, - getDescription: function () { - return ' is undefined'; - } - }, - isDefined: { - operation: function (input) { - return typeof input[0] !== 'undefined'; - }, - text: 'is defined', - appliesTo: ['string', 'number', 'enum'], - inputCount: 0, - getDescription: function () { - return ' is defined'; - } - }, - enumValueIs: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'is', - appliesTo: ['enum'], - inputCount: 1, - getDescription: function (values) { - return ' == ' + values[0]; - } - }, - enumValueIsNot: { - operation: function (input) { - return input[0] !== input[1]; - }, - text: 'is not', - appliesTo: ['enum'], - inputCount: 1, - getDescription: function (values) { - return ' != ' + values[0]; - } + if (mode === 'js') { + active = this.executeJavaScriptCondition(conditions); + } else { + (conditions || []).forEach(function (condition) { + conditionDefined = false; + if (condition.object === 'any') { + conditionValue = false; + Object.keys(compositionObjs).forEach(function (objId) { + try { + conditionValue = + conditionValue || + self.executeCondition(objId, condition.key, condition.operation, condition.values); + conditionDefined = true; + } catch (e) { + //ignore a malformed condition } - }; + }); + } else if (condition.object === 'all') { + conditionValue = true; + Object.keys(compositionObjs).forEach(function (objId) { + try { + conditionValue = + conditionValue && + self.executeCondition(objId, condition.key, condition.operation, condition.values); + conditionDefined = true; + } catch (e) { + //ignore a malformed condition + } + }); + } else { + try { + conditionValue = self.executeCondition( + condition.object, + condition.key, + condition.operation, + condition.values + ); + conditionDefined = true; + } catch (e) { + //ignore malformed condition + } + } + + if (conditionDefined) { + active = mode === 'all' && !firstRuleEvaluated ? true : active; + firstRuleEvaluated = true; + if (mode === 'any') { + active = active || conditionValue; + } else if (mode === 'all') { + active = active && conditionValue; + } + } + }); } - /** - * Evaluate the conditions passed in as an argument, and return the boolean - * value of these conditions. Available evaluation modes are 'any', which will - * return true if any of the conditions evaluates to true (i.e. logical OR); 'all', - * which returns true only if all conditions evalute to true (i.e. logical AND); - * or 'js', which returns the boolean value of a custom JavaScript conditional. - * @param {} conditions Either an array of objects with object, key, operation, - * and value fields, or a string representing a JavaScript - * condition. - * @param {string} mode The key of the mode to use when evaluating the conditions. - * @return {boolean} The boolean value of the conditions - */ - ConditionEvaluator.prototype.execute = function (conditions, mode) { - let active = false; - let conditionValue; - let conditionDefined = false; - const self = this; - let firstRuleEvaluated = false; - const compositionObjs = this.compositionObjs; + return active; + }; - if (mode === 'js') { - active = this.executeJavaScriptCondition(conditions); - } else { - (conditions || []).forEach(function (condition) { - conditionDefined = false; - if (condition.object === 'any') { - conditionValue = false; - Object.keys(compositionObjs).forEach(function (objId) { - try { - conditionValue = conditionValue - || self.executeCondition(objId, condition.key, - condition.operation, condition.values); - conditionDefined = true; - } catch (e) { - //ignore a malformed condition - } - }); - } else if (condition.object === 'all') { - conditionValue = true; - Object.keys(compositionObjs).forEach(function (objId) { - try { - conditionValue = conditionValue - && self.executeCondition(objId, condition.key, - condition.operation, condition.values); - conditionDefined = true; - } catch (e) { - //ignore a malformed condition - } - }); - } else { - try { - conditionValue = self.executeCondition(condition.object, condition.key, - condition.operation, condition.values); - conditionDefined = true; - } catch (e) { - //ignore malformed condition - } - } + /** + * Execute a condition defined as an object. + * @param {string} object The identifier of the telemetry object to retrieve data from + * @param {string} key The property of the telemetry object + * @param {string} operation The key of the operation in this ConditionEvaluator to executeCondition + * @param {string} values An array of comparison values to invoke the operation with + * @return {boolean} The value of this condition + */ + ConditionEvaluator.prototype.executeCondition = function (object, key, operation, values) { + const cache = this.useTestCache ? this.testCache : this.subscriptionCache; + let telemetryValue; + let op; + let input; + let validator; - if (conditionDefined) { - active = (mode === 'all' && !firstRuleEvaluated ? true : active); - firstRuleEvaluated = true; - if (mode === 'any') { - active = active || conditionValue; - } else if (mode === 'all') { - active = active && conditionValue; - } - } - }); - } + if (cache[object] && typeof cache[object][key] !== 'undefined') { + let value = cache[object][key]; + telemetryValue = [isNaN(Number(value)) ? value : Number(value)]; + } - return active; - }; + op = this.operations[operation] && this.operations[operation].operation; + input = telemetryValue && telemetryValue.concat(values); + validator = op && this.inputValidators[this.operations[operation].appliesTo[0]]; - /** - * Execute a condition defined as an object. - * @param {string} object The identifier of the telemetry object to retrieve data from - * @param {string} key The property of the telemetry object - * @param {string} operation The key of the operation in this ConditionEvaluator to executeCondition - * @param {string} values An array of comparison values to invoke the operation with - * @return {boolean} The value of this condition - */ - ConditionEvaluator.prototype.executeCondition = function (object, key, operation, values) { - const cache = (this.useTestCache ? this.testCache : this.subscriptionCache); - let telemetryValue; - let op; - let input; - let validator; + if (op && input && validator) { + if (this.operations[operation].appliesTo.length > 1) { + return (this.validateNumberInput(input) || this.validateStringInput(input)) && op(input); + } else { + return validator(input) && op(input); + } + } else { + throw new Error('Malformed condition'); + } + }; - if (cache[object] && typeof cache[object][key] !== 'undefined') { - let value = cache[object][key]; - telemetryValue = [isNaN(Number(value)) ? value : Number(value)]; - } + /** + * A function that returns true only if each value in its input argument is + * of a numerical type + * @param {[]} input An array of values + * @returns {boolean} + */ + ConditionEvaluator.prototype.validateNumberInput = function (input) { + let valid = true; + input.forEach(function (value) { + valid = valid && typeof value === 'number'; + }); - op = this.operations[operation] && this.operations[operation].operation; - input = telemetryValue && telemetryValue.concat(values); - validator = op && this.inputValidators[this.operations[operation].appliesTo[0]]; + return valid; + }; - if (op && input && validator) { - if (this.operations[operation].appliesTo.length > 1) { - return (this.validateNumberInput(input) || this.validateStringInput(input)) && op(input); - } else { - return validator(input) && op(input); - } - } else { - throw new Error('Malformed condition'); - } - }; + /** + * A function that returns true only if each value in its input argument is + * a string + * @param {[]} input An array of values + * @returns {boolean} + */ + ConditionEvaluator.prototype.validateStringInput = function (input) { + let valid = true; + input.forEach(function (value) { + valid = valid && typeof value === 'string'; + }); - /** - * A function that returns true only if each value in its input argument is - * of a numerical type - * @param {[]} input An array of values - * @returns {boolean} - */ - ConditionEvaluator.prototype.validateNumberInput = function (input) { - let valid = true; - input.forEach(function (value) { - valid = valid && (typeof value === 'number'); - }); + return valid; + }; - return valid; - }; + /** + * Get the keys of operations supported by this evaluator + * @return {string[]} An array of the keys of supported operations + */ + ConditionEvaluator.prototype.getOperationKeys = function () { + return Object.keys(this.operations); + }; - /** - * A function that returns true only if each value in its input argument is - * a string - * @param {[]} input An array of values - * @returns {boolean} - */ - ConditionEvaluator.prototype.validateStringInput = function (input) { - let valid = true; - input.forEach(function (value) { - valid = valid && (typeof value === 'string'); - }); + /** + * Get the human-readable text corresponding to a given operation + * @param {string} key The key of the operation + * @return {string} The text description of the operation + */ + ConditionEvaluator.prototype.getOperationText = function (key) { + return this.operations[key].text; + }; - return valid; - }; + /** + * Returns true only if the given operation applies to a given type + * @param {string} key The key of the operation + * @param {string} type The value type to query + * @returns {boolean} True if the condition applies, false otherwise + */ + ConditionEvaluator.prototype.operationAppliesTo = function (key, type) { + return this.operations[key].appliesTo.includes(type); + }; - /** - * Get the keys of operations supported by this evaluator - * @return {string[]} An array of the keys of supported operations - */ - ConditionEvaluator.prototype.getOperationKeys = function () { - return Object.keys(this.operations); - }; + /** + * Return the number of value inputs required by an operation + * @param {string} key The key of the operation to query + * @return {number} + */ + ConditionEvaluator.prototype.getInputCount = function (key) { + if (this.operations[key]) { + return this.operations[key].inputCount; + } + }; - /** - * Get the human-readable text corresponding to a given operation - * @param {string} key The key of the operation - * @return {string} The text description of the operation - */ - ConditionEvaluator.prototype.getOperationText = function (key) { - return this.operations[key].text; - }; + /** + * Return the human-readable shorthand description of the operation for a rule header + * @param {string} key The key of the operation to query + * @param {} values An array of values with which to invoke the getDescription function + * of the operation + * @return {string} A text description of this operation + */ + ConditionEvaluator.prototype.getOperationDescription = function (key, values) { + if (this.operations[key]) { + return this.operations[key].getDescription(values); + } + }; - /** - * Returns true only if the given operation applies to a given type - * @param {string} key The key of the operation - * @param {string} type The value type to query - * @returns {boolean} True if the condition applies, false otherwise - */ - ConditionEvaluator.prototype.operationAppliesTo = function (key, type) { - return (this.operations[key].appliesTo.includes(type)); - }; + /** + * Return the HTML input type associated with a given operation + * @param {string} key The key of the operation to query + * @return {string} The key for an HTML5 input type + */ + ConditionEvaluator.prototype.getInputType = function (key) { + let type; + if (this.operations[key]) { + type = this.operations[key].appliesTo[0]; + } - /** - * Return the number of value inputs required by an operation - * @param {string} key The key of the operation to query - * @return {number} - */ - ConditionEvaluator.prototype.getInputCount = function (key) { - if (this.operations[key]) { - return this.operations[key].inputCount; - } - }; + if (this.inputTypes[type]) { + return this.inputTypes[type]; + } + }; - /** - * Return the human-readable shorthand description of the operation for a rule header - * @param {string} key The key of the operation to query - * @param {} values An array of values with which to invoke the getDescription function - * of the operation - * @return {string} A text description of this operation - */ - ConditionEvaluator.prototype.getOperationDescription = function (key, values) { - if (this.operations[key]) { - return this.operations[key].getDescription(values); - } - }; + /** + * Returns the HTML input type associated with a value type + * @param {string} dataType The JavaScript value type + * @return {string} The key for an HTML5 input type + */ + ConditionEvaluator.prototype.getInputTypeById = function (dataType) { + return this.inputTypes[dataType]; + }; - /** - * Return the HTML input type associated with a given operation - * @param {string} key The key of the operation to query - * @return {string} The key for an HTML5 input type - */ - ConditionEvaluator.prototype.getInputType = function (key) { - let type; - if (this.operations[key]) { - type = this.operations[key].appliesTo[0]; - } + /** + * Set the test data cache used by this rule evaluator + * @param {object} testCache A mock cache following the format of the real + * subscription cache + */ + ConditionEvaluator.prototype.setTestDataCache = function (testCache) { + this.testCache = testCache; + }; - if (this.inputTypes[type]) { - return this.inputTypes[type]; - } - }; + /** + * Have this RuleEvaluator pull data values from the provided test cache + * instead of its actual subscription cache when evaluating. If invoked with true, + * will use the test cache; otherwise, will use the subscription cache + * @param {boolean} useTestData Boolean flag + */ + ConditionEvaluator.prototype.useTestData = function (useTestCache) { + this.useTestCache = useTestCache; + }; - /** - * Returns the HTML input type associated with a value type - * @param {string} dataType The JavaScript value type - * @return {string} The key for an HTML5 input type - */ - ConditionEvaluator.prototype.getInputTypeById = function (dataType) { - return this.inputTypes[dataType]; - }; - - /** - * Set the test data cache used by this rule evaluator - * @param {object} testCache A mock cache following the format of the real - * subscription cache - */ - ConditionEvaluator.prototype.setTestDataCache = function (testCache) { - this.testCache = testCache; - }; - - /** - * Have this RuleEvaluator pull data values from the provided test cache - * instead of its actual subscription cache when evaluating. If invoked with true, - * will use the test cache; otherwise, will use the subscription cache - * @param {boolean} useTestData Boolean flag - */ - ConditionEvaluator.prototype.useTestData = function (useTestCache) { - this.useTestCache = useTestCache; - }; - - return ConditionEvaluator; + return ConditionEvaluator; }); diff --git a/src/plugins/summaryWidget/src/ConditionManager.js b/src/plugins/summaryWidget/src/ConditionManager.js index e502649030..825d65b69b 100644 --- a/src/plugins/summaryWidget/src/ConditionManager.js +++ b/src/plugins/summaryWidget/src/ConditionManager.js @@ -1,386 +1,386 @@ -define ([ - './ConditionEvaluator', - 'objectUtils', - 'EventEmitter', - 'lodash' -], function ( - ConditionEvaluator, - objectUtils, - EventEmitter, - _ +define(['./ConditionEvaluator', 'objectUtils', 'EventEmitter', 'lodash'], function ( + ConditionEvaluator, + objectUtils, + EventEmitter, + _ ) { + /** + * Provides a centralized content manager for conditions in the summary widget. + * Loads and caches composition and telemetry subscriptions, and maintains a + * {ConditionEvaluator} instance to handle evaluation + * @constructor + * @param {Object} domainObject the Summary Widget domain object + * @param {MCT} openmct an MCT instance + */ + function ConditionManager(domainObject, openmct) { + this.domainObject = domainObject; + this.openmct = openmct; - /** - * Provides a centralized content manager for conditions in the summary widget. - * Loads and caches composition and telemetry subscriptions, and maintains a - * {ConditionEvaluator} instance to handle evaluation - * @constructor - * @param {Object} domainObject the Summary Widget domain object - * @param {MCT} openmct an MCT instance - */ - function ConditionManager(domainObject, openmct) { - this.domainObject = domainObject; - this.openmct = openmct; + this.composition = this.openmct.composition.get(this.domainObject); + this.compositionObjs = {}; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['add', 'remove', 'load', 'metadata', 'receiveTelemetry']; - this.composition = this.openmct.composition.get(this.domainObject); - this.compositionObjs = {}; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['add', 'remove', 'load', 'metadata', 'receiveTelemetry']; + this.keywordLabels = { + any: 'any Telemetry', + all: 'all Telemetry' + }; - this.keywordLabels = { - any: 'any Telemetry', - all: 'all Telemetry' - }; + this.telemetryMetadataById = { + any: {}, + all: {} + }; - this.telemetryMetadataById = { - any: {}, - all: {} - }; + this.telemetryTypesById = { + any: {}, + all: {} + }; - this.telemetryTypesById = { - any: {}, - all: {} - }; + this.subscriptions = {}; + this.subscriptionCache = {}; + this.loadComplete = false; + this.metadataLoadComplete = false; + this.evaluator = new ConditionEvaluator(this.subscriptionCache, this.compositionObjs); - this.subscriptions = {}; - this.subscriptionCache = {}; - this.loadComplete = false; - this.metadataLoadComplete = false; - this.evaluator = new ConditionEvaluator(this.subscriptionCache, this.compositionObjs); + this.composition.on('add', this.onCompositionAdd, this); + this.composition.on('remove', this.onCompositionRemove, this); + this.composition.on('load', this.onCompositionLoad, this); - this.composition.on('add', this.onCompositionAdd, this); - this.composition.on('remove', this.onCompositionRemove, this); - this.composition.on('load', this.onCompositionLoad, this); + this.composition.load(); + } - this.composition.load(); + /** + * Register a callback with this ConditionManager: supported callbacks are add + * remove, load, metadata, and receiveTelemetry + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + ConditionManager.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } else { + throw ( + event + ' is not a supported callback. Supported callbacks are ' + this.supportedCallbacks + ); + } + }; + + /** + * Given a set of rules, execute the conditions associated with each rule + * and return the id of the last rule whose conditions evaluate to true + * @param {string[]} ruleOrder An array of rule IDs indicating what order They + * should be evaluated in + * @param {Object} rules An object mapping rule IDs to rule configurations + * @return {string} The ID of the rule to display on the widget + */ + ConditionManager.prototype.executeRules = function (ruleOrder, rules) { + const self = this; + let activeId = ruleOrder[0]; + let rule; + let conditions; + + ruleOrder.forEach(function (ruleId) { + rule = rules[ruleId]; + conditions = rule.getProperty('conditions'); + if (self.evaluator.execute(conditions, rule.getProperty('trigger'))) { + activeId = ruleId; + } + }); + + return activeId; + }; + + /** + * Adds a field to the list of all available metadata fields in the widget + * @param {Object} metadatum An object representing a set of telemetry metadata + */ + ConditionManager.prototype.addGlobalMetadata = function (metadatum) { + this.telemetryMetadataById.any[metadatum.key] = metadatum; + this.telemetryMetadataById.all[metadatum.key] = metadatum; + }; + + /** + * Adds a field to the list of properties for globally available metadata + * @param {string} key The key for the property this type applies to + * @param {string} type The type that should be associated with this property + */ + ConditionManager.prototype.addGlobalPropertyType = function (key, type) { + this.telemetryTypesById.any[key] = type; + this.telemetryTypesById.all[key] = type; + }; + + /** + * Given a telemetry-producing domain object, associate each of it's telemetry + * fields with a type, parsing from historical data. + * @param {Object} object a domain object that can produce telemetry + * @return {Promise} A promise that resolves when a telemetry request + * has completed and types have been parsed + */ + ConditionManager.prototype.parsePropertyTypes = function (object) { + const objectId = objectUtils.makeKeyString(object.identifier); + + this.telemetryTypesById[objectId] = {}; + Object.values(this.telemetryMetadataById[objectId]).forEach(function (valueMetadata) { + let type; + if (valueMetadata.enumerations !== undefined) { + type = 'enum'; + } else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) { + type = 'number'; + } else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) { + type = 'number'; + } else if (valueMetadata.key === 'name') { + type = 'string'; + } else { + type = 'string'; + } + + this.telemetryTypesById[objectId][valueMetadata.key] = type; + this.addGlobalPropertyType(valueMetadata.key, type); + }, this); + }; + + /** + * Parse types of telemetry fields from all composition objects; used internally + * to perform a block types load once initial composition load has completed + * @return {Promise} A promise that resolves when all metadata has been loaded + * and property types parsed + */ + ConditionManager.prototype.parseAllPropertyTypes = function () { + Object.values(this.compositionObjs).forEach(this.parsePropertyTypes, this); + this.metadataLoadComplete = true; + this.eventEmitter.emit('metadata'); + }; + + /** + * Invoked when a telemtry subscription yields new data. Updates the LAD + * cache and invokes any registered receiveTelemetry callbacks + * @param {string} objId The key associated with the telemetry source + * @param {datum} datum The new data from the telemetry source + * @private + */ + ConditionManager.prototype.handleSubscriptionCallback = function (objId, telemetryDatum) { + this.subscriptionCache[objId] = this.createNormalizedDatum(objId, telemetryDatum); + this.eventEmitter.emit('receiveTelemetry'); + }; + + ConditionManager.prototype.createNormalizedDatum = function (objId, telemetryDatum) { + return Object.values(this.telemetryMetadataById[objId]).reduce((normalizedDatum, metadatum) => { + normalizedDatum[metadatum.key] = telemetryDatum[metadatum.source]; + + return normalizedDatum; + }, {}); + }; + + /** + * Event handler for an add event in this Summary Widget's composition. + * Sets up subscription handlers and parses its property types. + * @param {Object} obj The newly added domain object + * @private + */ + ConditionManager.prototype.onCompositionAdd = function (obj) { + let compositionKeys; + const telemetryAPI = this.openmct.telemetry; + const objId = objectUtils.makeKeyString(obj.identifier); + let telemetryMetadata; + const self = this; + + if (telemetryAPI.isTelemetryObject(obj)) { + self.compositionObjs[objId] = obj; + self.telemetryMetadataById[objId] = {}; + + // FIXME: this should just update based on listener. + compositionKeys = self.domainObject.composition.map(objectUtils.makeKeyString); + if (!compositionKeys.includes(objId)) { + self.domainObject.composition.push(obj.identifier); + } + + telemetryMetadata = telemetryAPI.getMetadata(obj).values(); + telemetryMetadata.forEach(function (metaDatum) { + self.telemetryMetadataById[objId][metaDatum.key] = metaDatum; + self.addGlobalMetadata(metaDatum); + }); + + self.subscriptionCache[objId] = {}; + self.subscriptions[objId] = telemetryAPI.subscribe( + obj, + function (datum) { + self.handleSubscriptionCallback(objId, datum); + }, + {} + ); + telemetryAPI + .request(obj, { + strategy: 'latest', + size: 1 + }) + .then(function (results) { + if (results && results.length) { + self.handleSubscriptionCallback(objId, results[results.length - 1]); + } + }); + + /** + * if this is the initial load, parsing property types will be postponed + * until all composition objects have been loaded + */ + if (self.loadComplete) { + self.parsePropertyTypes(obj); + } + + self.eventEmitter.emit('add', obj); + + const summaryWidget = document.querySelector('.w-summary-widget'); + if (summaryWidget) { + summaryWidget.classList.remove('s-status-no-data'); + } + } + }; + + /** + * Invoked on a remove event in this Summary Widget's compostion. Removes + * the object from the local composition, and untracks it + * @param {object} identifier The identifier of the object to be removed + * @private + */ + ConditionManager.prototype.onCompositionRemove = function (identifier) { + const objectId = objectUtils.makeKeyString(identifier); + // FIXME: this should just update by listener. + _.remove(this.domainObject.composition, function (id) { + return id.key === identifier.key && id.namespace === identifier.namespace; + }); + delete this.compositionObjs[objectId]; + delete this.subscriptionCache[objectId]; + this.subscriptions[objectId](); //unsubscribe from telemetry source + delete this.subscriptions[objectId]; + this.eventEmitter.emit('remove', identifier); + + if (_.isEmpty(this.compositionObjs)) { + const summaryWidget = document.querySelector('.w-summary-widget'); + if (summaryWidget) { + summaryWidget.classList.add('s-status-no-data'); + } + } + }; + + /** + * Invoked when the Summary Widget's composition finishes its initial load. + * Invokes any registered load callbacks, does a block load of all metadata, + * and then invokes any registered metadata load callbacks. + * @private + */ + ConditionManager.prototype.onCompositionLoad = function () { + this.loadComplete = true; + this.eventEmitter.emit('load'); + this.parseAllPropertyTypes(); + }; + + /** + * Returns the currently tracked telemetry sources + * @return {Object} An object mapping object keys to domain objects + */ + ConditionManager.prototype.getComposition = function () { + return this.compositionObjs; + }; + + /** + * Get the human-readable name of a domain object from its key + * @param {string} id The key of the domain object + * @return {string} The human-readable name of the domain object + */ + ConditionManager.prototype.getObjectName = function (id) { + let name; + + if (this.keywordLabels[id]) { + name = this.keywordLabels[id]; + } else if (this.compositionObjs[id]) { + name = this.compositionObjs[id].name; } - /** - * Register a callback with this ConditionManager: supported callbacks are add - * remove, load, metadata, and receiveTelemetry - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - ConditionManager.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } else { - throw event + " is not a supported callback. Supported callbacks are " + this.supportedCallbacks; - } - }; + return name; + }; - /** - * Given a set of rules, execute the conditions associated with each rule - * and return the id of the last rule whose conditions evaluate to true - * @param {string[]} ruleOrder An array of rule IDs indicating what order They - * should be evaluated in - * @param {Object} rules An object mapping rule IDs to rule configurations - * @return {string} The ID of the rule to display on the widget - */ - ConditionManager.prototype.executeRules = function (ruleOrder, rules) { - const self = this; - let activeId = ruleOrder[0]; - let rule; - let conditions; + /** + * Returns the property metadata associated with a given telemetry source + * @param {string} id The key associated with the domain object + * @return {Object} Returns an object with fields representing each telemetry field + */ + ConditionManager.prototype.getTelemetryMetadata = function (id) { + return this.telemetryMetadataById[id]; + }; - ruleOrder.forEach(function (ruleId) { - rule = rules[ruleId]; - conditions = rule.getProperty('conditions'); - if (self.evaluator.execute(conditions, rule.getProperty('trigger'))) { - activeId = ruleId; - } - }); + /** + * Returns the type associated with a telemtry data field of a particular domain + * object + * @param {string} id The key associated with the domain object + * @param {string} property The telemetry field key to retrieve the type of + * @return {string} The type name + */ + ConditionManager.prototype.getTelemetryPropertyType = function (id, property) { + if (this.telemetryTypesById[id]) { + return this.telemetryTypesById[id][property]; + } + }; - return activeId; - }; + /** + * Returns the human-readable name of a telemtry data field of a particular domain + * object + * @param {string} id The key associated with the domain object + * @param {string} property The telemetry field key to retrieve the type of + * @return {string} The telemetry field name + */ + ConditionManager.prototype.getTelemetryPropertyName = function (id, property) { + if (this.telemetryMetadataById[id] && this.telemetryMetadataById[id][property]) { + return this.telemetryMetadataById[id][property].name; + } + }; - /** - * Adds a field to the list of all available metadata fields in the widget - * @param {Object} metadatum An object representing a set of telemetry metadata - */ - ConditionManager.prototype.addGlobalMetadata = function (metadatum) { - this.telemetryMetadataById.any[metadatum.key] = metadatum; - this.telemetryMetadataById.all[metadatum.key] = metadatum; - }; + /** + * Returns the {ConditionEvaluator} instance associated with this condition + * manager + * @return {ConditionEvaluator} + */ + ConditionManager.prototype.getEvaluator = function () { + return this.evaluator; + }; - /** - * Adds a field to the list of properties for globally available metadata - * @param {string} key The key for the property this type applies to - * @param {string} type The type that should be associated with this property - */ - ConditionManager.prototype.addGlobalPropertyType = function (key, type) { - this.telemetryTypesById.any[key] = type; - this.telemetryTypesById.all[key] = type; - }; + /** + * Returns true if the initial compostion load has completed + * @return {boolean} + */ + ConditionManager.prototype.loadCompleted = function () { + return this.loadComplete; + }; - /** - * Given a telemetry-producing domain object, associate each of it's telemetry - * fields with a type, parsing from historical data. - * @param {Object} object a domain object that can produce telemetry - * @return {Promise} A promise that resolves when a telemetry request - * has completed and types have been parsed - */ - ConditionManager.prototype.parsePropertyTypes = function (object) { - const objectId = objectUtils.makeKeyString(object.identifier); + /** + * Returns true if the initial block metadata load has completed + */ + ConditionManager.prototype.metadataLoadCompleted = function () { + return this.metadataLoadComplete; + }; - this.telemetryTypesById[objectId] = {}; - Object.values(this.telemetryMetadataById[objectId]).forEach(function (valueMetadata) { - let type; - if (valueMetadata.enumerations !== undefined) { - type = 'enum'; - } else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) { - type = 'number'; - } else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) { - type = 'number'; - } else if (valueMetadata.key === 'name') { - type = 'string'; - } else { - type = 'string'; - } + /** + * Triggers the telemetryRecieve callbacks registered to this ConditionManager, + * used by the {TestDataManager} to force a rule evaluation when test data is + * enabled + */ + ConditionManager.prototype.triggerTelemetryCallback = function () { + this.eventEmitter.emit('receiveTelemetry'); + }; - this.telemetryTypesById[objectId][valueMetadata.key] = type; - this.addGlobalPropertyType(valueMetadata.key, type); - }, this); - }; + /** + * Unsubscribe from all registered telemetry sources and unregister all event + * listeners registered with the Open MCT APIs + */ + ConditionManager.prototype.destroy = function () { + Object.values(this.subscriptions).forEach(function (unsubscribeFunction) { + unsubscribeFunction(); + }); + this.composition.off('add', this.onCompositionAdd, this); + this.composition.off('remove', this.onCompositionRemove, this); + this.composition.off('load', this.onCompositionLoad, this); + }; - /** - * Parse types of telemetry fields from all composition objects; used internally - * to perform a block types load once initial composition load has completed - * @return {Promise} A promise that resolves when all metadata has been loaded - * and property types parsed - */ - ConditionManager.prototype.parseAllPropertyTypes = function () { - Object.values(this.compositionObjs).forEach(this.parsePropertyTypes, this); - this.metadataLoadComplete = true; - this.eventEmitter.emit('metadata'); - }; - - /** - * Invoked when a telemtry subscription yields new data. Updates the LAD - * cache and invokes any registered receiveTelemetry callbacks - * @param {string} objId The key associated with the telemetry source - * @param {datum} datum The new data from the telemetry source - * @private - */ - ConditionManager.prototype.handleSubscriptionCallback = function (objId, telemetryDatum) { - this.subscriptionCache[objId] = this.createNormalizedDatum(objId, telemetryDatum); - this.eventEmitter.emit('receiveTelemetry'); - }; - - ConditionManager.prototype.createNormalizedDatum = function (objId, telemetryDatum) { - return Object.values(this.telemetryMetadataById[objId]).reduce((normalizedDatum, metadatum) => { - normalizedDatum[metadatum.key] = telemetryDatum[metadatum.source]; - - return normalizedDatum; - }, {}); - }; - - /** - * Event handler for an add event in this Summary Widget's composition. - * Sets up subscription handlers and parses its property types. - * @param {Object} obj The newly added domain object - * @private - */ - ConditionManager.prototype.onCompositionAdd = function (obj) { - let compositionKeys; - const telemetryAPI = this.openmct.telemetry; - const objId = objectUtils.makeKeyString(obj.identifier); - let telemetryMetadata; - const self = this; - - if (telemetryAPI.isTelemetryObject(obj)) { - self.compositionObjs[objId] = obj; - self.telemetryMetadataById[objId] = {}; - - // FIXME: this should just update based on listener. - compositionKeys = self.domainObject.composition.map(objectUtils.makeKeyString); - if (!compositionKeys.includes(objId)) { - self.domainObject.composition.push(obj.identifier); - } - - telemetryMetadata = telemetryAPI.getMetadata(obj).values(); - telemetryMetadata.forEach(function (metaDatum) { - self.telemetryMetadataById[objId][metaDatum.key] = metaDatum; - self.addGlobalMetadata(metaDatum); - }); - - self.subscriptionCache[objId] = {}; - self.subscriptions[objId] = telemetryAPI.subscribe(obj, function (datum) { - self.handleSubscriptionCallback(objId, datum); - }, {}); - telemetryAPI.request(obj, { - strategy: 'latest', - size: 1 - }) - .then(function (results) { - if (results && results.length) { - self.handleSubscriptionCallback(objId, results[results.length - 1]); - } - }); - - /** - * if this is the initial load, parsing property types will be postponed - * until all composition objects have been loaded - */ - if (self.loadComplete) { - self.parsePropertyTypes(obj); - } - - self.eventEmitter.emit('add', obj); - - const summaryWidget = document.querySelector('.w-summary-widget'); - if (summaryWidget) { - summaryWidget.classList.remove('s-status-no-data'); - } - } - }; - - /** - * Invoked on a remove event in this Summary Widget's compostion. Removes - * the object from the local composition, and untracks it - * @param {object} identifier The identifier of the object to be removed - * @private - */ - ConditionManager.prototype.onCompositionRemove = function (identifier) { - const objectId = objectUtils.makeKeyString(identifier); - // FIXME: this should just update by listener. - _.remove(this.domainObject.composition, function (id) { - return id.key === identifier.key - && id.namespace === identifier.namespace; - }); - delete this.compositionObjs[objectId]; - delete this.subscriptionCache[objectId]; - this.subscriptions[objectId](); //unsubscribe from telemetry source - delete this.subscriptions[objectId]; - this.eventEmitter.emit('remove', identifier); - - if (_.isEmpty(this.compositionObjs)) { - const summaryWidget = document.querySelector('.w-summary-widget'); - if (summaryWidget) { - summaryWidget.classList.add('s-status-no-data'); - } - } - }; - - /** - * Invoked when the Summary Widget's composition finishes its initial load. - * Invokes any registered load callbacks, does a block load of all metadata, - * and then invokes any registered metadata load callbacks. - * @private - */ - ConditionManager.prototype.onCompositionLoad = function () { - this.loadComplete = true; - this.eventEmitter.emit('load'); - this.parseAllPropertyTypes(); - }; - - /** - * Returns the currently tracked telemetry sources - * @return {Object} An object mapping object keys to domain objects - */ - ConditionManager.prototype.getComposition = function () { - return this.compositionObjs; - }; - - /** - * Get the human-readable name of a domain object from its key - * @param {string} id The key of the domain object - * @return {string} The human-readable name of the domain object - */ - ConditionManager.prototype.getObjectName = function (id) { - let name; - - if (this.keywordLabels[id]) { - name = this.keywordLabels[id]; - } else if (this.compositionObjs[id]) { - name = this.compositionObjs[id].name; - } - - return name; - }; - - /** - * Returns the property metadata associated with a given telemetry source - * @param {string} id The key associated with the domain object - * @return {Object} Returns an object with fields representing each telemetry field - */ - ConditionManager.prototype.getTelemetryMetadata = function (id) { - return this.telemetryMetadataById[id]; - }; - - /** - * Returns the type associated with a telemtry data field of a particular domain - * object - * @param {string} id The key associated with the domain object - * @param {string} property The telemetry field key to retrieve the type of - * @return {string} The type name - */ - ConditionManager.prototype.getTelemetryPropertyType = function (id, property) { - if (this.telemetryTypesById[id]) { - return this.telemetryTypesById[id][property]; - } - }; - - /** - * Returns the human-readable name of a telemtry data field of a particular domain - * object - * @param {string} id The key associated with the domain object - * @param {string} property The telemetry field key to retrieve the type of - * @return {string} The telemetry field name - */ - ConditionManager.prototype.getTelemetryPropertyName = function (id, property) { - if (this.telemetryMetadataById[id] && this.telemetryMetadataById[id][property]) { - return this.telemetryMetadataById[id][property].name; - } - }; - - /** - * Returns the {ConditionEvaluator} instance associated with this condition - * manager - * @return {ConditionEvaluator} - */ - ConditionManager.prototype.getEvaluator = function () { - return this.evaluator; - }; - - /** - * Returns true if the initial compostion load has completed - * @return {boolean} - */ - ConditionManager.prototype.loadCompleted = function () { - return this.loadComplete; - }; - - /** - * Returns true if the initial block metadata load has completed - */ - ConditionManager.prototype.metadataLoadCompleted = function () { - return this.metadataLoadComplete; - }; - - /** - * Triggers the telemetryRecieve callbacks registered to this ConditionManager, - * used by the {TestDataManager} to force a rule evaluation when test data is - * enabled - */ - ConditionManager.prototype.triggerTelemetryCallback = function () { - this.eventEmitter.emit('receiveTelemetry'); - }; - - /** - * Unsubscribe from all registered telemetry sources and unregister all event - * listeners registered with the Open MCT APIs - */ - ConditionManager.prototype.destroy = function () { - Object.values(this.subscriptions).forEach(function (unsubscribeFunction) { - unsubscribeFunction(); - }); - this.composition.off('add', this.onCompositionAdd, this); - this.composition.off('remove', this.onCompositionRemove, this); - this.composition.off('load', this.onCompositionLoad, this); - }; - - return ConditionManager; + return ConditionManager; }); diff --git a/src/plugins/summaryWidget/src/Rule.js b/src/plugins/summaryWidget/src/Rule.js index 0b8f28804f..831fb0d2a7 100644 --- a/src/plugins/summaryWidget/src/Rule.js +++ b/src/plugins/summaryWidget/src/Rule.js @@ -1,525 +1,536 @@ define([ - '../res/ruleTemplate.html', - './Condition', - './input/ColorPalette', - './input/IconPalette', - './eventHelpers', - '../../../utils/template/templateHelpers', - 'EventEmitter', - 'lodash' + '../res/ruleTemplate.html', + './Condition', + './input/ColorPalette', + './input/IconPalette', + './eventHelpers', + '../../../utils/template/templateHelpers', + 'EventEmitter', + 'lodash' ], function ( - ruleTemplate, - Condition, - ColorPalette, - IconPalette, - eventHelpers, - templateHelpers, - EventEmitter, - _ + ruleTemplate, + Condition, + ColorPalette, + IconPalette, + eventHelpers, + templateHelpers, + EventEmitter, + _ ) { + /** + * An object representing a summary widget rule. Maintains a set of text + * and css properties for output, and a set of conditions for configuring + * when the rule will be applied to the summary widget. + * @constructor + * @param {Object} ruleConfig A JavaScript object representing the configuration of this rule + * @param {Object} domainObject The Summary Widget domain object which contains this rule + * @param {MCT} openmct An MCT instance + * @param {ConditionManager} conditionManager A ConditionManager instance + * @param {WidgetDnD} widgetDnD A WidgetDnD instance to handle dragging and dropping rules + * @param {element} container The DOM element which cotains this summary widget + */ + function Rule(ruleConfig, domainObject, openmct, conditionManager, widgetDnD, container) { + eventHelpers.extend(this); + const self = this; + const THUMB_ICON_CLASS = 'c-sw__icon js-sw__icon'; + + this.config = ruleConfig; + this.domainObject = domainObject; + this.openmct = openmct; + this.conditionManager = conditionManager; + this.widgetDnD = widgetDnD; + this.container = container; + + this.domElement = templateHelpers.convertTemplateToHTML(ruleTemplate)[0]; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['remove', 'duplicate', 'change', 'conditionChange']; + this.conditions = []; + this.dragging = false; + + this.remove = this.remove.bind(this); + this.duplicate = this.duplicate.bind(this); + + this.thumbnail = this.domElement.querySelector('.t-widget-thumb'); + this.thumbnailIcon = this.domElement.querySelector('.js-sw__icon'); + this.thumbnailLabel = this.domElement.querySelector('.c-sw__label'); + this.title = this.domElement.querySelector('.rule-title'); + this.description = this.domElement.querySelector('.rule-description'); + this.trigger = this.domElement.querySelector('.t-trigger'); + this.toggleConfigButton = this.domElement.querySelector('.js-disclosure'); + this.configArea = this.domElement.querySelector('.widget-rule-content'); + this.grippy = this.domElement.querySelector('.t-grippy'); + this.conditionArea = this.domElement.querySelector('.t-widget-rule-config'); + this.jsConditionArea = this.domElement.querySelector('.t-rule-js-condition-input-holder'); + this.deleteButton = this.domElement.querySelector('.t-delete'); + this.duplicateButton = this.domElement.querySelector('.t-duplicate'); + this.addConditionButton = this.domElement.querySelector('.add-condition'); + /** - * An object representing a summary widget rule. Maintains a set of text - * and css properties for output, and a set of conditions for configuring - * when the rule will be applied to the summary widget. - * @constructor - * @param {Object} ruleConfig A JavaScript object representing the configuration of this rule - * @param {Object} domainObject The Summary Widget domain object which contains this rule - * @param {MCT} openmct An MCT instance - * @param {ConditionManager} conditionManager A ConditionManager instance - * @param {WidgetDnD} widgetDnD A WidgetDnD instance to handle dragging and dropping rules - * @param {element} container The DOM element which cotains this summary widget + * The text inputs for this rule: any input included in this object will + * have the appropriate event handlers registered to it, and it's corresponding + * field in the domain object will be updated with its value */ - function Rule(ruleConfig, domainObject, openmct, conditionManager, widgetDnD, container) { - eventHelpers.extend(this); - const self = this; - const THUMB_ICON_CLASS = 'c-sw__icon js-sw__icon'; - this.config = ruleConfig; - this.domainObject = domainObject; - this.openmct = openmct; - this.conditionManager = conditionManager; - this.widgetDnD = widgetDnD; - this.container = container; + this.textInputs = { + name: this.domElement.querySelector('.t-rule-name-input'), + label: this.domElement.querySelector('.t-rule-label-input'), + message: this.domElement.querySelector('.t-rule-message-input'), + jsCondition: this.domElement.querySelector('.t-rule-js-condition-input') + }; - this.domElement = templateHelpers.convertTemplateToHTML(ruleTemplate)[0]; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['remove', 'duplicate', 'change', 'conditionChange']; - this.conditions = []; - this.dragging = false; + this.iconInput = new IconPalette('', container); + this.colorInputs = { + 'background-color': new ColorPalette('icon-paint-bucket', container), + 'border-color': new ColorPalette('icon-line-horz', container), + color: new ColorPalette('icon-font', container) + }; - this.remove = this.remove.bind(this); - this.duplicate = this.duplicate.bind(this); + this.colorInputs.color.toggleNullOption(); - this.thumbnail = this.domElement.querySelector('.t-widget-thumb'); - this.thumbnailIcon = this.domElement.querySelector('.js-sw__icon'); - this.thumbnailLabel = this.domElement.querySelector('.c-sw__label'); - this.title = this.domElement.querySelector('.rule-title'); - this.description = this.domElement.querySelector('.rule-description'); - this.trigger = this.domElement.querySelector('.t-trigger'); - this.toggleConfigButton = this.domElement.querySelector('.js-disclosure'); - this.configArea = this.domElement.querySelector('.widget-rule-content'); - this.grippy = this.domElement.querySelector('.t-grippy'); - this.conditionArea = this.domElement.querySelector('.t-widget-rule-config'); - this.jsConditionArea = this.domElement.querySelector('.t-rule-js-condition-input-holder'); - this.deleteButton = this.domElement.querySelector('.t-delete'); - this.duplicateButton = this.domElement.querySelector('.t-duplicate'); - this.addConditionButton = this.domElement.querySelector('.add-condition'); - - /** - * The text inputs for this rule: any input included in this object will - * have the appropriate event handlers registered to it, and it's corresponding - * field in the domain object will be updated with its value - */ - - this.textInputs = { - name: this.domElement.querySelector('.t-rule-name-input'), - label: this.domElement.querySelector('.t-rule-label-input'), - message: this.domElement.querySelector('.t-rule-message-input'), - jsCondition: this.domElement.querySelector('.t-rule-js-condition-input') - }; - - this.iconInput = new IconPalette('', container); - this.colorInputs = { - 'background-color': new ColorPalette('icon-paint-bucket', container), - 'border-color': new ColorPalette('icon-line-horz', container), - 'color': new ColorPalette('icon-font', container) - }; - - this.colorInputs.color.toggleNullOption(); - - /** - * An onchange event handler method for this rule's icon palettes - * @param {string} icon The css class name corresponding to this icon - * @private - */ - function onIconInput(icon) { - self.config.icon = icon; - self.updateDomainObject('icon', icon); - self.thumbnailIcon.className = `${THUMB_ICON_CLASS + ' ' + icon}`; - self.eventEmitter.emit('change'); - } - - /** - * An onchange event handler method for this rule's color palettes palettes - * @param {string} color The color selected in the palette - * @param {string} property The css property which this color corresponds to - * @private - */ - function onColorInput(color, property) { - self.config.style[property] = color; - self.thumbnail.style[property] = color; - self.eventEmitter.emit('change'); - } - - /** - * Parse input text from textbox to prevent HTML Injection - * @param {string} msg The text to be Parsed - * @private - */ - function encodeMsg(msg) { - const div = document.createElement('div'); - div.innerText = msg; - - return div.innerText; - } - - /** - * An onchange event handler method for this rule's trigger key - * @param {event} event The change event from this rule's select element - * @private - */ - function onTriggerInput(event) { - const elem = event.target; - self.config.trigger = encodeMsg(elem.value); - self.generateDescription(); - self.updateDomainObject(); - self.refreshConditions(); - self.eventEmitter.emit('conditionChange'); - } - - /** - * An onchange event handler method for this rule's text inputs - * @param {element} elem The input element that generated the event - * @param {string} inputKey The field of this rule's configuration to update - * @private - */ - function onTextInput(elem, inputKey) { - const text = encodeMsg(elem.value); - self.config[inputKey] = text; - self.updateDomainObject(); - if (inputKey === 'name') { - self.title.innerText = text; - } else if (inputKey === 'label') { - self.thumbnailLabel.innerText = text; - } - - self.eventEmitter.emit('change'); - } - - /** - * An onchange event handler for a mousedown event that initiates a drag gesture - * @param {event} event A mouseup event that was registered on this rule's grippy - * @private - */ - function onDragStart(event) { - document.querySelectorAll('.t-drag-indicator').forEach(indicator => { - // eslint-disable-next-line no-invalid-this - const ruleHeader = self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true); - indicator.innerHTML = ruleHeader; - }); - self.widgetDnD.setDragImage(self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true)); - self.widgetDnD.dragStart(self.config.id); - self.domElement.style.display = 'none'; - } - - /** - * Show or hide this rule's configuration properties - * @private - */ - function toggleConfig() { - if (self.configArea.classList.contains('expanded')) { - self.configArea.classList.remove('expanded'); - } else { - self.configArea.classList.add('expanded'); - } - - if (self.toggleConfigButton.classList.contains('c-disclosure-triangle--expanded')) { - self.toggleConfigButton.classList.remove('c-disclosure-triangle--expanded'); - } else { - self.toggleConfigButton.classList.add('c-disclosure-triangle--expanded'); - } - - self.config.expanded = !self.config.expanded; - } - - const labelInput = this.domElement.querySelector('.t-rule-label-input'); - labelInput.parentNode.insertBefore(this.iconInput.getDOM(), labelInput); - this.iconInput.set(self.config.icon); - this.iconInput.on('change', function (value) { - onIconInput(value); - }); - - // Initialize thumbs when first loading - this.thumbnailIcon.className = `${THUMB_ICON_CLASS + ' ' + self.config.icon}`; - this.thumbnailLabel.innerText = self.config.label; - - Object.keys(this.colorInputs).forEach(function (inputKey) { - const input = self.colorInputs[inputKey]; - - input.set(self.config.style[inputKey]); - onColorInput(self.config.style[inputKey], inputKey); - - input.on('change', function (value) { - onColorInput(value, inputKey); - self.updateDomainObject(); - }); - - self.domElement.querySelector('.t-style-input').append(input.getDOM()); - }); - - Object.keys(this.textInputs).forEach(function (inputKey) { - if (self.textInputs[inputKey]) { - self.textInputs[inputKey].value = self.config[inputKey] || ''; - self.listenTo(self.textInputs[inputKey], 'input', function () { - // eslint-disable-next-line no-invalid-this - onTextInput(this, inputKey); - }); - } - }); - - this.listenTo(this.deleteButton, 'click', this.remove); - this.listenTo(this.duplicateButton, 'click', this.duplicate); - this.listenTo(this.addConditionButton, 'click', function () { - self.initCondition(); - }); - this.listenTo(this.toggleConfigButton, 'click', toggleConfig); - this.listenTo(this.trigger, 'change', onTriggerInput); - - this.title.innerHTML = self.config.name; - this.description.innerHTML = self.config.description; - this.trigger.value = self.config.trigger; - - this.listenTo(this.grippy, 'mousedown', onDragStart); - this.widgetDnD.on('drop', function () { - // eslint-disable-next-line no-invalid-this - this.domElement.show(); - document.querySelector('.t-drag-indicator').style.display = 'none'; - }, this); - - if (!this.conditionManager.loadCompleted()) { - this.config.expanded = false; - } - - if (!this.config.expanded) { - this.configArea.classList.remove('expanded'); - this.toggleConfigButton.classList.remove('c-disclosure-triangle--expanded'); - } - - if (this.domainObject.configuration.ruleOrder.length === 2) { - this.domElement.querySelector('.t-grippy').style.display = 'none'; - } - - this.refreshConditions(); - - //if this is the default rule, hide elements that don't apply - if (this.config.id === 'default') { - this.domElement.querySelector('.t-delete').style.display = 'none'; - this.domElement.querySelector('.t-widget-rule-config').style.display = 'none'; - this.domElement.querySelector('.t-grippy').style.display = 'none'; - } + /** + * An onchange event handler method for this rule's icon palettes + * @param {string} icon The css class name corresponding to this icon + * @private + */ + function onIconInput(icon) { + self.config.icon = icon; + self.updateDomainObject('icon', icon); + self.thumbnailIcon.className = `${THUMB_ICON_CLASS + ' ' + icon}`; + self.eventEmitter.emit('change'); } /** - * Return the DOM element representing this rule - * @return {Element} A DOM element + * An onchange event handler method for this rule's color palettes palettes + * @param {string} color The color selected in the palette + * @param {string} property The css property which this color corresponds to + * @private */ - Rule.prototype.getDOM = function () { - return this.domElement; - }; + function onColorInput(color, property) { + self.config.style[property] = color; + self.thumbnail.style[property] = color; + self.eventEmitter.emit('change'); + } /** - * Unregister any event handlers registered with external sources + * Parse input text from textbox to prevent HTML Injection + * @param {string} msg The text to be Parsed + * @private */ - Rule.prototype.destroy = function () { - Object.values(this.colorInputs).forEach(function (palette) { - palette.destroy(); + function encodeMsg(msg) { + const div = document.createElement('div'); + div.innerText = msg; + + return div.innerText; + } + + /** + * An onchange event handler method for this rule's trigger key + * @param {event} event The change event from this rule's select element + * @private + */ + function onTriggerInput(event) { + const elem = event.target; + self.config.trigger = encodeMsg(elem.value); + self.generateDescription(); + self.updateDomainObject(); + self.refreshConditions(); + self.eventEmitter.emit('conditionChange'); + } + + /** + * An onchange event handler method for this rule's text inputs + * @param {element} elem The input element that generated the event + * @param {string} inputKey The field of this rule's configuration to update + * @private + */ + function onTextInput(elem, inputKey) { + const text = encodeMsg(elem.value); + self.config[inputKey] = text; + self.updateDomainObject(); + if (inputKey === 'name') { + self.title.innerText = text; + } else if (inputKey === 'label') { + self.thumbnailLabel.innerText = text; + } + + self.eventEmitter.emit('change'); + } + + /** + * An onchange event handler for a mousedown event that initiates a drag gesture + * @param {event} event A mouseup event that was registered on this rule's grippy + * @private + */ + function onDragStart(event) { + document.querySelectorAll('.t-drag-indicator').forEach((indicator) => { + // eslint-disable-next-line no-invalid-this + const ruleHeader = self.domElement + .querySelectorAll('.widget-rule-header')[0] + .cloneNode(true); + indicator.innerHTML = ruleHeader; + }); + self.widgetDnD.setDragImage( + self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true) + ); + self.widgetDnD.dragStart(self.config.id); + self.domElement.style.display = 'none'; + } + + /** + * Show or hide this rule's configuration properties + * @private + */ + function toggleConfig() { + if (self.configArea.classList.contains('expanded')) { + self.configArea.classList.remove('expanded'); + } else { + self.configArea.classList.add('expanded'); + } + + if (self.toggleConfigButton.classList.contains('c-disclosure-triangle--expanded')) { + self.toggleConfigButton.classList.remove('c-disclosure-triangle--expanded'); + } else { + self.toggleConfigButton.classList.add('c-disclosure-triangle--expanded'); + } + + self.config.expanded = !self.config.expanded; + } + + const labelInput = this.domElement.querySelector('.t-rule-label-input'); + labelInput.parentNode.insertBefore(this.iconInput.getDOM(), labelInput); + this.iconInput.set(self.config.icon); + this.iconInput.on('change', function (value) { + onIconInput(value); + }); + + // Initialize thumbs when first loading + this.thumbnailIcon.className = `${THUMB_ICON_CLASS + ' ' + self.config.icon}`; + this.thumbnailLabel.innerText = self.config.label; + + Object.keys(this.colorInputs).forEach(function (inputKey) { + const input = self.colorInputs[inputKey]; + + input.set(self.config.style[inputKey]); + onColorInput(self.config.style[inputKey], inputKey); + + input.on('change', function (value) { + onColorInput(value, inputKey); + self.updateDomainObject(); + }); + + self.domElement.querySelector('.t-style-input').append(input.getDOM()); + }); + + Object.keys(this.textInputs).forEach(function (inputKey) { + if (self.textInputs[inputKey]) { + self.textInputs[inputKey].value = self.config[inputKey] || ''; + self.listenTo(self.textInputs[inputKey], 'input', function () { + // eslint-disable-next-line no-invalid-this + onTextInput(this, inputKey); }); - this.iconInput.destroy(); - this.stopListening(); - this.conditions.forEach(function (condition) { - condition.destroy(); - }); - }; + } + }); - /** - * Register a callback with this rule: supported callbacks are remove, change, - * conditionChange, and duplicate - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - Rule.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } - }; + this.listenTo(this.deleteButton, 'click', this.remove); + this.listenTo(this.duplicateButton, 'click', this.duplicate); + this.listenTo(this.addConditionButton, 'click', function () { + self.initCondition(); + }); + this.listenTo(this.toggleConfigButton, 'click', toggleConfig); + this.listenTo(this.trigger, 'change', onTriggerInput); - /** - * An event handler for when a condition's configuration is modified - * @param {} value - * @param {string} property The path in the configuration to updateDomainObject - * @param {number} index The index of the condition that initiated this change - */ - Rule.prototype.onConditionChange = function (event) { - _.set(this.config.conditions[event.index], event.property, event.value); - this.generateDescription(); - this.updateDomainObject(); - this.eventEmitter.emit('conditionChange'); - }; + this.title.innerHTML = self.config.name; + this.description.innerHTML = self.config.description; + this.trigger.value = self.config.trigger; - /** - * During a rule drag event, show the placeholder element after this rule - */ - Rule.prototype.showDragIndicator = function () { + this.listenTo(this.grippy, 'mousedown', onDragStart); + this.widgetDnD.on( + 'drop', + function () { + // eslint-disable-next-line no-invalid-this + this.domElement.show(); document.querySelector('.t-drag-indicator').style.display = 'none'; - this.domElement.querySelector('.t-drag-indicator').style.display = ''; + }, + this + ); + + if (!this.conditionManager.loadCompleted()) { + this.config.expanded = false; + } + + if (!this.config.expanded) { + this.configArea.classList.remove('expanded'); + this.toggleConfigButton.classList.remove('c-disclosure-triangle--expanded'); + } + + if (this.domainObject.configuration.ruleOrder.length === 2) { + this.domElement.querySelector('.t-grippy').style.display = 'none'; + } + + this.refreshConditions(); + + //if this is the default rule, hide elements that don't apply + if (this.config.id === 'default') { + this.domElement.querySelector('.t-delete').style.display = 'none'; + this.domElement.querySelector('.t-widget-rule-config').style.display = 'none'; + this.domElement.querySelector('.t-grippy').style.display = 'none'; + } + } + + /** + * Return the DOM element representing this rule + * @return {Element} A DOM element + */ + Rule.prototype.getDOM = function () { + return this.domElement; + }; + + /** + * Unregister any event handlers registered with external sources + */ + Rule.prototype.destroy = function () { + Object.values(this.colorInputs).forEach(function (palette) { + palette.destroy(); + }); + this.iconInput.destroy(); + this.stopListening(); + this.conditions.forEach(function (condition) { + condition.destroy(); + }); + }; + + /** + * Register a callback with this rule: supported callbacks are remove, change, + * conditionChange, and duplicate + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + Rule.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } + }; + + /** + * An event handler for when a condition's configuration is modified + * @param {} value + * @param {string} property The path in the configuration to updateDomainObject + * @param {number} index The index of the condition that initiated this change + */ + Rule.prototype.onConditionChange = function (event) { + _.set(this.config.conditions[event.index], event.property, event.value); + this.generateDescription(); + this.updateDomainObject(); + this.eventEmitter.emit('conditionChange'); + }; + + /** + * During a rule drag event, show the placeholder element after this rule + */ + Rule.prototype.showDragIndicator = function () { + document.querySelector('.t-drag-indicator').style.display = 'none'; + this.domElement.querySelector('.t-drag-indicator').style.display = ''; + }; + + /** + * Mutate thet domain object with this rule's local configuration + */ + Rule.prototype.updateDomainObject = function () { + this.openmct.objects.mutate( + this.domainObject, + 'configuration.ruleConfigById.' + this.config.id, + this.config + ); + }; + + /** + * Get a property of this rule by key + * @param {string} prop They property key of this rule to get + * @return {} The queried property + */ + Rule.prototype.getProperty = function (prop) { + return this.config[prop]; + }; + + /** + * Remove this rule from the domain object's configuration and invoke any + * registered remove callbacks + */ + Rule.prototype.remove = function () { + const ruleOrder = this.domainObject.configuration.ruleOrder; + const ruleConfigById = this.domainObject.configuration.ruleConfigById; + const self = this; + + ruleConfigById[self.config.id] = undefined; + _.remove(ruleOrder, function (ruleId) { + return ruleId === self.config.id; + }); + + this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById', ruleConfigById); + this.openmct.objects.mutate(this.domainObject, 'configuration.ruleOrder', ruleOrder); + this.destroy(); + this.eventEmitter.emit('remove'); + }; + + /** + * Makes a deep clone of this rule's configuration, and calls the duplicate event + * callback with the cloned configuration as an argument if one has been registered + */ + Rule.prototype.duplicate = function () { + const sourceRule = JSON.parse(JSON.stringify(this.config)); + sourceRule.expanded = true; + this.eventEmitter.emit('duplicate', sourceRule); + }; + + /** + * Initialze a new condition. If called with the sourceConfig and sourceIndex arguments, + * will insert a new condition with the provided configuration after the sourceIndex + * index. Otherwise, initializes a new blank rule and inserts it at the end + * of the list. + * @param {Object} [config] The configuration to initialize this rule from, + * consisting of sourceCondition and index fields + */ + Rule.prototype.initCondition = function (config) { + const ruleConfigById = this.domainObject.configuration.ruleConfigById; + let newConfig; + const sourceIndex = config && config.index; + const defaultConfig = { + object: '', + key: '', + operation: '', + values: [] }; - /** - * Mutate thet domain object with this rule's local configuration - */ - Rule.prototype.updateDomainObject = function () { - this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById.' - + this.config.id, this.config); - }; + newConfig = config !== undefined ? config.sourceCondition : defaultConfig; + if (sourceIndex !== undefined) { + ruleConfigById[this.config.id].conditions.splice(sourceIndex + 1, 0, newConfig); + } else { + ruleConfigById[this.config.id].conditions.push(newConfig); + } - /** - * Get a property of this rule by key - * @param {string} prop They property key of this rule to get - * @return {} The queried property - */ - Rule.prototype.getProperty = function (prop) { - return this.config[prop]; - }; + this.domainObject.configuration.ruleConfigById = ruleConfigById; + this.updateDomainObject(); + this.refreshConditions(); + this.generateDescription(); + }; - /** - * Remove this rule from the domain object's configuration and invoke any - * registered remove callbacks - */ - Rule.prototype.remove = function () { - const ruleOrder = this.domainObject.configuration.ruleOrder; - const ruleConfigById = this.domainObject.configuration.ruleConfigById; - const self = this; + /** + * Build {Condition} objects from configuration and rebuild associated view + */ + Rule.prototype.refreshConditions = function () { + const self = this; + let $condition = null; + let loopCnt = 0; + const triggerContextStr = self.config.trigger === 'any' ? ' or ' : ' and '; - ruleConfigById[self.config.id] = undefined; - _.remove(ruleOrder, function (ruleId) { - return ruleId === self.config.id; - }); + self.conditions = []; - this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById', ruleConfigById); - this.openmct.objects.mutate(this.domainObject, 'configuration.ruleOrder', ruleOrder); - this.destroy(); - this.eventEmitter.emit('remove'); - }; + this.domElement.querySelectorAll('.t-condition').forEach((condition) => { + condition.remove(); + }); - /** - * Makes a deep clone of this rule's configuration, and calls the duplicate event - * callback with the cloned configuration as an argument if one has been registered - */ - Rule.prototype.duplicate = function () { - const sourceRule = JSON.parse(JSON.stringify(this.config)); - sourceRule.expanded = true; - this.eventEmitter.emit('duplicate', sourceRule); - }; + this.config.conditions.forEach(function (condition, index) { + const newCondition = new Condition(condition, index, self.conditionManager); + newCondition.on('remove', self.removeCondition, self); + newCondition.on('duplicate', self.initCondition, self); + newCondition.on('change', self.onConditionChange, self); + self.conditions.push(newCondition); + }); - /** - * Initialze a new condition. If called with the sourceConfig and sourceIndex arguments, - * will insert a new condition with the provided configuration after the sourceIndex - * index. Otherwise, initializes a new blank rule and inserts it at the end - * of the list. - * @param {Object} [config] The configuration to initialize this rule from, - * consisting of sourceCondition and index fields - */ - Rule.prototype.initCondition = function (config) { - const ruleConfigById = this.domainObject.configuration.ruleConfigById; - let newConfig; - const sourceIndex = config && config.index; - const defaultConfig = { - object: '', - key: '', - operation: '', - values: [] - }; + if (this.config.trigger === 'js') { + if (this.jsConditionArea) { + this.jsConditionArea.style.display = ''; + } - newConfig = (config !== undefined ? config.sourceCondition : defaultConfig); - if (sourceIndex !== undefined) { - ruleConfigById[this.config.id].conditions.splice(sourceIndex + 1, 0, newConfig); - } else { - ruleConfigById[this.config.id].conditions.push(newConfig); + this.addConditionButton.style.display = 'none'; + } else { + if (this.jsConditionArea) { + this.jsConditionArea.style.display = 'none'; + } + + this.addConditionButton.style.display = ''; + self.conditions.forEach(function (condition) { + $condition = condition.getDOM(); + const lastOfType = self.conditionArea.querySelector('li:last-of-type'); + lastOfType.parentNode.insertBefore($condition, lastOfType); + if (loopCnt > 0) { + $condition.querySelector('.t-condition-context').innerHTML = triggerContextStr + ' when'; } - this.domainObject.configuration.ruleConfigById = ruleConfigById; - this.updateDomainObject(); - this.refreshConditions(); - this.generateDescription(); - }; + loopCnt++; + }); + } - /** - * Build {Condition} objects from configuration and rebuild associated view - */ - Rule.prototype.refreshConditions = function () { - const self = this; - let $condition = null; - let loopCnt = 0; - const triggerContextStr = self.config.trigger === 'any' ? ' or ' : ' and '; + if (self.conditions.length === 1) { + self.conditions[0].hideButtons(); + } + }; - self.conditions = []; + /** + * Remove a condition from this rule's configuration at the given index + * @param {number} removeIndex The index of the condition to remove + */ + Rule.prototype.removeCondition = function (removeIndex) { + const ruleConfigById = this.domainObject.configuration.ruleConfigById; + const conditions = ruleConfigById[this.config.id].conditions; - this.domElement.querySelectorAll('.t-condition').forEach(condition => { - condition.remove(); - }); + _.remove(conditions, function (condition, index) { + return index === removeIndex; + }); + this.domainObject.configuration.ruleConfigById[this.config.id] = this.config; + this.updateDomainObject(); + this.refreshConditions(); + this.generateDescription(); + this.eventEmitter.emit('conditionChange'); + }; + + /** + * Build a human-readable description from this rule's conditions + */ + Rule.prototype.generateDescription = function () { + let description = ''; + const manager = this.conditionManager; + const evaluator = manager.getEvaluator(); + let name; + let property; + let operation; + const self = this; + + if (this.config.conditions && this.config.id !== 'default') { + if (self.config.trigger === 'js') { + description = 'when a custom JavaScript condition evaluates to true'; + } else { this.config.conditions.forEach(function (condition, index) { - const newCondition = new Condition(condition, index, self.conditionManager); - newCondition.on('remove', self.removeCondition, self); - newCondition.on('duplicate', self.initCondition, self); - newCondition.on('change', self.onConditionChange, self); - self.conditions.push(newCondition); + name = manager.getObjectName(condition.object); + property = manager.getTelemetryPropertyName(condition.object, condition.key); + operation = evaluator.getOperationDescription(condition.operation, condition.values); + if (name || property || operation) { + description += + 'when ' + + (name ? name + "'s " : '') + + (property ? property + ' ' : '') + + (operation ? operation + ' ' : '') + + (self.config.trigger === 'any' ? ' OR ' : ' AND '); + } }); + } + } - if (this.config.trigger === 'js') { - if (this.jsConditionArea) { - this.jsConditionArea.style.display = ''; - } + if (description.endsWith('OR ')) { + description = description.substring(0, description.length - 3); + } - this.addConditionButton.style.display = 'none'; - } else { - if (this.jsConditionArea) { - this.jsConditionArea.style.display = 'none'; - } + if (description.endsWith('AND ')) { + description = description.substring(0, description.length - 4); + } - this.addConditionButton.style.display = ''; - self.conditions.forEach(function (condition) { - $condition = condition.getDOM(); - const lastOfType = self.conditionArea.querySelector('li:last-of-type'); - lastOfType.parentNode.insertBefore($condition, lastOfType); - if (loopCnt > 0) { - $condition.querySelector('.t-condition-context').innerHTML = triggerContextStr + ' when'; - } + description = description === '' ? this.config.description : description; + this.description.innerHTML = self.config.description; + this.config.description = description; + }; - loopCnt++; - }); - } - - if (self.conditions.length === 1) { - self.conditions[0].hideButtons(); - } - - }; - - /** - * Remove a condition from this rule's configuration at the given index - * @param {number} removeIndex The index of the condition to remove - */ - Rule.prototype.removeCondition = function (removeIndex) { - const ruleConfigById = this.domainObject.configuration.ruleConfigById; - const conditions = ruleConfigById[this.config.id].conditions; - - _.remove(conditions, function (condition, index) { - return index === removeIndex; - }); - - this.domainObject.configuration.ruleConfigById[this.config.id] = this.config; - this.updateDomainObject(); - this.refreshConditions(); - this.generateDescription(); - this.eventEmitter.emit('conditionChange'); - }; - - /** - * Build a human-readable description from this rule's conditions - */ - Rule.prototype.generateDescription = function () { - let description = ''; - const manager = this.conditionManager; - const evaluator = manager.getEvaluator(); - let name; - let property; - let operation; - const self = this; - - if (this.config.conditions && this.config.id !== 'default') { - if (self.config.trigger === 'js') { - description = 'when a custom JavaScript condition evaluates to true'; - } else { - this.config.conditions.forEach(function (condition, index) { - name = manager.getObjectName(condition.object); - property = manager.getTelemetryPropertyName(condition.object, condition.key); - operation = evaluator.getOperationDescription(condition.operation, condition.values); - if (name || property || operation) { - description += 'when ' - + (name ? name + '\'s ' : '') - + (property ? property + ' ' : '') - + (operation ? operation + ' ' : '') - + (self.config.trigger === 'any' ? ' OR ' : ' AND '); - } - }); - } - } - - if (description.endsWith('OR ')) { - description = description.substring(0, description.length - 3); - } - - if (description.endsWith('AND ')) { - description = description.substring(0, description.length - 4); - } - - description = (description === '' ? this.config.description : description); - this.description.innerHTML = self.config.description; - this.config.description = description; - }; - - return Rule; + return Rule; }); diff --git a/src/plugins/summaryWidget/src/SummaryWidget.js b/src/plugins/summaryWidget/src/SummaryWidget.js index e9c1442bf2..3a6966bf35 100644 --- a/src/plugins/summaryWidget/src/SummaryWidget.js +++ b/src/plugins/summaryWidget/src/SummaryWidget.js @@ -1,382 +1,412 @@ -define([ - '../res/widgetTemplate.html', - './Rule', - './ConditionManager', - './TestDataManager', - './WidgetDnD', - './eventHelpers', - '../../../utils/template/templateHelpers', - 'objectUtils', - 'lodash', - '@braintree/sanitize-url' -], function ( - widgetTemplate, - Rule, - ConditionManager, - TestDataManager, - WidgetDnD, - eventHelpers, - templateHelpers, - objectUtils, - _, - urlSanitizeLib -) { - - //default css configuration for new rules - const DEFAULT_PROPS = { - 'color': '#cccccc', - 'background-color': '#666666', - 'border-color': 'rgba(0,0,0,0)' - }; - - /** - * A Summary Widget object, which allows a user to configure rules based - * on telemetry producing domain objects, and update a compact display - * accordingly. - * @constructor - * @param {Object} domainObject The domain Object represented by this Widget - * @param {MCT} openmct An MCT instance - */ - function SummaryWidget(domainObject, openmct) { - eventHelpers.extend(this); - - this.domainObject = domainObject; - this.openmct = openmct; - - this.domainObject.configuration = this.domainObject.configuration || {}; - this.domainObject.configuration.ruleConfigById = this.domainObject.configuration.ruleConfigById || {}; - this.domainObject.configuration.ruleOrder = this.domainObject.configuration.ruleOrder || ['default']; - this.domainObject.configuration.testDataConfig = this.domainObject.configuration.testDataConfig || [{ - object: '', - key: '', - value: '' - }]; - - this.activeId = 'default'; - this.rulesById = {}; - this.domElement = templateHelpers.convertTemplateToHTML(widgetTemplate)[0]; - this.toggleRulesControl = this.domElement.querySelector('.t-view-control-rules'); - this.toggleTestDataControl = this.domElement.querySelector('.t-view-control-test-data'); - - this.widgetButton = this.domElement.querySelector(':scope > #widget'); - - this.editing = false; - this.container = ''; - this.editListenerUnsubscribe = () => {}; - - this.outerWrapper = this.domElement.querySelector('.widget-edit-holder'); - this.ruleArea = this.domElement.querySelector('#ruleArea'); - this.configAreaRules = this.domElement.querySelector('.widget-rules-wrapper'); - - this.testDataArea = this.domElement.querySelector('.widget-test-data'); - this.addRuleButton = this.domElement.querySelector('#addRule'); - - this.conditionManager = new ConditionManager(this.domainObject, this.openmct); - this.testDataManager = new TestDataManager(this.domainObject, this.conditionManager, this.openmct); - - this.watchForChanges = this.watchForChanges.bind(this); - this.show = this.show.bind(this); - this.destroy = this.destroy.bind(this); - this.addRule = this.addRule.bind(this); - - this.addHyperlink(domainObject.url, domainObject.openNewTab); - this.watchForChanges(openmct, domainObject); - - const self = this; - - /** - * Toggles the configuration area for test data in the view - * @private - */ - function toggleTestData() { - if (self.outerWrapper.classList.contains('expanded-widget-test-data')) { - self.outerWrapper.classList.remove('expanded-widget-test-data'); - } else { - self.outerWrapper.classList.add('expanded-widget-test-data'); - } - - if (self.toggleTestDataControl.classList.contains('c-disclosure-triangle--expanded')) { - self.toggleTestDataControl.classList.remove('c-disclosure-triangle--expanded'); - } else { - self.toggleTestDataControl.classList.add('c-disclosure-triangle--expanded'); - } - } - - this.listenTo(this.toggleTestDataControl, 'click', toggleTestData); - - /** - * Toggles the configuration area for rules in the view - * @private - */ - function toggleRules() { - templateHelpers.toggleClass(self.outerWrapper, 'expanded-widget-rules'); - templateHelpers.toggleClass(self.toggleRulesControl, 'c-disclosure-triangle--expanded'); - } - - this.listenTo(this.toggleRulesControl, 'click', toggleRules); - - } - - /** - * adds or removes href to widget button and adds or removes openInNewTab - * @param {string} url String that denotes the url to be opened - * @param {string} openNewTab String that denotes wether to open link in new tab or not - */ - SummaryWidget.prototype.addHyperlink = function (url, openNewTab) { - if (url) { - this.widgetButton.href = urlSanitizeLib.sanitizeUrl(url); - } else { - this.widgetButton.removeAttribute('href'); - } - - if (openNewTab === 'newTab') { - this.widgetButton.target = '_blank'; - } else { - this.widgetButton.removeAttribute('target'); - } - }; - - /** - * adds a listener to the object to watch for any changes made by user - * only executes if changes are observed - * @param {openmct} Object Instance of OpenMCT - * @param {domainObject} Object instance of this object - */ - SummaryWidget.prototype.watchForChanges = function (openmct, domainObject) { - this.watchForChangesUnsubscribe = openmct.objects.observe(domainObject, '*', function (newDomainObject) { - if (newDomainObject.url !== this.domainObject.url - || newDomainObject.openNewTab !== this.domainObject.openNewTab) { - this.addHyperlink(newDomainObject.url, newDomainObject.openNewTab); - } - }.bind(this)); - }; - - /** - * Builds the Summary Widget's DOM, performs other necessary setup, and attaches - * this Summary Widget's view to the supplied container. - * @param {element} container The DOM element that will contain this Summary - * Widget's view. - */ - SummaryWidget.prototype.show = function (container) { - const self = this; - this.container = container; - this.container.append(this.domElement); - this.domElement.querySelector('.widget-test-data').append(this.testDataManager.getDOM()); - this.widgetDnD = new WidgetDnD(this.domElement, this.domainObject.configuration.ruleOrder, this.rulesById); - this.initRule('default', 'Default'); - this.domainObject.configuration.ruleOrder.forEach(function (ruleId) { - if (ruleId !== 'default') { - self.initRule(ruleId); - } - }); - this.refreshRules(); - this.updateWidget(); - - this.listenTo(this.addRuleButton, 'click', this.addRule); - this.conditionManager.on('receiveTelemetry', this.executeRules, this); - this.widgetDnD.on('drop', this.reorder, this); - }; - - /** - * Unregister event listeners with the Open MCT APIs, unsubscribe from telemetry, - * and clean up event handlers - */ - SummaryWidget.prototype.destroy = function (container) { - this.editListenerUnsubscribe(); - this.conditionManager.destroy(); - this.testDataManager.destroy(); - this.widgetDnD.destroy(); - this.watchForChangesUnsubscribe(); - Object.values(this.rulesById).forEach(function (rule) { - rule.destroy(); - }); - - this.stopListening(); - }; - - /** - * Update the view from the current rule configuration and order - */ - SummaryWidget.prototype.refreshRules = function () { - const self = this; - const ruleOrder = self.domainObject.configuration.ruleOrder; - const rules = self.rulesById; - self.ruleArea.innerHTML = ''; - Object.values(ruleOrder).forEach(function (ruleId) { - self.ruleArea.append(rules[ruleId].getDOM()); - }); - - this.executeRules(); - this.addOrRemoveDragIndicator(); - }; - - SummaryWidget.prototype.addOrRemoveDragIndicator = function () { - const rules = this.domainObject.configuration.ruleOrder; - const rulesById = this.rulesById; - - rules.forEach(function (ruleKey, index, array) { - if (array.length > 2 && index > 0) { - rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = ''; - } else { - rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = 'none'; - } - }); - }; - - /** - * Update the widget's appearance from the configuration of the active rule - */ - SummaryWidget.prototype.updateWidget = function () { - const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon'; - const activeRule = this.rulesById[this.activeId]; - this.applyStyle(this.domElement.querySelector('#widget'), activeRule.getProperty('style')); - this.domElement.querySelector('#widget').title = activeRule.getProperty('message'); - this.domElement.querySelector('#widgetLabel').innerHTML = activeRule.getProperty('label'); - this.domElement.querySelector('#widgetIcon').classList = WIDGET_ICON_CLASS + ' ' + activeRule.getProperty('icon'); - }; - - /** - * Get the active rule and update the Widget's appearance. - */ - SummaryWidget.prototype.executeRules = function () { - this.activeId = this.conditionManager.executeRules( - this.domainObject.configuration.ruleOrder, - this.rulesById - ); - this.updateWidget(); - }; - - /** - * Add a new rule to this widget - */ - SummaryWidget.prototype.addRule = function () { - let ruleCount = 0; - let ruleId; - const ruleOrder = this.domainObject.configuration.ruleOrder; - - while (Object.keys(this.rulesById).includes('rule' + ruleCount)) { - ruleCount++; - } - - ruleId = 'rule' + ruleCount; - ruleOrder.push(ruleId); - this.domainObject.configuration.ruleOrder = ruleOrder; - - this.initRule(ruleId, 'Rule'); - this.updateDomainObject(); - this.refreshRules(); - }; - - /** - * Duplicate an existing widget rule from its configuration and splice it in - * after the rule it duplicates - * @param {Object} sourceConfig The configuration properties of the rule to be - * instantiated - */ - SummaryWidget.prototype.duplicateRule = function (sourceConfig) { - let ruleCount = 0; - let ruleId; - const sourceRuleId = sourceConfig.id; - const ruleOrder = this.domainObject.configuration.ruleOrder; - const ruleIds = Object.keys(this.rulesById); - - while (ruleIds.includes('rule' + ruleCount)) { - ruleCount = ++ruleCount; - } - - ruleId = 'rule' + ruleCount; - sourceConfig.id = ruleId; - sourceConfig.name += ' Copy'; - ruleOrder.splice(ruleOrder.indexOf(sourceRuleId) + 1, 0, ruleId); - this.domainObject.configuration.ruleOrder = ruleOrder; - this.domainObject.configuration.ruleConfigById[ruleId] = sourceConfig; - this.initRule(ruleId, sourceConfig.name); - this.updateDomainObject(); - this.refreshRules(); - }; - - /** - * Initialze a new rule from a default configuration, or build a {Rule} object - * from it if already exists - * @param {string} ruleId An key to be used to identify this ruleId, or the key - of the rule to be instantiated - * @param {string} ruleName The initial human-readable name of this rule - */ - SummaryWidget.prototype.initRule = function (ruleId, ruleName) { - let ruleConfig; - const styleObj = {}; - - Object.assign(styleObj, DEFAULT_PROPS); - if (!this.domainObject.configuration.ruleConfigById[ruleId]) { - this.domainObject.configuration.ruleConfigById[ruleId] = { - name: ruleName || 'Rule', - label: 'Unnamed Rule', - message: '', - id: ruleId, - icon: ' ', - style: styleObj, - description: ruleId === 'default' ? 'Default appearance for the widget' : 'A new rule', - conditions: [{ - object: '', - key: '', - operation: '', - values: [] - }], - jsCondition: '', - trigger: 'any', - expanded: 'true' - }; - - } - - ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId]; - this.rulesById[ruleId] = new Rule(ruleConfig, this.domainObject, this.openmct, - this.conditionManager, this.widgetDnD, this.container); - this.rulesById[ruleId].on('remove', this.refreshRules, this); - this.rulesById[ruleId].on('duplicate', this.duplicateRule, this); - this.rulesById[ruleId].on('change', this.updateWidget, this); - this.rulesById[ruleId].on('conditionChange', this.executeRules, this); - }; - - /** - * Given two ruleIds, move the source rule after the target rule and update - * the view. - * @param {Object} event An event object representing this drop with draggingId - * and dropTarget fields - */ - SummaryWidget.prototype.reorder = function (event) { - const ruleOrder = this.domainObject.configuration.ruleOrder; - const sourceIndex = ruleOrder.indexOf(event.draggingId); - let targetIndex; - - if (event.draggingId !== event.dropTarget) { - ruleOrder.splice(sourceIndex, 1); - targetIndex = ruleOrder.indexOf(event.dropTarget); - ruleOrder.splice(targetIndex + 1, 0, event.draggingId); - this.domainObject.configuration.ruleOrder = ruleOrder; - this.updateDomainObject(); - } - - this.refreshRules(); - }; - - /** - * Apply a list of css properties to an element - * @param {element} elem The DOM element to which the rules will be applied - * @param {object} style an object representing the style - */ - SummaryWidget.prototype.applyStyle = function (elem, style) { - Object.keys(style).forEach(function (propId) { - elem.style[propId] = style[propId]; - }); - }; - - /** - * Mutate this domain object's configuration with the current local configuration - */ - SummaryWidget.prototype.updateDomainObject = function () { - this.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration); - }; - - return SummaryWidget; -}); +define([ + '../res/widgetTemplate.html', + './Rule', + './ConditionManager', + './TestDataManager', + './WidgetDnD', + './eventHelpers', + '../../../utils/template/templateHelpers', + 'objectUtils', + 'lodash', + '@braintree/sanitize-url' +], function ( + widgetTemplate, + Rule, + ConditionManager, + TestDataManager, + WidgetDnD, + eventHelpers, + templateHelpers, + objectUtils, + _, + urlSanitizeLib +) { + //default css configuration for new rules + const DEFAULT_PROPS = { + color: '#cccccc', + 'background-color': '#666666', + 'border-color': 'rgba(0,0,0,0)' + }; + + /** + * A Summary Widget object, which allows a user to configure rules based + * on telemetry producing domain objects, and update a compact display + * accordingly. + * @constructor + * @param {Object} domainObject The domain Object represented by this Widget + * @param {MCT} openmct An MCT instance + */ + function SummaryWidget(domainObject, openmct) { + eventHelpers.extend(this); + + this.domainObject = domainObject; + this.openmct = openmct; + + this.domainObject.configuration = this.domainObject.configuration || {}; + this.domainObject.configuration.ruleConfigById = + this.domainObject.configuration.ruleConfigById || {}; + this.domainObject.configuration.ruleOrder = this.domainObject.configuration.ruleOrder || [ + 'default' + ]; + this.domainObject.configuration.testDataConfig = this.domainObject.configuration + .testDataConfig || [ + { + object: '', + key: '', + value: '' + } + ]; + + this.activeId = 'default'; + this.rulesById = {}; + this.domElement = templateHelpers.convertTemplateToHTML(widgetTemplate)[0]; + this.toggleRulesControl = this.domElement.querySelector('.t-view-control-rules'); + this.toggleTestDataControl = this.domElement.querySelector('.t-view-control-test-data'); + + this.widgetButton = this.domElement.querySelector(':scope > #widget'); + + this.editing = false; + this.container = ''; + this.editListenerUnsubscribe = () => {}; + + this.outerWrapper = this.domElement.querySelector('.widget-edit-holder'); + this.ruleArea = this.domElement.querySelector('#ruleArea'); + this.configAreaRules = this.domElement.querySelector('.widget-rules-wrapper'); + + this.testDataArea = this.domElement.querySelector('.widget-test-data'); + this.addRuleButton = this.domElement.querySelector('#addRule'); + + this.conditionManager = new ConditionManager(this.domainObject, this.openmct); + this.testDataManager = new TestDataManager( + this.domainObject, + this.conditionManager, + this.openmct + ); + + this.watchForChanges = this.watchForChanges.bind(this); + this.show = this.show.bind(this); + this.destroy = this.destroy.bind(this); + this.addRule = this.addRule.bind(this); + + this.addHyperlink(domainObject.url, domainObject.openNewTab); + this.watchForChanges(openmct, domainObject); + + const self = this; + + /** + * Toggles the configuration area for test data in the view + * @private + */ + function toggleTestData() { + if (self.outerWrapper.classList.contains('expanded-widget-test-data')) { + self.outerWrapper.classList.remove('expanded-widget-test-data'); + } else { + self.outerWrapper.classList.add('expanded-widget-test-data'); + } + + if (self.toggleTestDataControl.classList.contains('c-disclosure-triangle--expanded')) { + self.toggleTestDataControl.classList.remove('c-disclosure-triangle--expanded'); + } else { + self.toggleTestDataControl.classList.add('c-disclosure-triangle--expanded'); + } + } + + this.listenTo(this.toggleTestDataControl, 'click', toggleTestData); + + /** + * Toggles the configuration area for rules in the view + * @private + */ + function toggleRules() { + templateHelpers.toggleClass(self.outerWrapper, 'expanded-widget-rules'); + templateHelpers.toggleClass(self.toggleRulesControl, 'c-disclosure-triangle--expanded'); + } + + this.listenTo(this.toggleRulesControl, 'click', toggleRules); + } + + /** + * adds or removes href to widget button and adds or removes openInNewTab + * @param {string} url String that denotes the url to be opened + * @param {string} openNewTab String that denotes wether to open link in new tab or not + */ + SummaryWidget.prototype.addHyperlink = function (url, openNewTab) { + if (url) { + this.widgetButton.href = urlSanitizeLib.sanitizeUrl(url); + } else { + this.widgetButton.removeAttribute('href'); + } + + if (openNewTab === 'newTab') { + this.widgetButton.target = '_blank'; + } else { + this.widgetButton.removeAttribute('target'); + } + }; + + /** + * adds a listener to the object to watch for any changes made by user + * only executes if changes are observed + * @param {openmct} Object Instance of OpenMCT + * @param {domainObject} Object instance of this object + */ + SummaryWidget.prototype.watchForChanges = function (openmct, domainObject) { + this.watchForChangesUnsubscribe = openmct.objects.observe( + domainObject, + '*', + function (newDomainObject) { + if ( + newDomainObject.url !== this.domainObject.url || + newDomainObject.openNewTab !== this.domainObject.openNewTab + ) { + this.addHyperlink(newDomainObject.url, newDomainObject.openNewTab); + } + }.bind(this) + ); + }; + + /** + * Builds the Summary Widget's DOM, performs other necessary setup, and attaches + * this Summary Widget's view to the supplied container. + * @param {element} container The DOM element that will contain this Summary + * Widget's view. + */ + SummaryWidget.prototype.show = function (container) { + const self = this; + this.container = container; + this.container.append(this.domElement); + this.domElement.querySelector('.widget-test-data').append(this.testDataManager.getDOM()); + this.widgetDnD = new WidgetDnD( + this.domElement, + this.domainObject.configuration.ruleOrder, + this.rulesById + ); + this.initRule('default', 'Default'); + this.domainObject.configuration.ruleOrder.forEach(function (ruleId) { + if (ruleId !== 'default') { + self.initRule(ruleId); + } + }); + this.refreshRules(); + this.updateWidget(); + + this.listenTo(this.addRuleButton, 'click', this.addRule); + this.conditionManager.on('receiveTelemetry', this.executeRules, this); + this.widgetDnD.on('drop', this.reorder, this); + }; + + /** + * Unregister event listeners with the Open MCT APIs, unsubscribe from telemetry, + * and clean up event handlers + */ + SummaryWidget.prototype.destroy = function (container) { + this.editListenerUnsubscribe(); + this.conditionManager.destroy(); + this.testDataManager.destroy(); + this.widgetDnD.destroy(); + this.watchForChangesUnsubscribe(); + Object.values(this.rulesById).forEach(function (rule) { + rule.destroy(); + }); + + this.stopListening(); + }; + + /** + * Update the view from the current rule configuration and order + */ + SummaryWidget.prototype.refreshRules = function () { + const self = this; + const ruleOrder = self.domainObject.configuration.ruleOrder; + const rules = self.rulesById; + self.ruleArea.innerHTML = ''; + Object.values(ruleOrder).forEach(function (ruleId) { + self.ruleArea.append(rules[ruleId].getDOM()); + }); + + this.executeRules(); + this.addOrRemoveDragIndicator(); + }; + + SummaryWidget.prototype.addOrRemoveDragIndicator = function () { + const rules = this.domainObject.configuration.ruleOrder; + const rulesById = this.rulesById; + + rules.forEach(function (ruleKey, index, array) { + if (array.length > 2 && index > 0) { + rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = ''; + } else { + rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = 'none'; + } + }); + }; + + /** + * Update the widget's appearance from the configuration of the active rule + */ + SummaryWidget.prototype.updateWidget = function () { + const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon'; + const activeRule = this.rulesById[this.activeId]; + this.applyStyle(this.domElement.querySelector('#widget'), activeRule.getProperty('style')); + this.domElement.querySelector('#widget').title = activeRule.getProperty('message'); + this.domElement.querySelector('#widgetLabel').innerHTML = activeRule.getProperty('label'); + this.domElement.querySelector('#widgetIcon').classList = + WIDGET_ICON_CLASS + ' ' + activeRule.getProperty('icon'); + }; + + /** + * Get the active rule and update the Widget's appearance. + */ + SummaryWidget.prototype.executeRules = function () { + this.activeId = this.conditionManager.executeRules( + this.domainObject.configuration.ruleOrder, + this.rulesById + ); + this.updateWidget(); + }; + + /** + * Add a new rule to this widget + */ + SummaryWidget.prototype.addRule = function () { + let ruleCount = 0; + let ruleId; + const ruleOrder = this.domainObject.configuration.ruleOrder; + + while (Object.keys(this.rulesById).includes('rule' + ruleCount)) { + ruleCount++; + } + + ruleId = 'rule' + ruleCount; + ruleOrder.push(ruleId); + this.domainObject.configuration.ruleOrder = ruleOrder; + + this.initRule(ruleId, 'Rule'); + this.updateDomainObject(); + this.refreshRules(); + }; + + /** + * Duplicate an existing widget rule from its configuration and splice it in + * after the rule it duplicates + * @param {Object} sourceConfig The configuration properties of the rule to be + * instantiated + */ + SummaryWidget.prototype.duplicateRule = function (sourceConfig) { + let ruleCount = 0; + let ruleId; + const sourceRuleId = sourceConfig.id; + const ruleOrder = this.domainObject.configuration.ruleOrder; + const ruleIds = Object.keys(this.rulesById); + + while (ruleIds.includes('rule' + ruleCount)) { + ruleCount = ++ruleCount; + } + + ruleId = 'rule' + ruleCount; + sourceConfig.id = ruleId; + sourceConfig.name += ' Copy'; + ruleOrder.splice(ruleOrder.indexOf(sourceRuleId) + 1, 0, ruleId); + this.domainObject.configuration.ruleOrder = ruleOrder; + this.domainObject.configuration.ruleConfigById[ruleId] = sourceConfig; + this.initRule(ruleId, sourceConfig.name); + this.updateDomainObject(); + this.refreshRules(); + }; + + /** + * Initialze a new rule from a default configuration, or build a {Rule} object + * from it if already exists + * @param {string} ruleId An key to be used to identify this ruleId, or the key + of the rule to be instantiated + * @param {string} ruleName The initial human-readable name of this rule + */ + SummaryWidget.prototype.initRule = function (ruleId, ruleName) { + let ruleConfig; + const styleObj = {}; + + Object.assign(styleObj, DEFAULT_PROPS); + if (!this.domainObject.configuration.ruleConfigById[ruleId]) { + this.domainObject.configuration.ruleConfigById[ruleId] = { + name: ruleName || 'Rule', + label: 'Unnamed Rule', + message: '', + id: ruleId, + icon: ' ', + style: styleObj, + description: ruleId === 'default' ? 'Default appearance for the widget' : 'A new rule', + conditions: [ + { + object: '', + key: '', + operation: '', + values: [] + } + ], + jsCondition: '', + trigger: 'any', + expanded: 'true' + }; + } + + ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId]; + this.rulesById[ruleId] = new Rule( + ruleConfig, + this.domainObject, + this.openmct, + this.conditionManager, + this.widgetDnD, + this.container + ); + this.rulesById[ruleId].on('remove', this.refreshRules, this); + this.rulesById[ruleId].on('duplicate', this.duplicateRule, this); + this.rulesById[ruleId].on('change', this.updateWidget, this); + this.rulesById[ruleId].on('conditionChange', this.executeRules, this); + }; + + /** + * Given two ruleIds, move the source rule after the target rule and update + * the view. + * @param {Object} event An event object representing this drop with draggingId + * and dropTarget fields + */ + SummaryWidget.prototype.reorder = function (event) { + const ruleOrder = this.domainObject.configuration.ruleOrder; + const sourceIndex = ruleOrder.indexOf(event.draggingId); + let targetIndex; + + if (event.draggingId !== event.dropTarget) { + ruleOrder.splice(sourceIndex, 1); + targetIndex = ruleOrder.indexOf(event.dropTarget); + ruleOrder.splice(targetIndex + 1, 0, event.draggingId); + this.domainObject.configuration.ruleOrder = ruleOrder; + this.updateDomainObject(); + } + + this.refreshRules(); + }; + + /** + * Apply a list of css properties to an element + * @param {element} elem The DOM element to which the rules will be applied + * @param {object} style an object representing the style + */ + SummaryWidget.prototype.applyStyle = function (elem, style) { + Object.keys(style).forEach(function (propId) { + elem.style[propId] = style[propId]; + }); + }; + + /** + * Mutate this domain object's configuration with the current local configuration + */ + SummaryWidget.prototype.updateDomainObject = function () { + this.openmct.objects.mutate( + this.domainObject, + 'configuration', + this.domainObject.configuration + ); + }; + + return SummaryWidget; +}); diff --git a/src/plugins/summaryWidget/src/TestDataItem.js b/src/plugins/summaryWidget/src/TestDataItem.js index ae005c46d0..4e9323c947 100644 --- a/src/plugins/summaryWidget/src/TestDataItem.js +++ b/src/plugins/summaryWidget/src/TestDataItem.js @@ -1,200 +1,193 @@ define([ - '../res/testDataItemTemplate.html', - './input/ObjectSelect', - './input/KeySelect', - './eventHelpers', - '../../../utils/template/templateHelpers', - 'EventEmitter' -], function ( - itemTemplate, - ObjectSelect, - KeySelect, - eventHelpers, - templateHelpers, - EventEmitter -) { + '../res/testDataItemTemplate.html', + './input/ObjectSelect', + './input/KeySelect', + './eventHelpers', + '../../../utils/template/templateHelpers', + 'EventEmitter' +], function (itemTemplate, ObjectSelect, KeySelect, eventHelpers, templateHelpers, EventEmitter) { + /** + * An object representing a single mock telemetry value + * @param {object} itemConfig the configuration for this item, consisting of + * object, key, and value fields + * @param {number} index the index of this TestDataItem object in the data + * model of its parent {TestDataManager} o be injected into callbacks + * for removes + * @param {ConditionManager} conditionManager a conditionManager instance + * for populating selects with configuration data + * @constructor + */ + function TestDataItem(itemConfig, index, conditionManager) { + eventHelpers.extend(this); + this.config = itemConfig; + this.index = index; + this.conditionManager = conditionManager; + + this.domElement = templateHelpers.convertTemplateToHTML(itemTemplate)[0]; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['remove', 'duplicate', 'change']; + + this.deleteButton = this.domElement.querySelector('.t-delete'); + this.duplicateButton = this.domElement.querySelector('.t-duplicate'); + + this.selects = {}; + this.valueInputs = []; + + this.remove = this.remove.bind(this); + this.duplicate = this.duplicate.bind(this); + + const self = this; /** - * An object representing a single mock telemetry value - * @param {object} itemConfig the configuration for this item, consisting of - * object, key, and value fields - * @param {number} index the index of this TestDataItem object in the data - * model of its parent {TestDataManager} o be injected into callbacks - * for removes - * @param {ConditionManager} conditionManager a conditionManager instance - * for populating selects with configuration data - * @constructor + * A change event handler for this item's select inputs, which also invokes + * change callbacks registered with this item + * @param {string} value The new value of this select item + * @param {string} property The property of this item to modify + * @private */ - function TestDataItem(itemConfig, index, conditionManager) { - eventHelpers.extend(this); - this.config = itemConfig; - this.index = index; - this.conditionManager = conditionManager; + function onSelectChange(value, property) { + if (property === 'key') { + self.generateValueInput(value); + } - this.domElement = templateHelpers.convertTemplateToHTML(itemTemplate)[0]; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['remove', 'duplicate', 'change']; - - this.deleteButton = this.domElement.querySelector('.t-delete'); - this.duplicateButton = this.domElement.querySelector('.t-duplicate'); - - this.selects = {}; - this.valueInputs = []; - - this.remove = this.remove.bind(this); - this.duplicate = this.duplicate.bind(this); - - const self = this; - - /** - * A change event handler for this item's select inputs, which also invokes - * change callbacks registered with this item - * @param {string} value The new value of this select item - * @param {string} property The property of this item to modify - * @private - */ - function onSelectChange(value, property) { - if (property === 'key') { - self.generateValueInput(value); - } - - self.eventEmitter.emit('change', { - value: value, - property: property, - index: self.index - }); - } - - /** - * An input event handler for this item's value field. Invokes any change - * callbacks associated with this item - * @param {Event} event The input event that initiated this callback - * @private - */ - function onValueInput(event) { - const elem = event.target; - const value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber); - - if (elem.tagName.toUpperCase() === 'INPUT') { - self.eventEmitter.emit('change', { - value: value, - property: 'value', - index: self.index - }); - } - } - - this.listenTo(this.deleteButton, 'click', this.remove); - this.listenTo(this.duplicateButton, 'click', this.duplicate); - - this.selects.object = new ObjectSelect(this.config, this.conditionManager); - this.selects.key = new KeySelect( - this.config, - this.selects.object, - this.conditionManager, - function (value) { - onSelectChange(value, 'key'); - }); - - this.selects.object.on('change', function (value) { - onSelectChange(value, 'object'); - }); - - Object.values(this.selects).forEach(function (select) { - self.domElement.querySelector('.t-configuration').append(select.getDOM()); - }); - this.listenTo(this.domElement, 'input', onValueInput); + self.eventEmitter.emit('change', { + value: value, + property: property, + index: self.index + }); } /** - * Gets the DOM associated with this element's view - * @return {Element} + * An input event handler for this item's value field. Invokes any change + * callbacks associated with this item + * @param {Event} event The input event that initiated this callback + * @private */ - TestDataItem.prototype.getDOM = function (container) { - return this.domElement; - }; + function onValueInput(event) { + const elem = event.target; + const value = isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber; - /** - * Register a callback with this item: supported callbacks are remove, change, - * and duplicate - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - TestDataItem.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } - }; - - /** - * Implement "off" to complete event emitter interface. - */ - TestDataItem.prototype.off = function (event, callback, context) { - this.eventEmitter.off(event, callback, context); - }; - - /** - * Hide the appropriate inputs when this is the only item - */ - TestDataItem.prototype.hideButtons = function () { - this.deleteButton.style.display = 'none'; - }; - - /** - * Remove this item from the configuration. Invokes any registered - * remove callbacks - */ - TestDataItem.prototype.remove = function () { - const self = this; - this.eventEmitter.emit('remove', self.index); - this.stopListening(); - - Object.values(this.selects).forEach(function (select) { - select.destroy(); + if (elem.tagName.toUpperCase() === 'INPUT') { + self.eventEmitter.emit('change', { + value: value, + property: 'value', + index: self.index }); - }; + } + } - /** - * Makes a deep clone of this item's configuration, and invokes any registered - * duplicate callbacks with the cloned configuration as an argument - */ - TestDataItem.prototype.duplicate = function () { - const sourceItem = JSON.parse(JSON.stringify(this.config)); - const self = this; + this.listenTo(this.deleteButton, 'click', this.remove); + this.listenTo(this.duplicateButton, 'click', this.duplicate); - this.eventEmitter.emit('duplicate', { - sourceItem: sourceItem, - index: self.index - }); - }; + this.selects.object = new ObjectSelect(this.config, this.conditionManager); + this.selects.key = new KeySelect( + this.config, + this.selects.object, + this.conditionManager, + function (value) { + onSelectChange(value, 'key'); + } + ); - /** - * When a telemetry property key is selected, create the appropriate value input - * and add it to the view - * @param {string} key The key of currently selected telemetry property - */ - TestDataItem.prototype.generateValueInput = function (key) { - const evaluator = this.conditionManager.getEvaluator(); - const inputArea = this.domElement.querySelector('.t-value-inputs'); - const dataType = this.conditionManager.getTelemetryPropertyType(this.config.object, key); - const inputType = evaluator.getInputTypeById(dataType); + this.selects.object.on('change', function (value) { + onSelectChange(value, 'object'); + }); - inputArea.innerHTML = ''; - if (inputType) { - if (!this.config.value) { - this.config.value = (inputType === 'number' ? 0 : ''); - } + Object.values(this.selects).forEach(function (select) { + self.domElement.querySelector('.t-configuration').append(select.getDOM()); + }); + this.listenTo(this.domElement, 'input', onValueInput); + } - const newInput = document.createElement("input"); - newInput.type = `${inputType}`; - newInput.value = `${this.config.value}`; + /** + * Gets the DOM associated with this element's view + * @return {Element} + */ + TestDataItem.prototype.getDOM = function (container) { + return this.domElement; + }; - this.valueInput = newInput; - inputArea.append(this.valueInput); - } - }; + /** + * Register a callback with this item: supported callbacks are remove, change, + * and duplicate + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + TestDataItem.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } + }; - return TestDataItem; + /** + * Implement "off" to complete event emitter interface. + */ + TestDataItem.prototype.off = function (event, callback, context) { + this.eventEmitter.off(event, callback, context); + }; + + /** + * Hide the appropriate inputs when this is the only item + */ + TestDataItem.prototype.hideButtons = function () { + this.deleteButton.style.display = 'none'; + }; + + /** + * Remove this item from the configuration. Invokes any registered + * remove callbacks + */ + TestDataItem.prototype.remove = function () { + const self = this; + this.eventEmitter.emit('remove', self.index); + this.stopListening(); + + Object.values(this.selects).forEach(function (select) { + select.destroy(); + }); + }; + + /** + * Makes a deep clone of this item's configuration, and invokes any registered + * duplicate callbacks with the cloned configuration as an argument + */ + TestDataItem.prototype.duplicate = function () { + const sourceItem = JSON.parse(JSON.stringify(this.config)); + const self = this; + + this.eventEmitter.emit('duplicate', { + sourceItem: sourceItem, + index: self.index + }); + }; + + /** + * When a telemetry property key is selected, create the appropriate value input + * and add it to the view + * @param {string} key The key of currently selected telemetry property + */ + TestDataItem.prototype.generateValueInput = function (key) { + const evaluator = this.conditionManager.getEvaluator(); + const inputArea = this.domElement.querySelector('.t-value-inputs'); + const dataType = this.conditionManager.getTelemetryPropertyType(this.config.object, key); + const inputType = evaluator.getInputTypeById(dataType); + + inputArea.innerHTML = ''; + if (inputType) { + if (!this.config.value) { + this.config.value = inputType === 'number' ? 0 : ''; + } + + const newInput = document.createElement('input'); + newInput.type = `${inputType}`; + newInput.value = `${this.config.value}`; + + this.valueInput = newInput; + inputArea.append(this.valueInput); + } + }; + + return TestDataItem; }); diff --git a/src/plugins/summaryWidget/src/TestDataManager.js b/src/plugins/summaryWidget/src/TestDataManager.js index 70240453d6..8566117feb 100644 --- a/src/plugins/summaryWidget/src/TestDataManager.js +++ b/src/plugins/summaryWidget/src/TestDataManager.js @@ -1,208 +1,201 @@ define([ - './eventHelpers', - '../res/testDataTemplate.html', - './TestDataItem', - '../../../utils/template/templateHelpers', - 'lodash' -], function ( - eventHelpers, - testDataTemplate, - TestDataItem, - templateHelpers, - _ -) { + './eventHelpers', + '../res/testDataTemplate.html', + './TestDataItem', + '../../../utils/template/templateHelpers', + 'lodash' +], function (eventHelpers, testDataTemplate, TestDataItem, templateHelpers, _) { + /** + * Controls the input and usage of test data in the summary widget. + * @constructor + * @param {Object} domainObject The summary widget domain object + * @param {ConditionManager} conditionManager A conditionManager instance + * @param {MCT} openmct and MCT instance + */ + function TestDataManager(domainObject, conditionManager, openmct) { + eventHelpers.extend(this); + const self = this; + + this.domainObject = domainObject; + this.manager = conditionManager; + this.openmct = openmct; + + this.evaluator = this.manager.getEvaluator(); + this.domElement = templateHelpers.convertTemplateToHTML(testDataTemplate)[0]; + this.config = this.domainObject.configuration.testDataConfig; + this.testCache = {}; + + this.itemArea = this.domElement.querySelector('.t-test-data-config'); + this.addItemButton = this.domElement.querySelector('.add-test-condition'); + this.testDataInput = this.domElement.querySelector('.t-test-data-checkbox'); /** - * Controls the input and usage of test data in the summary widget. - * @constructor - * @param {Object} domainObject The summary widget domain object - * @param {ConditionManager} conditionManager A conditionManager instance - * @param {MCT} openmct and MCT instance + * Toggles whether the associated {ConditionEvaluator} uses the actual + * subscription cache or the test data cache + * @param {Event} event The change event that triggered this callback + * @private */ - function TestDataManager(domainObject, conditionManager, openmct) { - eventHelpers.extend(this); - const self = this; - - this.domainObject = domainObject; - this.manager = conditionManager; - this.openmct = openmct; - - this.evaluator = this.manager.getEvaluator(); - this.domElement = templateHelpers.convertTemplateToHTML(testDataTemplate)[0]; - this.config = this.domainObject.configuration.testDataConfig; - this.testCache = {}; - - this.itemArea = this.domElement.querySelector('.t-test-data-config'); - this.addItemButton = this.domElement.querySelector('.add-test-condition'); - this.testDataInput = this.domElement.querySelector('.t-test-data-checkbox'); - - /** - * Toggles whether the associated {ConditionEvaluator} uses the actual - * subscription cache or the test data cache - * @param {Event} event The change event that triggered this callback - * @private - */ - function toggleTestData(event) { - const elem = event.target; - self.evaluator.useTestData(elem.checked); - self.updateTestCache(); - } - - this.listenTo(this.addItemButton, 'click', function () { - self.initItem(); - }); - this.listenTo(this.testDataInput, 'change', toggleTestData); - - this.evaluator.setTestDataCache(this.testCache); - this.evaluator.useTestData(false); - - this.refreshItems(); + function toggleTestData(event) { + const elem = event.target; + self.evaluator.useTestData(elem.checked); + self.updateTestCache(); } - /** - * Get the DOM element representing this test data manager in the view - */ - TestDataManager.prototype.getDOM = function () { - return this.domElement; + this.listenTo(this.addItemButton, 'click', function () { + self.initItem(); + }); + this.listenTo(this.testDataInput, 'change', toggleTestData); + + this.evaluator.setTestDataCache(this.testCache); + this.evaluator.useTestData(false); + + this.refreshItems(); + } + + /** + * Get the DOM element representing this test data manager in the view + */ + TestDataManager.prototype.getDOM = function () { + return this.domElement; + }; + + /** + * Initialze a new test data item, either from a source configuration, or with + * the default empty configuration + * @param {Object} [config] An object with sourceItem and index fields to instantiate + * this rule from, optional + */ + TestDataManager.prototype.initItem = function (config) { + const sourceIndex = config && config.index; + const defaultItem = { + object: '', + key: '', + value: '' }; + let newItem; - /** - * Initialze a new test data item, either from a source configuration, or with - * the default empty configuration - * @param {Object} [config] An object with sourceItem and index fields to instantiate - * this rule from, optional - */ - TestDataManager.prototype.initItem = function (config) { - const sourceIndex = config && config.index; - const defaultItem = { - object: '', - key: '', - value: '' - }; - let newItem; + newItem = config !== undefined ? config.sourceItem : defaultItem; + if (sourceIndex !== undefined) { + this.config.splice(sourceIndex + 1, 0, newItem); + } else { + this.config.push(newItem); + } - newItem = (config !== undefined ? config.sourceItem : defaultItem); - if (sourceIndex !== undefined) { - this.config.splice(sourceIndex + 1, 0, newItem); - } else { - this.config.push(newItem); - } + this.updateDomainObject(); + this.refreshItems(); + }; - this.updateDomainObject(); - this.refreshItems(); - }; + /** + * Remove an item from this TestDataManager at the given index + * @param {number} removeIndex The index of the item to remove + */ + TestDataManager.prototype.removeItem = function (removeIndex) { + _.remove(this.config, function (item, index) { + return index === removeIndex; + }); + this.updateDomainObject(); + this.refreshItems(); + }; - /** - * Remove an item from this TestDataManager at the given index - * @param {number} removeIndex The index of the item to remove - */ - TestDataManager.prototype.removeItem = function (removeIndex) { - _.remove(this.config, function (item, index) { - return index === removeIndex; - }); - this.updateDomainObject(); - this.refreshItems(); - }; + /** + * Change event handler for the test data items which compose this + * test data generateor + * @param {Object} event An object representing this event, with value, property, + * and index fields + */ + TestDataManager.prototype.onItemChange = function (event) { + this.config[event.index][event.property] = event.value; + this.updateDomainObject(); + this.updateTestCache(); + }; - /** - * Change event handler for the test data items which compose this - * test data generateor - * @param {Object} event An object representing this event, with value, property, - * and index fields - */ - TestDataManager.prototype.onItemChange = function (event) { - this.config[event.index][event.property] = event.value; - this.updateDomainObject(); - this.updateTestCache(); - }; + /** + * Builds the test cache from the current item configuration, and passes + * the new test cache to the associated {ConditionEvaluator} instance + */ + TestDataManager.prototype.updateTestCache = function () { + this.generateTestCache(); + this.evaluator.setTestDataCache(this.testCache); + this.manager.triggerTelemetryCallback(); + }; - /** - * Builds the test cache from the current item configuration, and passes - * the new test cache to the associated {ConditionEvaluator} instance - */ - TestDataManager.prototype.updateTestCache = function () { - this.generateTestCache(); - this.evaluator.setTestDataCache(this.testCache); - this.manager.triggerTelemetryCallback(); - }; + /** + * Intantiate {TestDataItem} objects from the current configuration, and + * update the view accordingly + */ + TestDataManager.prototype.refreshItems = function () { + const self = this; + if (this.items) { + this.items.forEach(function (item) { + this.stopListening(item); + }, this); + } - /** - * Intantiate {TestDataItem} objects from the current configuration, and - * update the view accordingly - */ - TestDataManager.prototype.refreshItems = function () { - const self = this; - if (this.items) { - this.items.forEach(function (item) { - this.stopListening(item); - }, this); - } + self.items = []; - self.items = []; + this.domElement.querySelectorAll('.t-test-data-item').forEach((item) => { + item.remove(); + }); - this.domElement.querySelectorAll('.t-test-data-item').forEach(item => { - item.remove(); - }); + this.config.forEach(function (item, index) { + const newItem = new TestDataItem(item, index, self.manager); + self.listenTo(newItem, 'remove', self.removeItem, self); + self.listenTo(newItem, 'duplicate', self.initItem, self); + self.listenTo(newItem, 'change', self.onItemChange, self); + self.items.push(newItem); + }); - this.config.forEach(function (item, index) { - const newItem = new TestDataItem(item, index, self.manager); - self.listenTo(newItem, 'remove', self.removeItem, self); - self.listenTo(newItem, 'duplicate', self.initItem, self); - self.listenTo(newItem, 'change', self.onItemChange, self); - self.items.push(newItem); - }); + self.items.forEach(function (item) { + self.itemArea.prepend(item.getDOM()); + }); - self.items.forEach(function (item) { - self.itemArea.prepend(item.getDOM()); - }); + if (self.items.length === 1) { + self.items[0].hideButtons(); + } - if (self.items.length === 1) { - self.items[0].hideButtons(); - } + this.updateTestCache(); + }; - this.updateTestCache(); - }; + /** + * Builds a test data cache in the format of a telemetry subscription cache + * as expected by a {ConditionEvaluator} + */ + TestDataManager.prototype.generateTestCache = function () { + let testCache = this.testCache; + const manager = this.manager; + const compositionObjs = manager.getComposition(); + let metadata; - /** - * Builds a test data cache in the format of a telemetry subscription cache - * as expected by a {ConditionEvaluator} - */ - TestDataManager.prototype.generateTestCache = function () { - let testCache = this.testCache; - const manager = this.manager; - const compositionObjs = manager.getComposition(); - let metadata; + testCache = {}; + Object.keys(compositionObjs).forEach(function (id) { + testCache[id] = {}; + metadata = manager.getTelemetryMetadata(id); + Object.keys(metadata).forEach(function (key) { + testCache[id][key] = ''; + }); + }); + this.config.forEach(function (item) { + if (testCache[item.object]) { + testCache[item.object][item.key] = item.value; + } + }); - testCache = {}; - Object.keys(compositionObjs).forEach(function (id) { - testCache[id] = {}; - metadata = manager.getTelemetryMetadata(id); - Object.keys(metadata).forEach(function (key) { - testCache[id][key] = ''; - }); - }); - this.config.forEach(function (item) { - if (testCache[item.object]) { - testCache[item.object][item.key] = item.value; - } - }); + this.testCache = testCache; + }; - this.testCache = testCache; - }; + /** + * Update the domain object configuration associated with this test data manager + */ + TestDataManager.prototype.updateDomainObject = function () { + this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config); + }; - /** - * Update the domain object configuration associated with this test data manager - */ - TestDataManager.prototype.updateDomainObject = function () { - this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config); - }; + TestDataManager.prototype.destroy = function () { + this.stopListening(); + this.items.forEach(function (item) { + item.remove(); + }); + }; - TestDataManager.prototype.destroy = function () { - this.stopListening(); - this.items.forEach(function (item) { - item.remove(); - }); - }; - - return TestDataManager; + return TestDataManager; }); diff --git a/src/plugins/summaryWidget/src/WidgetDnD.js b/src/plugins/summaryWidget/src/WidgetDnD.js index 90cd3b6971..15c79ed66d 100644 --- a/src/plugins/summaryWidget/src/WidgetDnD.js +++ b/src/plugins/summaryWidget/src/WidgetDnD.js @@ -1,170 +1,165 @@ define([ - '../res/ruleImageTemplate.html', - 'EventEmitter', - '../../../utils/template/templateHelpers' -], function ( - ruleImageTemplate, - EventEmitter, - templateHelpers -) { + '../res/ruleImageTemplate.html', + 'EventEmitter', + '../../../utils/template/templateHelpers' +], function (ruleImageTemplate, EventEmitter, templateHelpers) { + /** + * Manages the Sortable List interface for reordering rules by drag and drop + * @param {Element} container The DOM element that contains this Summary Widget's view + * @param {string[]} ruleOrder An array of rule IDs representing the current rule order + * @param {Object} rulesById An object mapping rule IDs to rule configurations + */ + function WidgetDnD(container, ruleOrder, rulesById) { + this.container = container; + this.ruleOrder = ruleOrder; + this.rulesById = rulesById; - /** - * Manages the Sortable List interface for reordering rules by drag and drop - * @param {Element} container The DOM element that contains this Summary Widget's view - * @param {string[]} ruleOrder An array of rule IDs representing the current rule order - * @param {Object} rulesById An object mapping rule IDs to rule configurations - */ - function WidgetDnD(container, ruleOrder, rulesById) { - this.container = container; - this.ruleOrder = ruleOrder; - this.rulesById = rulesById; + this.imageContainer = templateHelpers.convertTemplateToHTML(ruleImageTemplate)[0]; + this.image = this.imageContainer.querySelector('.t-drag-rule-image'); + this.draggingId = ''; + this.draggingRulePrevious = ''; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['drop']; - this.imageContainer = templateHelpers.convertTemplateToHTML(ruleImageTemplate)[0]; - this.image = this.imageContainer.querySelector('.t-drag-rule-image'); - this.draggingId = ''; - this.draggingRulePrevious = ''; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['drop']; + this.drag = this.drag.bind(this); + this.drop = this.drop.bind(this); - this.drag = this.drag.bind(this); - this.drop = this.drop.bind(this); + this.container.addEventListener('mousemove', this.drag); + document.addEventListener('mouseup', this.drop); + this.container.parentNode.insertBefore(this.imageContainer, this.container); + this.imageContainer.style.display = 'none'; + } - this.container.addEventListener('mousemove', this.drag); - document.addEventListener('mouseup', this.drop); - this.container.parentNode.insertBefore(this.imageContainer, this.container); - this.imageContainer.style.display = 'none'; + /** + * Remove event listeners registered to elements external to the widget + */ + WidgetDnD.prototype.destroy = function () { + this.container.removeEventListener('mousemove', this.drag); + document.removeEventListener('mouseup', this.drop); + }; + + /** + * Register a callback with this WidgetDnD: supported callback is drop + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + WidgetDnD.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); } + }; - /** - * Remove event listeners registered to elements external to the widget - */ - WidgetDnD.prototype.destroy = function () { - this.container.removeEventListener('mousemove', this.drag); - document.removeEventListener('mouseup', this.drop); - }; + /** + * Sets the image for the dragged element to the given DOM element + * @param {Element} image The HTML element to set as the drap image + */ + WidgetDnD.prototype.setDragImage = function (image) { + this.image.html(image); + }; - /** - * Register a callback with this WidgetDnD: supported callback is drop - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - WidgetDnD.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } - }; - - /** - * Sets the image for the dragged element to the given DOM element - * @param {Element} image The HTML element to set as the drap image - */ - WidgetDnD.prototype.setDragImage = function (image) { - this.image.html(image); - }; - - /** + /** * Calculate where this rule has been dragged relative to the other rules * @param {Event} event The mousemove or mouseup event that triggered this event handler * @return {string} The ID of the rule whose drag indicator should be displayed */ - WidgetDnD.prototype.getDropLocation = function (event) { - const ruleOrder = this.ruleOrder; - const rulesById = this.rulesById; - const draggingId = this.draggingId; - let offset; - let y; - let height; - const dropY = event.pageY; - let target = ''; + WidgetDnD.prototype.getDropLocation = function (event) { + const ruleOrder = this.ruleOrder; + const rulesById = this.rulesById; + const draggingId = this.draggingId; + let offset; + let y; + let height; + const dropY = event.pageY; + let target = ''; - ruleOrder.forEach(function (ruleId, index) { - const ruleDOM = rulesById[ruleId].getDOM(); - offset = window.innerWidth - (ruleDOM.offsetLeft + ruleDOM.offsetWidth); - y = offset.top; - height = offset.height; - if (index === 0) { - if (dropY < y + 7 * height / 3) { - target = ruleId; - } - } else if (index === ruleOrder.length - 1 && ruleId !== draggingId) { - if (y + height / 3 < dropY) { - target = ruleId; - } - } else { - if (y + height / 3 < dropY && dropY < y + 7 * height / 3) { - target = ruleId; - } - } - }); + ruleOrder.forEach(function (ruleId, index) { + const ruleDOM = rulesById[ruleId].getDOM(); + offset = window.innerWidth - (ruleDOM.offsetLeft + ruleDOM.offsetWidth); + y = offset.top; + height = offset.height; + if (index === 0) { + if (dropY < y + (7 * height) / 3) { + target = ruleId; + } + } else if (index === ruleOrder.length - 1 && ruleId !== draggingId) { + if (y + height / 3 < dropY) { + target = ruleId; + } + } else { + if (y + height / 3 < dropY && dropY < y + (7 * height) / 3) { + target = ruleId; + } + } + }); - return target; - }; + return target; + }; - /** - * Called by a {Rule} instance that initiates a drag gesture - * @param {string} ruleId The identifier of the rule which is being dragged - */ - WidgetDnD.prototype.dragStart = function (ruleId) { - const ruleOrder = this.ruleOrder; - this.draggingId = ruleId; - this.draggingRulePrevious = ruleOrder[ruleOrder.indexOf(ruleId) - 1]; + /** + * Called by a {Rule} instance that initiates a drag gesture + * @param {string} ruleId The identifier of the rule which is being dragged + */ + WidgetDnD.prototype.dragStart = function (ruleId) { + const ruleOrder = this.ruleOrder; + this.draggingId = ruleId; + this.draggingRulePrevious = ruleOrder[ruleOrder.indexOf(ruleId) - 1]; + this.rulesById[this.draggingRulePrevious].showDragIndicator(); + this.imageContainer.show(); + this.imageContainer.offset({ + top: event.pageY - this.image.height() / 2, + left: event.pageX - this.image.querySelector('.t-grippy').style.width + }); + }; + + /** + * An event handler for a mousemove event, once a rule has begun a drag gesture + * @param {Event} event The mousemove event that triggered this callback + */ + WidgetDnD.prototype.drag = function (event) { + let dragTarget; + if (this.draggingId && this.draggingId !== '') { + event.preventDefault(); + dragTarget = this.getDropLocation(event); + this.imageContainer.offset({ + top: event.pageY - this.image.height() / 2, + left: event.pageX - this.image.querySelector('.t-grippy').style.width + }); + if (this.rulesById[dragTarget]) { + this.rulesById[dragTarget].showDragIndicator(); + } else { this.rulesById[this.draggingRulePrevious].showDragIndicator(); - this.imageContainer.show(); - this.imageContainer.offset({ - top: event.pageY - this.image.height() / 2, - left: event.pageX - this.image.querySelector('.t-grippy').style.width - }); - }; + } + } + }; - /** - * An event handler for a mousemove event, once a rule has begun a drag gesture - * @param {Event} event The mousemove event that triggered this callback - */ - WidgetDnD.prototype.drag = function (event) { - let dragTarget; - if (this.draggingId && this.draggingId !== '') { - event.preventDefault(); - dragTarget = this.getDropLocation(event); - this.imageContainer.offset({ - top: event.pageY - this.image.height() / 2, - left: event.pageX - this.image.querySelector('.t-grippy').style.width - }); - if (this.rulesById[dragTarget]) { - this.rulesById[dragTarget].showDragIndicator(); - } else { - this.rulesById[this.draggingRulePrevious].showDragIndicator(); - } - } - }; + /** + * Handles the mouseup event that corresponds to the user dropping the rule + * in its final location. Invokes any registered drop callbacks with the dragged + * rule's ID and the ID of the target rule that the dragged rule should be + * inserted after + * @param {Event} event The mouseup event that triggered this callback + */ + WidgetDnD.prototype.drop = function (event) { + let dropTarget = this.getDropLocation(event); + const draggingId = this.draggingId; - /** - * Handles the mouseup event that corresponds to the user dropping the rule - * in its final location. Invokes any registered drop callbacks with the dragged - * rule's ID and the ID of the target rule that the dragged rule should be - * inserted after - * @param {Event} event The mouseup event that triggered this callback - */ - WidgetDnD.prototype.drop = function (event) { - let dropTarget = this.getDropLocation(event); - const draggingId = this.draggingId; + if (this.draggingId && this.draggingId !== '') { + if (!this.rulesById[dropTarget]) { + dropTarget = this.draggingId; + } - if (this.draggingId && this.draggingId !== '') { - if (!this.rulesById[dropTarget]) { - dropTarget = this.draggingId; - } + this.eventEmitter.emit('drop', { + draggingId: draggingId, + dropTarget: dropTarget + }); + this.draggingId = ''; + this.draggingRulePrevious = ''; + this.imageContainer.hide(); + } + }; - this.eventEmitter.emit('drop', { - draggingId: draggingId, - dropTarget: dropTarget - }); - this.draggingId = ''; - this.draggingRulePrevious = ''; - this.imageContainer.hide(); - } - }; - - return WidgetDnD; + return WidgetDnD; }); diff --git a/src/plugins/summaryWidget/src/eventHelpers.js b/src/plugins/summaryWidget/src/eventHelpers.js index 337db1bc0c..367072b9f6 100644 --- a/src/plugins/summaryWidget/src/eventHelpers.js +++ b/src/plugins/summaryWidget/src/eventHelpers.js @@ -21,78 +21,79 @@ *****************************************************************************/ define([], function () { - const helperFunctions = { - listenTo: function (object, event, callback, context) { - if (!this._listeningTo) { - this._listeningTo = []; - } + const helperFunctions = { + listenTo: function (object, event, callback, context) { + if (!this._listeningTo) { + this._listeningTo = []; + } - const listener = { - object: object, - event: event, - callback: callback, - context: context, - _cb: context ? callback.bind(context) : callback - }; - if (object.$watch && event.indexOf('change:') === 0) { - const scopePath = event.replace('change:', ''); - listener.unlisten = object.$watch(scopePath, listener._cb, true); - } else if (object.$on) { - listener.unlisten = object.$on(event, listener._cb); - } else if (object.addEventListener) { - object.addEventListener(event, listener._cb); - } else { - object.on(event, listener._cb); - } + const listener = { + object: object, + event: event, + callback: callback, + context: context, + _cb: context ? callback.bind(context) : callback + }; + if (object.$watch && event.indexOf('change:') === 0) { + const scopePath = event.replace('change:', ''); + listener.unlisten = object.$watch(scopePath, listener._cb, true); + } else if (object.$on) { + listener.unlisten = object.$on(event, listener._cb); + } else if (object.addEventListener) { + object.addEventListener(event, listener._cb); + } else { + object.on(event, listener._cb); + } - this._listeningTo.push(listener); - }, + this._listeningTo.push(listener); + }, - stopListening: function (object, event, callback, context) { - if (!this._listeningTo) { - this._listeningTo = []; - } + stopListening: function (object, event, callback, context) { + if (!this._listeningTo) { + this._listeningTo = []; + } - this._listeningTo.filter(function (listener) { - if (object && object !== listener.object) { - return false; - } + this._listeningTo + .filter(function (listener) { + if (object && object !== listener.object) { + return false; + } - if (event && event !== listener.event) { - return false; - } + if (event && event !== listener.event) { + return false; + } - if (callback && callback !== listener.callback) { - return false; - } + if (callback && callback !== listener.callback) { + return false; + } - if (context && context !== listener.context) { - return false; - } + if (context && context !== listener.context) { + return false; + } - return true; - }) - .map(function (listener) { - if (listener.unlisten) { - listener.unlisten(); - } else if (listener.object.removeEventListener) { - listener.object.removeEventListener(listener.event, listener._cb); - } else { - listener.object.off(listener.event, listener._cb); - } + return true; + }) + .map(function (listener) { + if (listener.unlisten) { + listener.unlisten(); + } else if (listener.object.removeEventListener) { + listener.object.removeEventListener(listener.event, listener._cb); + } else { + listener.object.off(listener.event, listener._cb); + } - return listener; - }) - .forEach(function (listener) { - this._listeningTo.splice(this._listeningTo.indexOf(listener), 1); - }, this); - }, + return listener; + }) + .forEach(function (listener) { + this._listeningTo.splice(this._listeningTo.indexOf(listener), 1); + }, this); + }, - extend: function (object) { - object.listenTo = helperFunctions.listenTo; - object.stopListening = helperFunctions.stopListening; - } - }; + extend: function (object) { + object.listenTo = helperFunctions.listenTo; + object.stopListening = helperFunctions.stopListening; + } + }; - return helperFunctions; + return helperFunctions; }); diff --git a/src/plugins/summaryWidget/src/input/ColorPalette.js b/src/plugins/summaryWidget/src/input/ColorPalette.js index 2319f98304..82ed0b3981 100644 --- a/src/plugins/summaryWidget/src/input/ColorPalette.js +++ b/src/plugins/summaryWidget/src/input/ColorPalette.js @@ -1,62 +1,128 @@ -define([ - './Palette' -], -function ( - Palette -) { +define(['./Palette'], function (Palette) { + //The colors that will be used to instantiate this palette if none are provided + const DEFAULT_COLORS = [ + '#000000', + '#434343', + '#666666', + '#999999', + '#b7b7b7', + '#cccccc', + '#d9d9d9', + '#efefef', + '#f3f3f3', + '#ffffff', + '#980000', + '#ff0000', + '#ff9900', + '#ffff00', + '#00ff00', + '#00ffff', + '#4a86e8', + '#0000ff', + '#9900ff', + '#ff00ff', + '#e6b8af', + '#f4cccc', + '#fce5cd', + '#fff2cc', + '#d9ead3', + '#d0e0e3', + '#c9daf8', + '#cfe2f3', + '#d9d2e9', + '#ead1dc', + '#dd7e6b', + '#dd7e6b', + '#f9cb9c', + '#ffe599', + '#b6d7a8', + '#a2c4c9', + '#a4c2f4', + '#9fc5e8', + '#b4a7d6', + '#d5a6bd', + '#cc4125', + '#e06666', + '#f6b26b', + '#ffd966', + '#93c47d', + '#76a5af', + '#6d9eeb', + '#6fa8dc', + '#8e7cc3', + '#c27ba0', + '#a61c00', + '#cc0000', + '#e69138', + '#f1c232', + '#6aa84f', + '#45818e', + '#3c78d8', + '#3d85c6', + '#674ea7', + '#a64d79', + '#85200c', + '#990000', + '#b45f06', + '#bf9000', + '#38761d', + '#134f5c', + '#1155cc', + '#0b5394', + '#351c75', + '#741b47', + '#5b0f00', + '#660000', + '#783f04', + '#7f6000', + '#274e13', + '#0c343d', + '#1c4587', + '#073763', + '#20124d', + '#4c1130' + ]; - //The colors that will be used to instantiate this palette if none are provided - const DEFAULT_COLORS = [ - '#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#d9d9d9', '#efefef', '#f3f3f3', '#ffffff', - '#980000', '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#0000ff', '#9900ff', '#ff00ff', - '#e6b8af', '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#cfe2f3', '#d9d2e9', '#ead1dc', - '#dd7e6b', '#dd7e6b', '#f9cb9c', '#ffe599', '#b6d7a8', '#a2c4c9', '#a4c2f4', '#9fc5e8', '#b4a7d6', '#d5a6bd', - '#cc4125', '#e06666', '#f6b26b', '#ffd966', '#93c47d', '#76a5af', '#6d9eeb', '#6fa8dc', '#8e7cc3', '#c27ba0', - '#a61c00', '#cc0000', '#e69138', '#f1c232', '#6aa84f', '#45818e', '#3c78d8', '#3d85c6', '#674ea7', '#a64d79', - '#85200c', '#990000', '#b45f06', '#bf9000', '#38761d', '#134f5c', '#1155cc', '#0b5394', '#351c75', '#741b47', - '#5b0f00', '#660000', '#783f04', '#7f6000', '#274e13', '#0c343d', '#1c4587', '#073763', '#20124d', '#4c1130' - ]; + /** + * Instantiates a new Open MCT Color Palette input + * @constructor + * @param {string} cssClass The class name of the icon which should be applied + * to this palette + * @param {Element} container The view that contains this palette + * @param {string[]} colors (optional) A list of colors that should be used to instantiate this palette + */ + function ColorPalette(cssClass, container, colors) { + this.colors = colors || DEFAULT_COLORS; + this.palette = new Palette(cssClass, container, this.colors); + + this.palette.setNullOption('rgba(0,0,0,0)'); + + const domElement = this.palette.getDOM(); + const self = this; + + domElement.querySelector('.c-button--menu').classList.add('c-button--swatched'); + domElement.querySelector('.t-swatch').classList.add('color-swatch'); + domElement.querySelector('.c-palette').classList.add('c-palette--color'); + + domElement.querySelectorAll('.c-palette__item').forEach((item) => { + // eslint-disable-next-line no-invalid-this + item.style.backgroundColor = item.dataset.item; + }); /** - * Instantiates a new Open MCT Color Palette input - * @constructor - * @param {string} cssClass The class name of the icon which should be applied - * to this palette - * @param {Element} container The view that contains this palette - * @param {string[]} colors (optional) A list of colors that should be used to instantiate this palette + * Update this palette's current selection indicator with the style + * of the currently selected item + * @private */ - function ColorPalette(cssClass, container, colors) { - this.colors = colors || DEFAULT_COLORS; - this.palette = new Palette(cssClass, container, this.colors); - - this.palette.setNullOption('rgba(0,0,0,0)'); - - const domElement = this.palette.getDOM(); - const self = this; - - domElement.querySelector('.c-button--menu').classList.add('c-button--swatched'); - domElement.querySelector('.t-swatch').classList.add('color-swatch'); - domElement.querySelector('.c-palette').classList.add('c-palette--color'); - - domElement.querySelectorAll('.c-palette__item').forEach(item => { - // eslint-disable-next-line no-invalid-this - item.style.backgroundColor = item.dataset.item; - }); - - /** - * Update this palette's current selection indicator with the style - * of the currently selected item - * @private - */ - function updateSwatch() { - const color = self.palette.getCurrent(); - domElement.querySelector('.color-swatch').style.backgroundColor = color; - } - - this.palette.on('change', updateSwatch); - - return this.palette; + function updateSwatch() { + const color = self.palette.getCurrent(); + domElement.querySelector('.color-swatch').style.backgroundColor = color; } - return ColorPalette; + this.palette.on('change', updateSwatch); + + return this.palette; + } + + return ColorPalette; }); diff --git a/src/plugins/summaryWidget/src/input/IconPalette.js b/src/plugins/summaryWidget/src/input/IconPalette.js index 557cc4d958..4c2cf5e6ff 100644 --- a/src/plugins/summaryWidget/src/input/IconPalette.js +++ b/src/plugins/summaryWidget/src/input/IconPalette.js @@ -1,81 +1,77 @@ -define([ - './Palette' -], function ( - Palette -) { - //The icons that will be used to instantiate this palette if none are provided - const DEFAULT_ICONS = [ - 'icon-alert-rect', - 'icon-alert-triangle', - 'icon-arrow-down', - 'icon-arrow-left', - 'icon-arrow-right', - 'icon-arrow-double-up', - 'icon-arrow-tall-up', - 'icon-arrow-tall-down', - 'icon-arrow-double-down', - 'icon-arrow-up', - 'icon-asterisk', - 'icon-bell', - 'icon-check', - 'icon-eye-open', - 'icon-gear', - 'icon-hourglass', - 'icon-info', - 'icon-link', - 'icon-lock', - 'icon-people', - 'icon-person', - 'icon-plus', - 'icon-trash', - 'icon-x' - ]; +define(['./Palette'], function (Palette) { + //The icons that will be used to instantiate this palette if none are provided + const DEFAULT_ICONS = [ + 'icon-alert-rect', + 'icon-alert-triangle', + 'icon-arrow-down', + 'icon-arrow-left', + 'icon-arrow-right', + 'icon-arrow-double-up', + 'icon-arrow-tall-up', + 'icon-arrow-tall-down', + 'icon-arrow-double-down', + 'icon-arrow-up', + 'icon-asterisk', + 'icon-bell', + 'icon-check', + 'icon-eye-open', + 'icon-gear', + 'icon-hourglass', + 'icon-info', + 'icon-link', + 'icon-lock', + 'icon-people', + 'icon-person', + 'icon-plus', + 'icon-trash', + 'icon-x' + ]; + + /** + * Instantiates a new Open MCT Icon Palette input + * @constructor + * @param {string} cssClass The class name of the icon which should be applied + * to this palette + * @param {Element} container The view that contains this palette + * @param {string[]} icons (optional) A list of icons that should be used to instantiate this palette + */ + function IconPalette(cssClass, container, icons) { + this.icons = icons || DEFAULT_ICONS; + this.palette = new Palette(cssClass, container, this.icons); + + this.palette.setNullOption(''); + this.oldIcon = this.palette.current || ''; + + const domElement = this.palette.getDOM(); + const self = this; + + domElement.querySelector('.c-button--menu').classList.add('c-button--swatched'); + domElement.querySelector('.t-swatch').classList.add('icon-swatch'); + domElement.querySelector('.c-palette').classList.add('c-palette--icon'); + + domElement.querySelectorAll('.c-palette-item').forEach((item) => { + // eslint-disable-next-line no-invalid-this + item.classList.add(item.dataset.item); + }); /** - * Instantiates a new Open MCT Icon Palette input - * @constructor - * @param {string} cssClass The class name of the icon which should be applied - * to this palette - * @param {Element} container The view that contains this palette - * @param {string[]} icons (optional) A list of icons that should be used to instantiate this palette + * Update this palette's current selection indicator with the style + * of the currently selected item + * @private */ - function IconPalette(cssClass, container, icons) { - this.icons = icons || DEFAULT_ICONS; - this.palette = new Palette(cssClass, container, this.icons); + function updateSwatch() { + if (self.oldIcon) { + domElement.querySelector('.icon-swatch').classList.remove(self.oldIcon); + } - this.palette.setNullOption(''); - this.oldIcon = this.palette.current || ''; - - const domElement = this.palette.getDOM(); - const self = this; - - domElement.querySelector('.c-button--menu').classList.add('c-button--swatched'); - domElement.querySelector('.t-swatch').classList.add('icon-swatch'); - domElement.querySelector('.c-palette').classList.add('c-palette--icon'); - - domElement.querySelectorAll('.c-palette-item').forEach(item => { - // eslint-disable-next-line no-invalid-this - item.classList.add(item.dataset.item); - }); - - /** - * Update this palette's current selection indicator with the style - * of the currently selected item - * @private - */ - function updateSwatch() { - if (self.oldIcon) { - domElement.querySelector('.icon-swatch').classList.remove(self.oldIcon); - } - - domElement.querySelector('.icon-swatch').classList.add(self.palette.getCurrent()); - self.oldIcon = self.palette.getCurrent(); - } - - this.palette.on('change', updateSwatch); - - return this.palette; + domElement.querySelector('.icon-swatch').classList.add(self.palette.getCurrent()); + self.oldIcon = self.palette.getCurrent(); } - return IconPalette; + this.palette.on('change', updateSwatch); + + return this.palette; + } + + return IconPalette; }); diff --git a/src/plugins/summaryWidget/src/input/KeySelect.js b/src/plugins/summaryWidget/src/input/KeySelect.js index 7be2b8dbb3..42671027cc 100644 --- a/src/plugins/summaryWidget/src/input/KeySelect.js +++ b/src/plugins/summaryWidget/src/input/KeySelect.js @@ -1,99 +1,95 @@ -define([ - './Select' -], function ( - Select -) { +define(['./Select'], function (Select) { + /** + * Create a {Select} element whose composition is dynamically updated with + * the telemetry fields of a particular domain object + * @constructor + * @param {Object} config The current state of this select. Must have object + * and key fields + * @param {ObjectSelect} objectSelect The linked ObjectSelect instance to which + * this KeySelect should listen to for change + * events + * @param {ConditionManager} manager A ConditionManager instance from which + * to receive telemetry metadata + * @param {function} changeCallback A change event callback to register with this + * select on initialization + */ + const NULLVALUE = '- Select Field -'; - /** - * Create a {Select} element whose composition is dynamically updated with - * the telemetry fields of a particular domain object - * @constructor - * @param {Object} config The current state of this select. Must have object - * and key fields - * @param {ObjectSelect} objectSelect The linked ObjectSelect instance to which - * this KeySelect should listen to for change - * events - * @param {ConditionManager} manager A ConditionManager instance from which - * to receive telemetry metadata - * @param {function} changeCallback A change event callback to register with this - * select on initialization - */ - const NULLVALUE = '- Select Field -'; + function KeySelect(config, objectSelect, manager, changeCallback) { + const self = this; - function KeySelect(config, objectSelect, manager, changeCallback) { - const self = this; + this.config = config; + this.objectSelect = objectSelect; + this.manager = manager; - this.config = config; - this.objectSelect = objectSelect; - this.manager = manager; - - this.select = new Select(); - this.select.hide(); - this.select.addOption('', NULLVALUE); - if (changeCallback) { - this.select.on('change', changeCallback); - } - - /** - * Change event handler for the {ObjectSelect} to which this KeySelect instance - * is linked. Loads the new object's metadata and updates its select element's - * composition. - * @param {Object} key The key identifying the newly selected domain object - * @private - */ - function onObjectChange(key) { - const selected = self.manager.metadataLoadCompleted() ? self.select.getSelected() : self.config.key; - self.telemetryMetadata = self.manager.getTelemetryMetadata(key) || {}; - self.generateOptions(); - self.select.setSelected(selected); - } - - /** - * Event handler for the intial metadata load event from the associated - * ConditionManager. Retreives metadata from the manager and populates - * the select element. - * @private - */ - function onMetadataLoad() { - if (self.manager.getTelemetryMetadata(self.config.object)) { - self.telemetryMetadata = self.manager.getTelemetryMetadata(self.config.object); - self.generateOptions(); - } - - self.select.setSelected(self.config.key); - } - - if (self.manager.metadataLoadCompleted()) { - onMetadataLoad(); - } - - this.objectSelect.on('change', onObjectChange, this); - this.manager.on('metadata', onMetadataLoad); - - return this.select; + this.select = new Select(); + this.select.hide(); + this.select.addOption('', NULLVALUE); + if (changeCallback) { + this.select.on('change', changeCallback); } /** - * Populate this select with options based on its current composition + * Change event handler for the {ObjectSelect} to which this KeySelect instance + * is linked. Loads the new object's metadata and updates its select element's + * composition. + * @param {Object} key The key identifying the newly selected domain object + * @private */ - KeySelect.prototype.generateOptions = function () { - const items = Object.entries(this.telemetryMetadata).map(function (metaDatum) { - return [metaDatum[0], metaDatum[1].name]; - }); - items.splice(0, 0, ['', NULLVALUE]); - this.select.setOptions(items); + function onObjectChange(key) { + const selected = self.manager.metadataLoadCompleted() + ? self.select.getSelected() + : self.config.key; + self.telemetryMetadata = self.manager.getTelemetryMetadata(key) || {}; + self.generateOptions(); + self.select.setSelected(selected); + } - if (this.select.options.length < 2) { - this.select.hide(); - } else if (this.select.options.length > 1) { - this.select.show(); - } - }; + /** + * Event handler for the intial metadata load event from the associated + * ConditionManager. Retreives metadata from the manager and populates + * the select element. + * @private + */ + function onMetadataLoad() { + if (self.manager.getTelemetryMetadata(self.config.object)) { + self.telemetryMetadata = self.manager.getTelemetryMetadata(self.config.object); + self.generateOptions(); + } - KeySelect.prototype.destroy = function () { - this.objectSelect.destroy(); - }; + self.select.setSelected(self.config.key); + } - return KeySelect; + if (self.manager.metadataLoadCompleted()) { + onMetadataLoad(); + } + this.objectSelect.on('change', onObjectChange, this); + this.manager.on('metadata', onMetadataLoad); + + return this.select; + } + + /** + * Populate this select with options based on its current composition + */ + KeySelect.prototype.generateOptions = function () { + const items = Object.entries(this.telemetryMetadata).map(function (metaDatum) { + return [metaDatum[0], metaDatum[1].name]; + }); + items.splice(0, 0, ['', NULLVALUE]); + this.select.setOptions(items); + + if (this.select.options.length < 2) { + this.select.hide(); + } else if (this.select.options.length > 1) { + this.select.show(); + } + }; + + KeySelect.prototype.destroy = function () { + this.objectSelect.destroy(); + }; + + return KeySelect; }); diff --git a/src/plugins/summaryWidget/src/input/ObjectSelect.js b/src/plugins/summaryWidget/src/input/ObjectSelect.js index 4b2a8a20be..f3bac8b377 100644 --- a/src/plugins/summaryWidget/src/input/ObjectSelect.js +++ b/src/plugins/summaryWidget/src/input/ObjectSelect.js @@ -1,93 +1,86 @@ -define([ - './Select', - 'objectUtils' -], function ( - Select, - objectUtils -) { +define(['./Select', 'objectUtils'], function (Select, objectUtils) { + /** + * Create a {Select} element whose composition is dynamically updated with + * the current composition of the Summary Widget + * @constructor + * @param {Object} config The current state of this select. Must have an + * object field + * @param {ConditionManager} manager A ConditionManager instance from which + * to receive the current composition status + * @param {string[][]} baseOptions A set of [value, label] keyword pairs to + * display regardless of the composition state + */ + function ObjectSelect(config, manager, baseOptions) { + const self = this; + + this.config = config; + this.manager = manager; + + this.select = new Select(); + this.baseOptions = [['', '- Select Telemetry -']]; + if (baseOptions) { + this.baseOptions = this.baseOptions.concat(baseOptions); + } + + this.baseOptions.forEach(function (option) { + self.select.addOption(option[0], option[1]); + }); + + this.compositionObjs = this.manager.getComposition(); + self.generateOptions(); /** - * Create a {Select} element whose composition is dynamically updated with - * the current composition of the Summary Widget - * @constructor - * @param {Object} config The current state of this select. Must have an - * object field - * @param {ConditionManager} manager A ConditionManager instance from which - * to receive the current composition status - * @param {string[][]} baseOptions A set of [value, label] keyword pairs to - * display regardless of the composition state + * Add a new composition object to this select when a composition added + * is detected on the Summary Widget + * @param {Object} obj The newly added domain object + * @private */ - function ObjectSelect(config, manager, baseOptions) { - const self = this; - - this.config = config; - this.manager = manager; - - this.select = new Select(); - this.baseOptions = [['', '- Select Telemetry -']]; - if (baseOptions) { - this.baseOptions = this.baseOptions.concat(baseOptions); - } - - this.baseOptions.forEach(function (option) { - self.select.addOption(option[0], option[1]); - }); - - this.compositionObjs = this.manager.getComposition(); - self.generateOptions(); - - /** - * Add a new composition object to this select when a composition added - * is detected on the Summary Widget - * @param {Object} obj The newly added domain object - * @private - */ - function onCompositionAdd(obj) { - self.select.addOption(objectUtils.makeKeyString(obj.identifier), obj.name); - } - - /** - * Refresh the composition of this select when a domain object is removed - * from the Summary Widget's composition - * @private - */ - function onCompositionRemove() { - const selected = self.select.getSelected(); - self.generateOptions(); - self.select.setSelected(selected); - } - - /** - * Defer setting the selected state on initial load until load is complete - * @private - */ - function onCompositionLoad() { - self.select.setSelected(self.config.object); - } - - this.manager.on('add', onCompositionAdd); - this.manager.on('remove', onCompositionRemove); - this.manager.on('load', onCompositionLoad); - - if (this.manager.loadCompleted()) { - onCompositionLoad(); - } - - return this.select; + function onCompositionAdd(obj) { + self.select.addOption(objectUtils.makeKeyString(obj.identifier), obj.name); } /** - * Populate this select with options based on its current composition + * Refresh the composition of this select when a domain object is removed + * from the Summary Widget's composition + * @private */ - ObjectSelect.prototype.generateOptions = function () { - const items = Object.values(this.compositionObjs).map(function (obj) { - return [objectUtils.makeKeyString(obj.identifier), obj.name]; - }); - this.baseOptions.forEach(function (option, index) { - items.splice(index, 0, option); - }); - this.select.setOptions(items); - }; + function onCompositionRemove() { + const selected = self.select.getSelected(); + self.generateOptions(); + self.select.setSelected(selected); + } - return ObjectSelect; + /** + * Defer setting the selected state on initial load until load is complete + * @private + */ + function onCompositionLoad() { + self.select.setSelected(self.config.object); + } + + this.manager.on('add', onCompositionAdd); + this.manager.on('remove', onCompositionRemove); + this.manager.on('load', onCompositionLoad); + + if (this.manager.loadCompleted()) { + onCompositionLoad(); + } + + return this.select; + } + + /** + * Populate this select with options based on its current composition + */ + ObjectSelect.prototype.generateOptions = function () { + const items = Object.values(this.compositionObjs).map(function (obj) { + return [objectUtils.makeKeyString(obj.identifier), obj.name]; + }); + this.baseOptions.forEach(function (option, index) { + items.splice(index, 0, option); + }); + this.select.setOptions(items); + }; + + return ObjectSelect; }); diff --git a/src/plugins/summaryWidget/src/input/OperationSelect.js b/src/plugins/summaryWidget/src/input/OperationSelect.js index 1e1f7a889b..822852f478 100644 --- a/src/plugins/summaryWidget/src/input/OperationSelect.js +++ b/src/plugins/summaryWidget/src/input/OperationSelect.js @@ -1,128 +1,120 @@ -define([ - './Select', - '../eventHelpers' -], function ( - Select, - eventHelpers -) { +define(['./Select', '../eventHelpers'], function (Select, eventHelpers) { + /** + * Create a {Select} element whose composition is dynamically updated with + * the operations applying to a particular telemetry property + * @constructor + * @param {Object} config The current state of this select. Must have object, + * key, and operation fields + * @param {KeySelect} keySelect The linked Key Select instance to which + * this OperationSelect should listen to for change + * events + * @param {ConditionManager} manager A ConditionManager instance from which + * to receive telemetry metadata + * @param {function} changeCallback A change event callback to register with this + * select on initialization + */ + const NULLVALUE = '- Select Comparison -'; - /** - * Create a {Select} element whose composition is dynamically updated with - * the operations applying to a particular telemetry property - * @constructor - * @param {Object} config The current state of this select. Must have object, - * key, and operation fields - * @param {KeySelect} keySelect The linked Key Select instance to which - * this OperationSelect should listen to for change - * events - * @param {ConditionManager} manager A ConditionManager instance from which - * to receive telemetry metadata - * @param {function} changeCallback A change event callback to register with this - * select on initialization - */ - const NULLVALUE = '- Select Comparison -'; + function OperationSelect(config, keySelect, manager, changeCallback) { + eventHelpers.extend(this); + const self = this; - function OperationSelect(config, keySelect, manager, changeCallback) { - eventHelpers.extend(this); - const self = this; + this.config = config; + this.keySelect = keySelect; + this.manager = manager; - this.config = config; - this.keySelect = keySelect; - this.manager = manager; + this.operationKeys = []; + this.evaluator = this.manager.getEvaluator(); + this.loadComplete = false; - this.operationKeys = []; - this.evaluator = this.manager.getEvaluator(); - this.loadComplete = false; - - this.select = new Select(); - this.select.hide(); - this.select.addOption('', NULLVALUE); - if (changeCallback) { - this.listenTo(this.select, 'change', changeCallback); - } - - /** - * Change event handler for the {KeySelect} to which this OperationSelect instance - * is linked. Loads the operations applicable to the given telemetry property and updates - * its select element's composition - * @param {Object} key The key identifying the newly selected property - * @private - */ - function onKeyChange(key) { - const selected = self.config.operation; - if (self.manager.metadataLoadCompleted()) { - self.loadOptions(key); - self.generateOptions(); - self.select.setSelected(selected); - } - } - - /** - * Event handler for the intial metadata load event from the associated - * ConditionManager. Retreives telemetry property types and updates the - * select - * @private - */ - function onMetadataLoad() { - if (self.manager.getTelemetryPropertyType(self.config.object, self.config.key)) { - self.loadOptions(self.config.key); - self.generateOptions(); - } - - self.select.setSelected(self.config.operation); - } - - this.keySelect.on('change', onKeyChange); - this.manager.on('metadata', onMetadataLoad); - - if (this.manager.metadataLoadCompleted()) { - onMetadataLoad(); - } - - return this.select; + this.select = new Select(); + this.select.hide(); + this.select.addOption('', NULLVALUE); + if (changeCallback) { + this.listenTo(this.select, 'change', changeCallback); } /** - * Populate this select with options based on its current composition + * Change event handler for the {KeySelect} to which this OperationSelect instance + * is linked. Loads the operations applicable to the given telemetry property and updates + * its select element's composition + * @param {Object} key The key identifying the newly selected property + * @private */ - OperationSelect.prototype.generateOptions = function () { - const self = this; - const items = this.operationKeys.map(function (operation) { - return [operation, self.evaluator.getOperationText(operation)]; - }); - items.splice(0, 0, ['', NULLVALUE]); - this.select.setOptions(items); - - if (this.select.options.length < 2) { - this.select.hide(); - } else { - this.select.show(); - } - }; + function onKeyChange(key) { + const selected = self.config.operation; + if (self.manager.metadataLoadCompleted()) { + self.loadOptions(key); + self.generateOptions(); + self.select.setSelected(selected); + } + } /** - * Retrieve the data type associated with a given telemetry property and - * the applicable operations from the {ConditionEvaluator} - * @param {string} key The telemetry property to load operations for + * Event handler for the intial metadata load event from the associated + * ConditionManager. Retreives telemetry property types and updates the + * select + * @private */ - OperationSelect.prototype.loadOptions = function (key) { - const self = this; - const operations = self.evaluator.getOperationKeys(); - let type; + function onMetadataLoad() { + if (self.manager.getTelemetryPropertyType(self.config.object, self.config.key)) { + self.loadOptions(self.config.key); + self.generateOptions(); + } - type = self.manager.getTelemetryPropertyType(self.config.object, key); + self.select.setSelected(self.config.operation); + } - if (type !== undefined) { - self.operationKeys = operations.filter(function (operation) { - return self.evaluator.operationAppliesTo(operation, type); - }); - } - }; + this.keySelect.on('change', onKeyChange); + this.manager.on('metadata', onMetadataLoad); - OperationSelect.prototype.destroy = function () { - this.stopListening(); - }; + if (this.manager.metadataLoadCompleted()) { + onMetadataLoad(); + } - return OperationSelect; + return this.select; + } + /** + * Populate this select with options based on its current composition + */ + OperationSelect.prototype.generateOptions = function () { + const self = this; + const items = this.operationKeys.map(function (operation) { + return [operation, self.evaluator.getOperationText(operation)]; + }); + items.splice(0, 0, ['', NULLVALUE]); + this.select.setOptions(items); + + if (this.select.options.length < 2) { + this.select.hide(); + } else { + this.select.show(); + } + }; + + /** + * Retrieve the data type associated with a given telemetry property and + * the applicable operations from the {ConditionEvaluator} + * @param {string} key The telemetry property to load operations for + */ + OperationSelect.prototype.loadOptions = function (key) { + const self = this; + const operations = self.evaluator.getOperationKeys(); + let type; + + type = self.manager.getTelemetryPropertyType(self.config.object, key); + + if (type !== undefined) { + self.operationKeys = operations.filter(function (operation) { + return self.evaluator.operationAppliesTo(operation, type); + }); + } + }; + + OperationSelect.prototype.destroy = function () { + this.stopListening(); + }; + + return OperationSelect; }); diff --git a/src/plugins/summaryWidget/src/input/Palette.js b/src/plugins/summaryWidget/src/input/Palette.js index 96df813de2..1515aacff4 100644 --- a/src/plugins/summaryWidget/src/input/Palette.js +++ b/src/plugins/summaryWidget/src/input/Palette.js @@ -1,188 +1,183 @@ define([ - '../eventHelpers', - '../../res/input/paletteTemplate.html', - '../../../../utils/template/templateHelpers', - 'EventEmitter' -], function ( - eventHelpers, - paletteTemplate, - templateHelpers, - EventEmitter -) { - /** - * Instantiates a new Open MCT Color Palette input - * @constructor - * @param {string} cssClass The class name of the icon which should be applied - * to this palette - * @param {Element} container The view that contains this palette - * @param {string[]} items A list of data items that will be associated with each - * palette item in the view; how this data is represented is - * up to the descendent class - */ - function Palette(cssClass, container, items) { - eventHelpers.extend(this); + '../eventHelpers', + '../../res/input/paletteTemplate.html', + '../../../../utils/template/templateHelpers', + 'EventEmitter' +], function (eventHelpers, paletteTemplate, templateHelpers, EventEmitter) { + /** + * Instantiates a new Open MCT Color Palette input + * @constructor + * @param {string} cssClass The class name of the icon which should be applied + * to this palette + * @param {Element} container The view that contains this palette + * @param {string[]} items A list of data items that will be associated with each + * palette item in the view; how this data is represented is + * up to the descendent class + */ + function Palette(cssClass, container, items) { + eventHelpers.extend(this); - const self = this; + const self = this; - this.cssClass = cssClass; - this.items = items; - this.container = container; + this.cssClass = cssClass; + this.items = items; + this.container = container; - this.domElement = templateHelpers.convertTemplateToHTML(paletteTemplate)[0]; + this.domElement = templateHelpers.convertTemplateToHTML(paletteTemplate)[0]; - this.itemElements = { - nullOption: this.domElement.querySelector('.c-palette__item-none .c-palette__item') - }; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['change']; - this.value = this.items[0]; - this.nullOption = ' '; - this.button = this.domElement.querySelector('.js-button'); - this.menu = this.domElement.querySelector('.c-menu'); + this.itemElements = { + nullOption: this.domElement.querySelector('.c-palette__item-none .c-palette__item') + }; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['change']; + this.value = this.items[0]; + this.nullOption = ' '; + this.button = this.domElement.querySelector('.js-button'); + this.menu = this.domElement.querySelector('.c-menu'); - this.hideMenu = this.hideMenu.bind(this); + this.hideMenu = this.hideMenu.bind(this); - if (this.cssClass) { - self.button.classList.add(this.cssClass); - } - - self.setNullOption(this.nullOption); - - self.items.forEach(function (item) { - const itemElement = `
`; - const temp = document.createElement('div'); - temp.innerHTML = itemElement; - self.itemElements[item] = temp.firstChild; - self.domElement.querySelector('.c-palette__items').appendChild(temp.firstChild); - }); - - self.domElement.querySelector('.c-menu').style.display = 'none'; - - this.listenTo(window.document, 'click', this.hideMenu); - this.listenTo(self.domElement.querySelector('.js-button'), 'click', function (event) { - event.stopPropagation(); - self.container.querySelector('.c-menu').style.display = 'none'; - self.domElement.querySelector('.c-menu').style.display = ''; - }); - - /** - * Event handler for selection of an individual palette item. Sets the - * currently selected element to be the one associated with that item's data - * @param {Event} event the click event that initiated this callback - * @private - */ - function handleItemClick(event) { - const elem = event.currentTarget; - const item = elem.dataset.item; - self.set(item); - self.domElement.querySelector('.c-menu').style.display = 'none'; - } - - self.domElement.querySelectorAll('.c-palette__item').forEach(item => { - this.listenTo(item, 'click', handleItemClick); - }); + if (this.cssClass) { + self.button.classList.add(this.cssClass); } - /** - * Get the DOM element representing this palette in the view - */ - Palette.prototype.getDOM = function () { - return this.domElement; - }; + self.setNullOption(this.nullOption); + + self.items.forEach(function (item) { + const itemElement = `
`; + const temp = document.createElement('div'); + temp.innerHTML = itemElement; + self.itemElements[item] = temp.firstChild; + self.domElement.querySelector('.c-palette__items').appendChild(temp.firstChild); + }); + + self.domElement.querySelector('.c-menu').style.display = 'none'; + + this.listenTo(window.document, 'click', this.hideMenu); + this.listenTo(self.domElement.querySelector('.js-button'), 'click', function (event) { + event.stopPropagation(); + self.container.querySelector('.c-menu').style.display = 'none'; + self.domElement.querySelector('.c-menu').style.display = ''; + }); /** - * Clean up any event listeners registered to DOM elements external to the widget + * Event handler for selection of an individual palette item. Sets the + * currently selected element to be the one associated with that item's data + * @param {Event} event the click event that initiated this callback + * @private */ - Palette.prototype.destroy = function () { - this.stopListening(); - }; + function handleItemClick(event) { + const elem = event.currentTarget; + const item = elem.dataset.item; + self.set(item); + self.domElement.querySelector('.c-menu').style.display = 'none'; + } - Palette.prototype.hideMenu = function () { - this.domElement.querySelector('.c-menu').style.display = 'none'; - }; + self.domElement.querySelectorAll('.c-palette__item').forEach((item) => { + this.listenTo(item, 'click', handleItemClick); + }); + } - /** - * Register a callback with this palette: supported callback is change - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - Palette.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } else { - throw new Error('Unsupported event type: ' + event); - } - }; + /** + * Get the DOM element representing this palette in the view + */ + Palette.prototype.getDOM = function () { + return this.domElement; + }; - /** - * Get the currently selected value of this palette - * @return {string} The selected value - */ - Palette.prototype.getCurrent = function () { - return this.value; - }; + /** + * Clean up any event listeners registered to DOM elements external to the widget + */ + Palette.prototype.destroy = function () { + this.stopListening(); + }; - /** - * Set the selected value of this palette; if the item doesn't exist in the - * palette's data model, the selected value will not change. Invokes any - * change callbacks associated with this palette. - * @param {string} item The key of the item to set as selected - */ - Palette.prototype.set = function (item) { - const self = this; - if (this.items.includes(item) || item === this.nullOption) { - this.value = item; - if (item === this.nullOption) { - this.updateSelected('nullOption'); - } else { - this.updateSelected(item); - } - } + Palette.prototype.hideMenu = function () { + this.domElement.querySelector('.c-menu').style.display = 'none'; + }; - this.eventEmitter.emit('change', self.value); - }; + /** + * Register a callback with this palette: supported callback is change + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + Palette.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } else { + throw new Error('Unsupported event type: ' + event); + } + }; - /** - * Update the view assoicated with the currently selected item - */ - Palette.prototype.updateSelected = function (item) { - this.domElement.querySelectorAll('.c-palette__item').forEach(paletteItem => { - if (paletteItem.classList.contains('is-selected')) { - paletteItem.classList.remove('is-selected'); - } - }); - this.itemElements[item].classList.add('is-selected'); - if (item === 'nullOption') { - this.domElement.querySelector('.t-swatch').classList.add('no-selection'); - } else { - this.domElement.querySelector('.t-swatch').classList.remove('no-selection'); - } - }; + /** + * Get the currently selected value of this palette + * @return {string} The selected value + */ + Palette.prototype.getCurrent = function () { + return this.value; + }; - /** - * set the property to be used for the 'no selection' item. If not set, this - * defaults to a single space - * @param {string} item The key to use as the 'no selection' item - */ - Palette.prototype.setNullOption = function (item) { - this.nullOption = item; - this.itemElements.nullOption.data = { item: item }; - }; + /** + * Set the selected value of this palette; if the item doesn't exist in the + * palette's data model, the selected value will not change. Invokes any + * change callbacks associated with this palette. + * @param {string} item The key of the item to set as selected + */ + Palette.prototype.set = function (item) { + const self = this; + if (this.items.includes(item) || item === this.nullOption) { + this.value = item; + if (item === this.nullOption) { + this.updateSelected('nullOption'); + } else { + this.updateSelected(item); + } + } - /** - * Hides the 'no selection' option to be hidden in the view if it doesn't apply - */ - Palette.prototype.toggleNullOption = function () { - const elem = this.domElement.querySelector('.c-palette__item-none'); + this.eventEmitter.emit('change', self.value); + }; - if (elem.style.display === 'none') { - this.domElement.querySelector('.c-palette__item-none').style.display = 'flex'; - } else { - this.domElement.querySelector('.c-palette__item-none').style.display = 'none'; - } - }; + /** + * Update the view assoicated with the currently selected item + */ + Palette.prototype.updateSelected = function (item) { + this.domElement.querySelectorAll('.c-palette__item').forEach((paletteItem) => { + if (paletteItem.classList.contains('is-selected')) { + paletteItem.classList.remove('is-selected'); + } + }); + this.itemElements[item].classList.add('is-selected'); + if (item === 'nullOption') { + this.domElement.querySelector('.t-swatch').classList.add('no-selection'); + } else { + this.domElement.querySelector('.t-swatch').classList.remove('no-selection'); + } + }; - return Palette; + /** + * set the property to be used for the 'no selection' item. If not set, this + * defaults to a single space + * @param {string} item The key to use as the 'no selection' item + */ + Palette.prototype.setNullOption = function (item) { + this.nullOption = item; + this.itemElements.nullOption.data = { item: item }; + }; + + /** + * Hides the 'no selection' option to be hidden in the view if it doesn't apply + */ + Palette.prototype.toggleNullOption = function () { + const elem = this.domElement.querySelector('.c-palette__item-none'); + + if (elem.style.display === 'none') { + this.domElement.querySelector('.c-palette__item-none').style.display = 'flex'; + } else { + this.domElement.querySelector('.c-palette__item-none').style.display = 'none'; + } + }; + + return Palette; }); diff --git a/src/plugins/summaryWidget/src/input/Select.js b/src/plugins/summaryWidget/src/input/Select.js index 676a9791b2..38df9c5fc1 100644 --- a/src/plugins/summaryWidget/src/input/Select.js +++ b/src/plugins/summaryWidget/src/input/Select.js @@ -1,160 +1,154 @@ define([ - '../eventHelpers', - '../../res/input/selectTemplate.html', - '../../../../utils/template/templateHelpers', - 'EventEmitter' -], function ( - eventHelpers, - selectTemplate, - templateHelpers, - EventEmitter -) { + '../eventHelpers', + '../../res/input/selectTemplate.html', + '../../../../utils/template/templateHelpers', + 'EventEmitter' +], function (eventHelpers, selectTemplate, templateHelpers, EventEmitter) { + /** + * Wraps an HTML select element, and provides methods for dynamically altering + * its composition from the data model + * @constructor + */ + function Select() { + eventHelpers.extend(this); + + const self = this; + + this.domElement = templateHelpers.convertTemplateToHTML(selectTemplate)[0]; + + this.options = []; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['change']; + + this.populate(); /** - * Wraps an HTML select element, and provides methods for dynamically altering - * its composition from the data model - * @constructor + * Event handler for the wrapped select element. Also invokes any change + * callbacks registered with this select with the new value + * @param {Event} event The change event that triggered this callback + * @private */ - function Select() { - eventHelpers.extend(this); + function onChange(event) { + const elem = event.target; + const value = self.options[elem.selectedIndex]; - const self = this; - - this.domElement = templateHelpers.convertTemplateToHTML(selectTemplate)[0]; - - this.options = []; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['change']; - - this.populate(); - - /** - * Event handler for the wrapped select element. Also invokes any change - * callbacks registered with this select with the new value - * @param {Event} event The change event that triggered this callback - * @private - */ - function onChange(event) { - const elem = event.target; - const value = self.options[elem.selectedIndex]; - - self.eventEmitter.emit('change', value[0]); - } - - this.listenTo(this.domElement.querySelector('select'), 'change', onChange, this); + self.eventEmitter.emit('change', value[0]); } - /** - * Get the DOM element representing this Select in the view - * @return {Element} - */ - Select.prototype.getDOM = function () { - return this.domElement; - }; + this.listenTo(this.domElement.querySelector('select'), 'change', onChange, this); + } - /** - * Register a callback with this select: supported callback is change - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - Select.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } else { - throw new Error('Unsupported event type' + event); - } - }; + /** + * Get the DOM element representing this Select in the view + * @return {Element} + */ + Select.prototype.getDOM = function () { + return this.domElement; + }; - /** - * Update the select element in the view from the current state of the data - * model - */ - Select.prototype.populate = function () { - const self = this; - let selectedIndex = 0; + /** + * Register a callback with this select: supported callback is change + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + Select.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } else { + throw new Error('Unsupported event type' + event); + } + }; - selectedIndex = this.domElement.querySelector('select').selectedIndex; + /** + * Update the select element in the view from the current state of the data + * model + */ + Select.prototype.populate = function () { + const self = this; + let selectedIndex = 0; - this.domElement.querySelector('select').innerHTML = ''; + selectedIndex = this.domElement.querySelector('select').selectedIndex; - self.options.forEach(function (option) { - const optionElement = document.createElement('option'); - optionElement.value = option[0]; - optionElement.innerText = `+ ${option[1]}`; + this.domElement.querySelector('select').innerHTML = ''; - self.domElement.querySelector('select').appendChild(optionElement); - }); + self.options.forEach(function (option) { + const optionElement = document.createElement('option'); + optionElement.value = option[0]; + optionElement.innerText = `+ ${option[1]}`; - this.domElement.querySelector('select').selectedIndex = selectedIndex; - }; + self.domElement.querySelector('select').appendChild(optionElement); + }); - /** - * Add a single option to this select - * @param {string} value The value for the new option - * @param {string} label The human-readable text for the new option - */ - Select.prototype.addOption = function (value, label) { - this.options.push([value, label]); - this.populate(); - }; + this.domElement.querySelector('select').selectedIndex = selectedIndex; + }; - /** - * Set the available options for this select. Replaces any existing options - * @param {string[][]} options An array of [value, label] pairs to display - */ - Select.prototype.setOptions = function (options) { - this.options = options; - this.populate(); - }; + /** + * Add a single option to this select + * @param {string} value The value for the new option + * @param {string} label The human-readable text for the new option + */ + Select.prototype.addOption = function (value, label) { + this.options.push([value, label]); + this.populate(); + }; - /** - * Sets the currently selected element an invokes any registered change - * callbacks with the new value. If the value doesn't exist in this select's - * model, its state will not change. - * @param {string} value The value to set as the selected option - */ - Select.prototype.setSelected = function (value) { - let selectedIndex = 0; - let selectedOption; + /** + * Set the available options for this select. Replaces any existing options + * @param {string[][]} options An array of [value, label] pairs to display + */ + Select.prototype.setOptions = function (options) { + this.options = options; + this.populate(); + }; - this.options.forEach (function (option, index) { - if (option[0] === value) { - selectedIndex = index; - } - }); - this.domElement.querySelector('select').selectedIndex = selectedIndex; + /** + * Sets the currently selected element an invokes any registered change + * callbacks with the new value. If the value doesn't exist in this select's + * model, its state will not change. + * @param {string} value The value to set as the selected option + */ + Select.prototype.setSelected = function (value) { + let selectedIndex = 0; + let selectedOption; - selectedOption = this.options[selectedIndex]; - this.eventEmitter.emit('change', selectedOption[0]); - }; + this.options.forEach(function (option, index) { + if (option[0] === value) { + selectedIndex = index; + } + }); + this.domElement.querySelector('select').selectedIndex = selectedIndex; - /** - * Get the value of the currently selected item - * @return {string} - */ - Select.prototype.getSelected = function () { - return this.domElement.querySelector('select').value; - }; + selectedOption = this.options[selectedIndex]; + this.eventEmitter.emit('change', selectedOption[0]); + }; - Select.prototype.hide = function () { - this.domElement.classList.add('hidden'); - if (this.domElement.querySelector('.equal-to')) { - this.domElement.querySelector('.equal-to').classList.add('hidden'); - } - }; + /** + * Get the value of the currently selected item + * @return {string} + */ + Select.prototype.getSelected = function () { + return this.domElement.querySelector('select').value; + }; - Select.prototype.show = function () { - this.domElement.classList.remove('hidden'); - if (this.domElement.querySelector('.equal-to')) { - this.domElement.querySelector('.equal-to').classList.remove('hidden'); - } - }; + Select.prototype.hide = function () { + this.domElement.classList.add('hidden'); + if (this.domElement.querySelector('.equal-to')) { + this.domElement.querySelector('.equal-to').classList.add('hidden'); + } + }; - Select.prototype.destroy = function () { - this.stopListening(); - }; + Select.prototype.show = function () { + this.domElement.classList.remove('hidden'); + if (this.domElement.querySelector('.equal-to')) { + this.domElement.querySelector('.equal-to').classList.remove('hidden'); + } + }; - return Select; + Select.prototype.destroy = function () { + this.stopListening(); + }; + + return Select; }); diff --git a/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js b/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js index 8954ff4e3d..b6cac0595e 100644 --- a/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js +++ b/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js @@ -20,47 +20,40 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetEvaluator', - 'objectUtils' -], function ( - SummaryWidgetEvaluator, - objectUtils -) { +define(['./SummaryWidgetEvaluator', 'objectUtils'], function (SummaryWidgetEvaluator, objectUtils) { + function EvaluatorPool(openmct) { + this.openmct = openmct; + this.byObjectId = {}; + this.byEvaluator = new WeakMap(); + } - function EvaluatorPool(openmct) { - this.openmct = openmct; - this.byObjectId = {}; - this.byEvaluator = new WeakMap(); + EvaluatorPool.prototype.get = function (domainObject) { + const objectId = objectUtils.makeKeyString(domainObject.identifier); + let poolEntry = this.byObjectId[objectId]; + if (!poolEntry) { + poolEntry = { + leases: 0, + objectId: objectId, + evaluator: new SummaryWidgetEvaluator(domainObject, this.openmct) + }; + this.byEvaluator.set(poolEntry.evaluator, poolEntry); + this.byObjectId[objectId] = poolEntry; } - EvaluatorPool.prototype.get = function (domainObject) { - const objectId = objectUtils.makeKeyString(domainObject.identifier); - let poolEntry = this.byObjectId[objectId]; - if (!poolEntry) { - poolEntry = { - leases: 0, - objectId: objectId, - evaluator: new SummaryWidgetEvaluator(domainObject, this.openmct) - }; - this.byEvaluator.set(poolEntry.evaluator, poolEntry); - this.byObjectId[objectId] = poolEntry; - } + poolEntry.leases += 1; - poolEntry.leases += 1; + return poolEntry.evaluator; + }; - return poolEntry.evaluator; - }; + EvaluatorPool.prototype.release = function (evaluator) { + const poolEntry = this.byEvaluator.get(evaluator); + poolEntry.leases -= 1; + if (poolEntry.leases === 0) { + evaluator.destroy(); + this.byEvaluator.delete(evaluator); + delete this.byObjectId[poolEntry.objectId]; + } + }; - EvaluatorPool.prototype.release = function (evaluator) { - const poolEntry = this.byEvaluator.get(evaluator); - poolEntry.leases -= 1; - if (poolEntry.leases === 0) { - evaluator.destroy(); - this.byEvaluator.delete(evaluator); - delete this.byObjectId[poolEntry.objectId]; - } - }; - - return EvaluatorPool; + return EvaluatorPool; }); diff --git a/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js b/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js index 28fc2d129f..d5b3398267 100644 --- a/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js +++ b/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js @@ -20,84 +20,78 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './EvaluatorPool', - './SummaryWidgetEvaluator' -], function ( - EvaluatorPool, - SummaryWidgetEvaluator +define(['./EvaluatorPool', './SummaryWidgetEvaluator'], function ( + EvaluatorPool, + SummaryWidgetEvaluator ) { - describe('EvaluatorPool', function () { - let pool; - let openmct; - let objectA; - let objectB; + describe('EvaluatorPool', function () { + let pool; + let openmct; + let objectA; + let objectB; - beforeEach(function () { - openmct = { - composition: jasmine.createSpyObj('compositionAPI', ['get']), - objects: jasmine.createSpyObj('objectAPI', ['observe']) - }; - openmct.composition.get.and.callFake(function () { - const compositionCollection = jasmine.createSpyObj( - 'compositionCollection', - [ - 'load', - 'on', - 'off' - ] - ); - compositionCollection.load.and.returnValue(Promise.resolve()); + beforeEach(function () { + openmct = { + composition: jasmine.createSpyObj('compositionAPI', ['get']), + objects: jasmine.createSpyObj('objectAPI', ['observe']) + }; + openmct.composition.get.and.callFake(function () { + const compositionCollection = jasmine.createSpyObj('compositionCollection', [ + 'load', + 'on', + 'off' + ]); + compositionCollection.load.and.returnValue(Promise.resolve()); - return compositionCollection; - }); - openmct.objects.observe.and.callFake(function () { - return function () {}; - }); - pool = new EvaluatorPool(openmct); - objectA = { - identifier: { - namespace: 'someNamespace', - key: 'someKey' - }, - configuration: { - ruleOrder: [] - } - }; - objectB = { - identifier: { - namespace: 'otherNamespace', - key: 'otherKey' - }, - configuration: { - ruleOrder: [] - } - }; - }); - - it('returns new evaluators for different objects', function () { - const evaluatorA = pool.get(objectA); - const evaluatorB = pool.get(objectB); - expect(evaluatorA).not.toBe(evaluatorB); - }); - - it('returns the same evaluator for the same object', function () { - const evaluatorA = pool.get(objectA); - const evaluatorB = pool.get(objectA); - expect(evaluatorA).toBe(evaluatorB); - - const evaluatorC = pool.get(JSON.parse(JSON.stringify(objectA))); - expect(evaluatorA).toBe(evaluatorC); - }); - - it('returns new evaluator when old is released', function () { - const evaluatorA = pool.get(objectA); - const evaluatorB = pool.get(objectA); - expect(evaluatorA).toBe(evaluatorB); - pool.release(evaluatorA); - pool.release(evaluatorB); - const evaluatorC = pool.get(objectA); - expect(evaluatorA).not.toBe(evaluatorC); - }); + return compositionCollection; + }); + openmct.objects.observe.and.callFake(function () { + return function () {}; + }); + pool = new EvaluatorPool(openmct); + objectA = { + identifier: { + namespace: 'someNamespace', + key: 'someKey' + }, + configuration: { + ruleOrder: [] + } + }; + objectB = { + identifier: { + namespace: 'otherNamespace', + key: 'otherKey' + }, + configuration: { + ruleOrder: [] + } + }; }); + + it('returns new evaluators for different objects', function () { + const evaluatorA = pool.get(objectA); + const evaluatorB = pool.get(objectB); + expect(evaluatorA).not.toBe(evaluatorB); + }); + + it('returns the same evaluator for the same object', function () { + const evaluatorA = pool.get(objectA); + const evaluatorB = pool.get(objectA); + expect(evaluatorA).toBe(evaluatorB); + + const evaluatorC = pool.get(JSON.parse(JSON.stringify(objectA))); + expect(evaluatorA).toBe(evaluatorC); + }); + + it('returns new evaluator when old is released', function () { + const evaluatorA = pool.get(objectA); + const evaluatorB = pool.get(objectA); + expect(evaluatorA).toBe(evaluatorB); + pool.release(evaluatorA); + pool.release(evaluatorB); + const evaluatorC = pool.get(objectA); + expect(evaluatorA).not.toBe(evaluatorC); + }); + }); }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js index df6828fa07..621e656d37 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js @@ -20,64 +20,57 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './operations' -], function ( - OPERATIONS -) { - - function SummaryWidgetCondition(definition) { - this.object = definition.object; - this.key = definition.key; - this.values = definition.values; - if (!definition.operation) { - // TODO: better handling for default rule. - this.evaluate = function () { - return true; - }; - } else { - this.comparator = OPERATIONS[definition.operation].operation; - } +define(['./operations'], function (OPERATIONS) { + function SummaryWidgetCondition(definition) { + this.object = definition.object; + this.key = definition.key; + this.values = definition.values; + if (!definition.operation) { + // TODO: better handling for default rule. + this.evaluate = function () { + return true; + }; + } else { + this.comparator = OPERATIONS[definition.operation].operation; } + } - SummaryWidgetCondition.prototype.evaluate = function (telemetryState) { - const stateKeys = Object.keys(telemetryState); - let state; - let result; - let i; + SummaryWidgetCondition.prototype.evaluate = function (telemetryState) { + const stateKeys = Object.keys(telemetryState); + let state; + let result; + let i; - if (this.object === 'any') { - for (i = 0; i < stateKeys.length; i++) { - state = telemetryState[stateKeys[i]]; - result = this.evaluateState(state); - if (result) { - return true; - } - } - - return false; - } else if (this.object === 'all') { - for (i = 0; i < stateKeys.length; i++) { - state = telemetryState[stateKeys[i]]; - result = this.evaluateState(state); - if (!result) { - return false; - } - } - - return true; - } else { - return this.evaluateState(telemetryState[this.object]); + if (this.object === 'any') { + for (i = 0; i < stateKeys.length; i++) { + state = telemetryState[stateKeys[i]]; + result = this.evaluateState(state); + if (result) { + return true; } - }; + } - SummaryWidgetCondition.prototype.evaluateState = function (state) { - const testValues = [ - state.formats[this.key].parse(state.lastDatum) - ].concat(this.values); + return false; + } else if (this.object === 'all') { + for (i = 0; i < stateKeys.length; i++) { + state = telemetryState[stateKeys[i]]; + result = this.evaluateState(state); + if (!result) { + return false; + } + } - return this.comparator(testValues); - }; + return true; + } else { + return this.evaluateState(telemetryState[this.object]); + } + }; - return SummaryWidgetCondition; + SummaryWidgetCondition.prototype.evaluateState = function (state) { + const testValues = [state.formats[this.key].parse(state.lastDatum)].concat(this.values); + + return this.comparator(testValues); + }; + + return SummaryWidgetCondition; }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js index 49cf9bf4ef..9bf35a3f27 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js @@ -20,123 +20,106 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetCondition' -], function ( - SummaryWidgetCondition -) { +define(['./SummaryWidgetCondition'], function (SummaryWidgetCondition) { + describe('SummaryWidgetCondition', function () { + let condition; + let telemetryState; - describe('SummaryWidgetCondition', function () { - let condition; - let telemetryState; - - beforeEach(function () { - // Format map intentionally uses different keys than those present - // in datum, which serves to verify conditions use format map to get - // data. - const formatMap = { - adjusted: { - parse: function (datum) { - return datum.value + 10; - } - }, - raw: { - parse: function (datum) { - return datum.value; - } - } - }; - - telemetryState = { - objectId: { - formats: formatMap, - lastDatum: { - } - }, - otherObjectId: { - formats: formatMap, - lastDatum: { - } - } - }; - - }); - - it('can evaluate if a single object matches', function () { - condition = new SummaryWidgetCondition({ - object: 'objectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }); - telemetryState.objectId.lastDatum.value = 5; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - expect(condition.evaluate(telemetryState)).toBe(true); - }); - - it('can evaluate if a single object matches (alternate keys)', function () { - condition = new SummaryWidgetCondition({ - object: 'objectId', - key: 'adjusted', - operation: 'greaterThan', - values: [ - 10 - ] - }); - telemetryState.objectId.lastDatum.value = -5; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 5; - expect(condition.evaluate(telemetryState)).toBe(true); - }); - - it('can evaluate "if all objects match"', function () { - condition = new SummaryWidgetCondition({ - object: 'all', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }); - telemetryState.objectId.lastDatum.value = 0; - telemetryState.otherObjectId.lastDatum.value = 0; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 0; - telemetryState.otherObjectId.lastDatum.value = 15; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 0; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 15; - expect(condition.evaluate(telemetryState)).toBe(true); - }); - - it('can evaluate "if any object matches"', function () { - condition = new SummaryWidgetCondition({ - object: 'any', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }); - telemetryState.objectId.lastDatum.value = 0; - telemetryState.otherObjectId.lastDatum.value = 0; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 0; - telemetryState.otherObjectId.lastDatum.value = 15; - expect(condition.evaluate(telemetryState)).toBe(true); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 0; - expect(condition.evaluate(telemetryState)).toBe(true); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 15; - expect(condition.evaluate(telemetryState)).toBe(true); - }); + beforeEach(function () { + // Format map intentionally uses different keys than those present + // in datum, which serves to verify conditions use format map to get + // data. + const formatMap = { + adjusted: { + parse: function (datum) { + return datum.value + 10; + } + }, + raw: { + parse: function (datum) { + return datum.value; + } + } + }; + telemetryState = { + objectId: { + formats: formatMap, + lastDatum: {} + }, + otherObjectId: { + formats: formatMap, + lastDatum: {} + } + }; }); + + it('can evaluate if a single object matches', function () { + condition = new SummaryWidgetCondition({ + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [10] + }); + telemetryState.objectId.lastDatum.value = 5; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + expect(condition.evaluate(telemetryState)).toBe(true); + }); + + it('can evaluate if a single object matches (alternate keys)', function () { + condition = new SummaryWidgetCondition({ + object: 'objectId', + key: 'adjusted', + operation: 'greaterThan', + values: [10] + }); + telemetryState.objectId.lastDatum.value = -5; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 5; + expect(condition.evaluate(telemetryState)).toBe(true); + }); + + it('can evaluate "if all objects match"', function () { + condition = new SummaryWidgetCondition({ + object: 'all', + key: 'raw', + operation: 'greaterThan', + values: [10] + }); + telemetryState.objectId.lastDatum.value = 0; + telemetryState.otherObjectId.lastDatum.value = 0; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 0; + telemetryState.otherObjectId.lastDatum.value = 15; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 0; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 15; + expect(condition.evaluate(telemetryState)).toBe(true); + }); + + it('can evaluate "if any object matches"', function () { + condition = new SummaryWidgetCondition({ + object: 'any', + key: 'raw', + operation: 'greaterThan', + values: [10] + }); + telemetryState.objectId.lastDatum.value = 0; + telemetryState.otherObjectId.lastDatum.value = 0; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 0; + telemetryState.otherObjectId.lastDatum.value = 15; + expect(condition.evaluate(telemetryState)).toBe(true); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 0; + expect(condition.evaluate(telemetryState)).toBe(true); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 15; + expect(condition.evaluate(telemetryState)).toBe(true); + }); + }); }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js index 0139ff4418..3a98e643c4 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js @@ -20,273 +20,251 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetRule', - '../eventHelpers', - 'objectUtils', - 'lodash' -], function ( - SummaryWidgetRule, - eventHelpers, - objectUtils, - _ +define(['./SummaryWidgetRule', '../eventHelpers', 'objectUtils', 'lodash'], function ( + SummaryWidgetRule, + eventHelpers, + objectUtils, + _ ) { + /** + * evaluates rules defined in a summary widget against either lad or + * realtime state. + * + */ + function SummaryWidgetEvaluator(domainObject, openmct) { + this.openmct = openmct; + this.baseState = {}; - /** - * evaluates rules defined in a summary widget against either lad or - * realtime state. - * - */ - function SummaryWidgetEvaluator(domainObject, openmct) { - this.openmct = openmct; - this.baseState = {}; + this.updateRules(domainObject); + this.removeObserver = openmct.objects.observe(domainObject, '*', this.updateRules.bind(this)); - this.updateRules(domainObject); - this.removeObserver = openmct.objects.observe( - domainObject, - '*', - this.updateRules.bind(this) - ); + const composition = openmct.composition.get(domainObject); - const composition = openmct.composition.get(domainObject); + this.listenTo(composition, 'add', this.addChild, this); + this.listenTo(composition, 'remove', this.removeChild, this); - this.listenTo(composition, 'add', this.addChild, this); - this.listenTo(composition, 'remove', this.removeChild, this); + this.loadPromise = composition.load(); + } - this.loadPromise = composition.load(); - } + eventHelpers.extend(SummaryWidgetEvaluator.prototype); - eventHelpers.extend(SummaryWidgetEvaluator.prototype); + /** + * Subscribes to realtime telemetry for the given summary widget. + */ + SummaryWidgetEvaluator.prototype.subscribe = function (callback) { + let active = true; + let unsubscribes = []; - /** - * Subscribes to realtime telemetry for the given summary widget. - */ - SummaryWidgetEvaluator.prototype.subscribe = function (callback) { - let active = true; - let unsubscribes = []; - - this.getBaseStateClone() - .then(function (realtimeStates) { - if (!active) { - return; - } - - const updateCallback = function () { - const datum = this.evaluateState( - realtimeStates, - this.openmct.time.timeSystem().key - ); - if (datum) { - callback(datum); - } - }.bind(this); - - /* eslint-disable you-dont-need-lodash-underscore/map */ - unsubscribes = _.map( - realtimeStates, - this.subscribeToObjectState.bind(this, updateCallback) - ); - /* eslint-enable you-dont-need-lodash-underscore/map */ - }.bind(this)); - - return function () { - active = false; - unsubscribes.forEach(function (unsubscribe) { - unsubscribe(); - }); - }; - }; - - /** - * Returns a promise for a telemetry datum obtained by evaluating the - * current lad data. - */ - SummaryWidgetEvaluator.prototype.requestLatest = function (options) { - return this.getBaseStateClone() - .then(function (ladState) { - const promises = Object.values(ladState) - .map(this.updateObjectStateFromLAD.bind(this, options)); - - return Promise.all(promises) - .then(function () { - return ladState; - }); - }.bind(this)) - .then(function (ladStates) { - return this.evaluateState(ladStates, options.domain); - }.bind(this)); - }; - - SummaryWidgetEvaluator.prototype.updateRules = function (domainObject) { - this.rules = domainObject.configuration.ruleOrder.map(function (ruleId) { - return new SummaryWidgetRule(domainObject.configuration.ruleConfigById[ruleId]); - }); - }; - - SummaryWidgetEvaluator.prototype.addChild = function (childObject) { - const childId = objectUtils.makeKeyString(childObject.identifier); - const metadata = this.openmct.telemetry.getMetadata(childObject); - const formats = this.openmct.telemetry.getFormatMap(metadata); - - this.baseState[childId] = { - id: childId, - domainObject: childObject, - metadata: metadata, - formats: formats - }; - }; - - SummaryWidgetEvaluator.prototype.removeChild = function (childObject) { - const childId = objectUtils.makeKeyString(childObject.identifier); - delete this.baseState[childId]; - }; - - SummaryWidgetEvaluator.prototype.load = function () { - return this.loadPromise; - }; - - /** - * Return a promise for a 2-deep clone of the base state object: object - * states are shallow cloned, and then assembled and returned as a new base - * state. Allows object states to be mutated while sharing telemetry - * metadata and formats. - */ - SummaryWidgetEvaluator.prototype.getBaseStateClone = function () { - return this.load() - .then(function () { - /* eslint-disable you-dont-need-lodash-underscore/values */ - return _(this.baseState) - .values() - .map(_.clone) - .keyBy('id') - .value(); - /* eslint-enable you-dont-need-lodash-underscore/values */ - }.bind(this)); - }; - - /** - * Subscribes to realtime updates for a given objectState, and invokes - * the supplied callback when objectState has been updated. Returns - * a function to unsubscribe. - * @private. - */ - SummaryWidgetEvaluator.prototype.subscribeToObjectState = function (callback, objectState) { - return this.openmct.telemetry.subscribe( - objectState.domainObject, - function (datum) { - objectState.lastDatum = datum; - objectState.timestamps = this.getTimestamps(objectState.id, datum); - callback(); - }.bind(this) - ); - }; - - /** - * Given an object state, will return a promise that is resolved when the - * object state has been updated from the LAD. - * @private. - */ - SummaryWidgetEvaluator.prototype.updateObjectStateFromLAD = function (options, objectState) { - options = Object.assign({}, options, { - strategy: 'latest', - size: 1 - }); - - return this.openmct - .telemetry - .request( - objectState.domainObject, - options - ) - .then(function (results) { - objectState.lastDatum = results[results.length - 1]; - objectState.timestamps = this.getTimestamps( - objectState.id, - objectState.lastDatum - ); - }.bind(this)); - }; - - /** - * Returns an object containing all domain values in a datum. - * @private. - */ - SummaryWidgetEvaluator.prototype.getTimestamps = function (childId, datum) { - const timestampedDatum = {}; - this.openmct.time.getAllTimeSystems().forEach(function (timeSystem) { - timestampedDatum[timeSystem.key] = - this.baseState[childId].formats[timeSystem.key].parse(datum); - }, this); - - return timestampedDatum; - }; - - /** - * Given a base datum(containing timestamps) and rule index, adds values - * from the matching rule. - * @private - */ - SummaryWidgetEvaluator.prototype.makeDatumFromRule = function (ruleIndex, baseDatum) { - const rule = this.rules[ruleIndex]; - - baseDatum.ruleLabel = rule.label; - baseDatum.ruleName = rule.name; - baseDatum.message = rule.message; - baseDatum.ruleIndex = ruleIndex; - baseDatum.backgroundColor = rule.style['background-color']; - baseDatum.textColor = rule.style.color; - baseDatum.borderColor = rule.style['border-color']; - baseDatum.icon = rule.icon; - - return baseDatum; - }; - - /** - * Evaluate a `state` object and return a summary widget telemetry datum. - * Datum timestamps will be taken from the "latest" datum in the `state` - * where "latest" is the datum with the largest value for the given - * `timestampKey`. - * @private. - */ - SummaryWidgetEvaluator.prototype.evaluateState = function (state, timestampKey) { - const hasRequiredData = Object.keys(state).reduce(function (itDoes, k) { - return itDoes && state[k].lastDatum; - }, true); - if (!hasRequiredData) { - return; + this.getBaseStateClone().then( + function (realtimeStates) { + if (!active) { + return; } - let i; - for (i = this.rules.length - 1; i > 0; i--) { - if (this.rules[i].evaluate(state, false)) { - break; - } - } + const updateCallback = function () { + const datum = this.evaluateState(realtimeStates, this.openmct.time.timeSystem().key); + if (datum) { + callback(datum); + } + }.bind(this); /* eslint-disable you-dont-need-lodash-underscore/map */ - let latestTimestamp = _(state) - .map('timestamps') - .sortBy(timestampKey) - .last(); + unsubscribes = _.map( + realtimeStates, + this.subscribeToObjectState.bind(this, updateCallback) + ); /* eslint-enable you-dont-need-lodash-underscore/map */ + }.bind(this) + ); - if (!latestTimestamp) { - latestTimestamp = {}; - } - - const baseDatum = _.clone(latestTimestamp); - - return this.makeDatumFromRule(i, baseDatum); + return function () { + active = false; + unsubscribes.forEach(function (unsubscribe) { + unsubscribe(); + }); }; + }; - /** - * remove all listeners and clean up any resources. - */ - SummaryWidgetEvaluator.prototype.destroy = function () { - this.stopListening(); - this.removeObserver(); + /** + * Returns a promise for a telemetry datum obtained by evaluating the + * current lad data. + */ + SummaryWidgetEvaluator.prototype.requestLatest = function (options) { + return this.getBaseStateClone() + .then( + function (ladState) { + const promises = Object.values(ladState).map( + this.updateObjectStateFromLAD.bind(this, options) + ); + + return Promise.all(promises).then(function () { + return ladState; + }); + }.bind(this) + ) + .then( + function (ladStates) { + return this.evaluateState(ladStates, options.domain); + }.bind(this) + ); + }; + + SummaryWidgetEvaluator.prototype.updateRules = function (domainObject) { + this.rules = domainObject.configuration.ruleOrder.map(function (ruleId) { + return new SummaryWidgetRule(domainObject.configuration.ruleConfigById[ruleId]); + }); + }; + + SummaryWidgetEvaluator.prototype.addChild = function (childObject) { + const childId = objectUtils.makeKeyString(childObject.identifier); + const metadata = this.openmct.telemetry.getMetadata(childObject); + const formats = this.openmct.telemetry.getFormatMap(metadata); + + this.baseState[childId] = { + id: childId, + domainObject: childObject, + metadata: metadata, + formats: formats }; + }; - return SummaryWidgetEvaluator; + SummaryWidgetEvaluator.prototype.removeChild = function (childObject) { + const childId = objectUtils.makeKeyString(childObject.identifier); + delete this.baseState[childId]; + }; + SummaryWidgetEvaluator.prototype.load = function () { + return this.loadPromise; + }; + + /** + * Return a promise for a 2-deep clone of the base state object: object + * states are shallow cloned, and then assembled and returned as a new base + * state. Allows object states to be mutated while sharing telemetry + * metadata and formats. + */ + SummaryWidgetEvaluator.prototype.getBaseStateClone = function () { + return this.load().then( + function () { + /* eslint-disable you-dont-need-lodash-underscore/values */ + return _(this.baseState).values().map(_.clone).keyBy('id').value(); + /* eslint-enable you-dont-need-lodash-underscore/values */ + }.bind(this) + ); + }; + + /** + * Subscribes to realtime updates for a given objectState, and invokes + * the supplied callback when objectState has been updated. Returns + * a function to unsubscribe. + * @private. + */ + SummaryWidgetEvaluator.prototype.subscribeToObjectState = function (callback, objectState) { + return this.openmct.telemetry.subscribe( + objectState.domainObject, + function (datum) { + objectState.lastDatum = datum; + objectState.timestamps = this.getTimestamps(objectState.id, datum); + callback(); + }.bind(this) + ); + }; + + /** + * Given an object state, will return a promise that is resolved when the + * object state has been updated from the LAD. + * @private. + */ + SummaryWidgetEvaluator.prototype.updateObjectStateFromLAD = function (options, objectState) { + options = Object.assign({}, options, { + strategy: 'latest', + size: 1 + }); + + return this.openmct.telemetry.request(objectState.domainObject, options).then( + function (results) { + objectState.lastDatum = results[results.length - 1]; + objectState.timestamps = this.getTimestamps(objectState.id, objectState.lastDatum); + }.bind(this) + ); + }; + + /** + * Returns an object containing all domain values in a datum. + * @private. + */ + SummaryWidgetEvaluator.prototype.getTimestamps = function (childId, datum) { + const timestampedDatum = {}; + this.openmct.time.getAllTimeSystems().forEach(function (timeSystem) { + timestampedDatum[timeSystem.key] = + this.baseState[childId].formats[timeSystem.key].parse(datum); + }, this); + + return timestampedDatum; + }; + + /** + * Given a base datum(containing timestamps) and rule index, adds values + * from the matching rule. + * @private + */ + SummaryWidgetEvaluator.prototype.makeDatumFromRule = function (ruleIndex, baseDatum) { + const rule = this.rules[ruleIndex]; + + baseDatum.ruleLabel = rule.label; + baseDatum.ruleName = rule.name; + baseDatum.message = rule.message; + baseDatum.ruleIndex = ruleIndex; + baseDatum.backgroundColor = rule.style['background-color']; + baseDatum.textColor = rule.style.color; + baseDatum.borderColor = rule.style['border-color']; + baseDatum.icon = rule.icon; + + return baseDatum; + }; + + /** + * Evaluate a `state` object and return a summary widget telemetry datum. + * Datum timestamps will be taken from the "latest" datum in the `state` + * where "latest" is the datum with the largest value for the given + * `timestampKey`. + * @private. + */ + SummaryWidgetEvaluator.prototype.evaluateState = function (state, timestampKey) { + const hasRequiredData = Object.keys(state).reduce(function (itDoes, k) { + return itDoes && state[k].lastDatum; + }, true); + if (!hasRequiredData) { + return; + } + + let i; + for (i = this.rules.length - 1; i > 0; i--) { + if (this.rules[i].evaluate(state, false)) { + break; + } + } + + /* eslint-disable you-dont-need-lodash-underscore/map */ + let latestTimestamp = _(state).map('timestamps').sortBy(timestampKey).last(); + /* eslint-enable you-dont-need-lodash-underscore/map */ + + if (!latestTimestamp) { + latestTimestamp = {}; + } + + const baseDatum = _.clone(latestTimestamp); + + return this.makeDatumFromRule(i, baseDatum); + }; + + /** + * remove all listeners and clean up any resources. + */ + SummaryWidgetEvaluator.prototype.destroy = function () { + this.stopListening(); + this.removeObserver(); + }; + + return SummaryWidgetEvaluator; }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js index 813f559f42..796e60cb8b 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js @@ -20,100 +20,94 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ +define([], function () { + function SummaryWidgetMetadataProvider(openmct) { + this.openmct = openmct; + } -], function ( + SummaryWidgetMetadataProvider.prototype.supportsMetadata = function (domainObject) { + return domainObject.type === 'summary-widget'; + }; -) { + SummaryWidgetMetadataProvider.prototype.getDomains = function (domainObject) { + return this.openmct.time.getAllTimeSystems().map(function (ts, i) { + return { + key: ts.key, + name: ts.name, + format: ts.timeFormat, + hints: { + domain: i + } + }; + }); + }; - function SummaryWidgetMetadataProvider(openmct) { - this.openmct = openmct; - } - - SummaryWidgetMetadataProvider.prototype.supportsMetadata = function (domainObject) { - return domainObject.type === 'summary-widget'; - }; - - SummaryWidgetMetadataProvider.prototype.getDomains = function (domainObject) { - return this.openmct.time.getAllTimeSystems().map(function (ts, i) { - return { - key: ts.key, - name: ts.name, - format: ts.timeFormat, - hints: { - domain: i - } - }; - }); - }; - - SummaryWidgetMetadataProvider.prototype.getMetadata = function (domainObject) { - const ruleOrder = domainObject.configuration.ruleOrder || []; - const enumerations = ruleOrder - .filter(function (ruleId) { - return Boolean(domainObject.configuration.ruleConfigById[ruleId]); - }) - .map(function (ruleId, ruleIndex) { - return { - string: domainObject.configuration.ruleConfigById[ruleId].label, - value: ruleIndex - }; - }); - - const metadata = { - // Generally safe assumption is that we have one domain per timeSystem. - values: this.getDomains().concat([ - { - name: 'State', - key: 'state', - source: 'ruleIndex', - format: 'enum', - enumerations: enumerations, - hints: { - range: 1 - } - }, - { - name: 'Rule Label', - key: 'ruleLabel', - format: 'string' - }, - { - name: 'Rule Name', - key: 'ruleName', - format: 'string' - }, - { - name: 'Message', - key: 'message', - format: 'string' - }, - { - name: 'Background Color', - key: 'backgroundColor', - format: 'string' - }, - { - name: 'Text Color', - key: 'textColor', - format: 'string' - }, - { - name: 'Border Color', - key: 'borderColor', - format: 'string' - }, - { - name: 'Display Icon', - key: 'icon', - format: 'string' - } - ]) + SummaryWidgetMetadataProvider.prototype.getMetadata = function (domainObject) { + const ruleOrder = domainObject.configuration.ruleOrder || []; + const enumerations = ruleOrder + .filter(function (ruleId) { + return Boolean(domainObject.configuration.ruleConfigById[ruleId]); + }) + .map(function (ruleId, ruleIndex) { + return { + string: domainObject.configuration.ruleConfigById[ruleId].label, + value: ruleIndex }; + }); - return metadata; + const metadata = { + // Generally safe assumption is that we have one domain per timeSystem. + values: this.getDomains().concat([ + { + name: 'State', + key: 'state', + source: 'ruleIndex', + format: 'enum', + enumerations: enumerations, + hints: { + range: 1 + } + }, + { + name: 'Rule Label', + key: 'ruleLabel', + format: 'string' + }, + { + name: 'Rule Name', + key: 'ruleName', + format: 'string' + }, + { + name: 'Message', + key: 'message', + format: 'string' + }, + { + name: 'Background Color', + key: 'backgroundColor', + format: 'string' + }, + { + name: 'Text Color', + key: 'textColor', + format: 'string' + }, + { + name: 'Border Color', + key: 'borderColor', + format: 'string' + }, + { + name: 'Display Icon', + key: 'icon', + format: 'string' + } + ]) }; - return SummaryWidgetMetadataProvider; + return metadata; + }; + return SummaryWidgetMetadataProvider; }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js index dae51ec654..f611506986 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js @@ -20,56 +20,51 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetCondition' -], function ( - SummaryWidgetCondition -) { - function SummaryWidgetRule(definition) { - this.name = definition.name; - this.label = definition.label; - this.id = definition.id; - this.icon = definition.icon; - this.style = definition.style; - this.message = definition.message; - this.description = definition.description; - this.conditions = definition.conditions.map(function (cDefinition) { - return new SummaryWidgetCondition(cDefinition); - }); - this.trigger = definition.trigger; - } +define(['./SummaryWidgetCondition'], function (SummaryWidgetCondition) { + function SummaryWidgetRule(definition) { + this.name = definition.name; + this.label = definition.label; + this.id = definition.id; + this.icon = definition.icon; + this.style = definition.style; + this.message = definition.message; + this.description = definition.description; + this.conditions = definition.conditions.map(function (cDefinition) { + return new SummaryWidgetCondition(cDefinition); + }); + this.trigger = definition.trigger; + } - /** - * Evaluate the given rule against a telemetryState and return true if it - * matches. - */ - SummaryWidgetRule.prototype.evaluate = function (telemetryState) { - let i; - let result; + /** + * Evaluate the given rule against a telemetryState and return true if it + * matches. + */ + SummaryWidgetRule.prototype.evaluate = function (telemetryState) { + let i; + let result; - if (this.trigger === 'all') { - for (i = 0; i < this.conditions.length; i++) { - result = this.conditions[i].evaluate(telemetryState); - if (!result) { - return false; - } - } - - return true; - } else if (this.trigger === 'any') { - for (i = 0; i < this.conditions.length; i++) { - result = this.conditions[i].evaluate(telemetryState); - if (result) { - return true; - } - } - - return false; - } else { - throw new Error('Invalid rule trigger: ' + this.trigger); + if (this.trigger === 'all') { + for (i = 0; i < this.conditions.length; i++) { + result = this.conditions[i].evaluate(telemetryState); + if (!result) { + return false; } - }; + } - return SummaryWidgetRule; + return true; + } else if (this.trigger === 'any') { + for (i = 0; i < this.conditions.length; i++) { + result = this.conditions[i].evaluate(telemetryState); + if (result) { + return true; + } + } + + return false; + } else { + throw new Error('Invalid rule trigger: ' + this.trigger); + } + }; + + return SummaryWidgetRule; }); - diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js index 1570893540..5f740bca9f 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js @@ -20,144 +20,134 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetRule' -], function ( - SummaryWidgetRule -) { - describe('SummaryWidgetRule', function () { +define(['./SummaryWidgetRule'], function (SummaryWidgetRule) { + describe('SummaryWidgetRule', function () { + let rule; + let telemetryState; - let rule; - let telemetryState; + beforeEach(function () { + const formatMap = { + raw: { + parse: function (datum) { + return datum.value; + } + } + }; - beforeEach(function () { - const formatMap = { - raw: { - parse: function (datum) { - return datum.value; - } - } - }; - - telemetryState = { - objectId: { - formats: formatMap, - lastDatum: { - } - }, - otherObjectId: { - formats: formatMap, - lastDatum: { - } - } - }; - }); - - it('allows single condition rules with any', function () { - rule = new SummaryWidgetRule({ - trigger: 'any', - conditions: [{ - object: 'objectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }] - }); - - telemetryState.objectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - expect(rule.evaluate(telemetryState)).toBe(true); - }); - - it('allows single condition rules with all', function () { - rule = new SummaryWidgetRule({ - trigger: 'all', - conditions: [{ - object: 'objectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }] - }); - - telemetryState.objectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - expect(rule.evaluate(telemetryState)).toBe(true); - }); - - it('can combine multiple conditions with all', function () { - rule = new SummaryWidgetRule({ - trigger: 'all', - conditions: [{ - object: 'objectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }, { - object: 'otherObjectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 20 - ] - }] - }); - - telemetryState.objectId.lastDatum.value = 5; - telemetryState.otherObjectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 5; - telemetryState.otherObjectId.lastDatum.value = 25; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 25; - expect(rule.evaluate(telemetryState)).toBe(true); - - }); - - it('can combine multiple conditions with any', function () { - rule = new SummaryWidgetRule({ - trigger: 'any', - conditions: [{ - object: 'objectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }, { - object: 'otherObjectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 20 - ] - }] - }); - - telemetryState.objectId.lastDatum.value = 5; - telemetryState.otherObjectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 5; - telemetryState.otherObjectId.lastDatum.value = 25; - expect(rule.evaluate(telemetryState)).toBe(true); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(true); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 25; - expect(rule.evaluate(telemetryState)).toBe(true); - }); + telemetryState = { + objectId: { + formats: formatMap, + lastDatum: {} + }, + otherObjectId: { + formats: formatMap, + lastDatum: {} + } + }; }); + + it('allows single condition rules with any', function () { + rule = new SummaryWidgetRule({ + trigger: 'any', + conditions: [ + { + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [10] + } + ] + }); + + telemetryState.objectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + expect(rule.evaluate(telemetryState)).toBe(true); + }); + + it('allows single condition rules with all', function () { + rule = new SummaryWidgetRule({ + trigger: 'all', + conditions: [ + { + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [10] + } + ] + }); + + telemetryState.objectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + expect(rule.evaluate(telemetryState)).toBe(true); + }); + + it('can combine multiple conditions with all', function () { + rule = new SummaryWidgetRule({ + trigger: 'all', + conditions: [ + { + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [10] + }, + { + object: 'otherObjectId', + key: 'raw', + operation: 'greaterThan', + values: [20] + } + ] + }); + + telemetryState.objectId.lastDatum.value = 5; + telemetryState.otherObjectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 5; + telemetryState.otherObjectId.lastDatum.value = 25; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 25; + expect(rule.evaluate(telemetryState)).toBe(true); + }); + + it('can combine multiple conditions with any', function () { + rule = new SummaryWidgetRule({ + trigger: 'any', + conditions: [ + { + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [10] + }, + { + object: 'otherObjectId', + key: 'raw', + operation: 'greaterThan', + values: [20] + } + ] + }); + + telemetryState.objectId.lastDatum.value = 5; + telemetryState.otherObjectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 5; + telemetryState.otherObjectId.lastDatum.value = 25; + expect(rule.evaluate(telemetryState)).toBe(true); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(true); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 25; + expect(rule.evaluate(telemetryState)).toBe(true); + }); + }); }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js index 376b7ba379..13a3737360 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js @@ -20,48 +20,44 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './EvaluatorPool' -], function ( - EvaluatorPool -) { +define(['./EvaluatorPool'], function (EvaluatorPool) { + function SummaryWidgetTelemetryProvider(openmct) { + this.pool = new EvaluatorPool(openmct); + } - function SummaryWidgetTelemetryProvider(openmct) { - this.pool = new EvaluatorPool(openmct); + SummaryWidgetTelemetryProvider.prototype.supportsRequest = function (domainObject, options) { + return domainObject.type === 'summary-widget'; + }; + + SummaryWidgetTelemetryProvider.prototype.request = function (domainObject, options) { + if (options.strategy !== 'latest' && options.size !== 1) { + return Promise.resolve([]); } - SummaryWidgetTelemetryProvider.prototype.supportsRequest = function (domainObject, options) { - return domainObject.type === 'summary-widget'; - }; + const evaluator = this.pool.get(domainObject); - SummaryWidgetTelemetryProvider.prototype.request = function (domainObject, options) { - if (options.strategy !== 'latest' && options.size !== 1) { - return Promise.resolve([]); - } + return evaluator.requestLatest(options).then( + function (latestDatum) { + this.pool.release(evaluator); - const evaluator = this.pool.get(domainObject); + return latestDatum ? [latestDatum] : []; + }.bind(this) + ); + }; - return evaluator.requestLatest(options) - .then(function (latestDatum) { - this.pool.release(evaluator); + SummaryWidgetTelemetryProvider.prototype.supportsSubscribe = function (domainObject) { + return domainObject.type === 'summary-widget'; + }; - return latestDatum ? [latestDatum] : []; - }.bind(this)); - }; + SummaryWidgetTelemetryProvider.prototype.subscribe = function (domainObject, callback) { + const evaluator = this.pool.get(domainObject); + const unsubscribe = evaluator.subscribe(callback); - SummaryWidgetTelemetryProvider.prototype.supportsSubscribe = function (domainObject) { - return domainObject.type === 'summary-widget'; - }; + return function () { + this.pool.release(evaluator); + unsubscribe(); + }.bind(this); + }; - SummaryWidgetTelemetryProvider.prototype.subscribe = function (domainObject, callback) { - const evaluator = this.pool.get(domainObject); - const unsubscribe = evaluator.subscribe(callback); - - return function () { - this.pool.release(evaluator); - unsubscribe(); - }.bind(this); - }; - - return SummaryWidgetTelemetryProvider; + return SummaryWidgetTelemetryProvider; }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js index e563065fa8..ec7853f130 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js @@ -20,462 +20,444 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetTelemetryProvider' -], function ( - SummaryWidgetTelemetryProvider -) { +define(['./SummaryWidgetTelemetryProvider'], function (SummaryWidgetTelemetryProvider) { + xdescribe('SummaryWidgetTelemetryProvider', function () { + let telemObjectA; + let telemObjectB; + let summaryWidgetObject; + let openmct; + let telemUnsubscribes; + let unobserver; + let composition; + let telemetryProvider; + let loader; - xdescribe('SummaryWidgetTelemetryProvider', function () { - let telemObjectA; - let telemObjectB; - let summaryWidgetObject; - let openmct; - let telemUnsubscribes; - let unobserver; - let composition; - let telemetryProvider; - let loader; - - beforeEach(function () { - telemObjectA = { - identifier: { - namespace: 'a', - key: 'telem' + beforeEach(function () { + telemObjectA = { + identifier: { + namespace: 'a', + key: 'telem' + } + }; + telemObjectB = { + identifier: { + namespace: 'b', + key: 'telem' + } + }; + summaryWidgetObject = { + name: 'Summary Widget', + type: 'summary-widget', + identifier: { + namespace: 'base', + key: 'widgetId' + }, + composition: ['a:telem', 'b:telem'], + configuration: { + ruleOrder: ['default', 'rule0', 'rule1'], + ruleConfigById: { + default: { + name: 'safe', + label: "Don't Worry", + message: "It's Ok", + id: 'default', + icon: 'a-ok', + style: { + color: '#ffffff', + 'background-color': '#38761d', + 'border-color': 'rgba(0,0,0,0)' + }, + conditions: [ + { + object: '', + key: '', + operation: '', + values: [] } - }; - telemObjectB = { - identifier: { - namespace: 'b', - key: 'telem' + ], + trigger: 'any' + }, + rule0: { + name: 'A High', + label: 'Start Worrying', + message: 'A is a little high...', + id: 'rule0', + icon: 'a-high', + style: { + color: '#000000', + 'background-color': '#ffff00', + 'border-color': 'rgba(1,1,0,0)' + }, + conditions: [ + { + object: 'a:telem', + key: 'measurement', + operation: 'greaterThan', + values: [50] } - }; - summaryWidgetObject = { - name: "Summary Widget", - type: "summary-widget", - identifier: { - namespace: 'base', - key: 'widgetId' - }, - composition: [ - 'a:telem', - 'b:telem' - ], - configuration: { - ruleOrder: [ - "default", - "rule0", - "rule1" - ], - ruleConfigById: { - "default": { - name: "safe", - label: "Don't Worry", - message: "It's Ok", - id: "default", - icon: "a-ok", - style: { - "color": "#ffffff", - "background-color": "#38761d", - "border-color": "rgba(0,0,0,0)" - }, - conditions: [ - { - object: "", - key: "", - operation: "", - values: [] - } - ], - trigger: "any" - }, - "rule0": { - name: "A High", - label: "Start Worrying", - message: "A is a little high...", - id: "rule0", - icon: "a-high", - style: { - "color": "#000000", - "background-color": "#ffff00", - "border-color": "rgba(1,1,0,0)" - }, - conditions: [ - { - object: "a:telem", - key: "measurement", - operation: "greaterThan", - values: [ - 50 - ] - } - ], - trigger: "any" - }, - rule1: { - name: "B Low", - label: "WORRY!", - message: "B is Low", - id: "rule1", - icon: "b-low", - style: { - "color": "#ff00ff", - "background-color": "#ff0000", - "border-color": "rgba(1,0,0,0)" - }, - conditions: [ - { - object: "b:telem", - key: "measurement", - operation: "lessThan", - values: [ - 10 - ] - } - ], - trigger: "any" - } - } + ], + trigger: 'any' + }, + rule1: { + name: 'B Low', + label: 'WORRY!', + message: 'B is Low', + id: 'rule1', + icon: 'b-low', + style: { + color: '#ff00ff', + 'background-color': '#ff0000', + 'border-color': 'rgba(1,0,0,0)' + }, + conditions: [ + { + object: 'b:telem', + key: 'measurement', + operation: 'lessThan', + values: [10] } - }; - openmct = { - objects: jasmine.createSpyObj('objectAPI', [ - 'get', - 'observe' - ]), - telemetry: jasmine.createSpyObj('telemetryAPI', [ - 'getMetadata', - 'getFormatMap', - 'request', - 'subscribe', - 'addProvider' - ]), - composition: jasmine.createSpyObj('compositionAPI', [ - 'get' - ]), - time: jasmine.createSpyObj('timeAPI', [ - 'getAllTimeSystems', - 'timeSystem' - ]) - }; - - openmct.time.getAllTimeSystems.and.returnValue([{key: 'timestamp'}]); - openmct.time.timeSystem.and.returnValue({key: 'timestamp'}); - - unobserver = jasmine.createSpy('unobserver'); - openmct.objects.observe.and.returnValue(unobserver); - - composition = jasmine.createSpyObj('compositionCollection', [ - 'on', - 'off', - 'load' - ]); - - function notify(eventName, a, b) { - composition.on.calls.all().filter(function (c) { - return c.args[0] === eventName; - }).forEach(function (c) { - if (c.args[2]) { // listener w/ context. - c.args[1].call(c.args[2], a, b); - } else { // listener w/o context. - c.args[1](a, b); - } - }); + ], + trigger: 'any' } + } + } + }; + openmct = { + objects: jasmine.createSpyObj('objectAPI', ['get', 'observe']), + telemetry: jasmine.createSpyObj('telemetryAPI', [ + 'getMetadata', + 'getFormatMap', + 'request', + 'subscribe', + 'addProvider' + ]), + composition: jasmine.createSpyObj('compositionAPI', ['get']), + time: jasmine.createSpyObj('timeAPI', ['getAllTimeSystems', 'timeSystem']) + }; - loader = {}; - loader.promise = new Promise(function (resolve, reject) { - loader.resolve = resolve; - loader.reject = reject; + openmct.time.getAllTimeSystems.and.returnValue([{ key: 'timestamp' }]); + openmct.time.timeSystem.and.returnValue({ key: 'timestamp' }); + + unobserver = jasmine.createSpy('unobserver'); + openmct.objects.observe.and.returnValue(unobserver); + + composition = jasmine.createSpyObj('compositionCollection', ['on', 'off', 'load']); + + function notify(eventName, a, b) { + composition.on.calls + .all() + .filter(function (c) { + return c.args[0] === eventName; + }) + .forEach(function (c) { + if (c.args[2]) { + // listener w/ context. + c.args[1].call(c.args[2], a, b); + } else { + // listener w/o context. + c.args[1](a, b); + } + }); + } + + loader = {}; + loader.promise = new Promise(function (resolve, reject) { + loader.resolve = resolve; + loader.reject = reject; + }); + + composition.load.and.callFake(function () { + setTimeout(function () { + notify('add', telemObjectA); + setTimeout(function () { + notify('add', telemObjectB); + setTimeout(function () { + loader.resolve(); }); - - composition.load.and.callFake(function () { - setTimeout(function () { - notify('add', telemObjectA); - setTimeout(function () { - notify('add', telemObjectB); - setTimeout(function () { - loader.resolve(); - }); - }); - }); - - return loader.promise; - }); - openmct.composition.get.and.returnValue(composition); - - telemUnsubscribes = []; - openmct.telemetry.subscribe.and.callFake(function () { - const unsubscriber = jasmine.createSpy('unsubscriber' + telemUnsubscribes.length); - telemUnsubscribes.push(unsubscriber); - - return unsubscriber; - }); - - openmct.telemetry.getMetadata.and.callFake(function (object) { - return { - name: 'fake metadata manager', - object: object, - keys: ['timestamp', 'measurement'] - }; - }); - - openmct.telemetry.getFormatMap.and.callFake(function (metadata) { - expect(metadata.name).toBe('fake metadata manager'); - - return { - metadata: metadata, - timestamp: { - parse: function (datum) { - return datum.t; - } - }, - measurement: { - parse: function (datum) { - return datum.m; - } - } - }; - }); - telemetryProvider = new SummaryWidgetTelemetryProvider(openmct); + }); }); - it("supports subscription for summary widgets", function () { - expect(telemetryProvider.supportsSubscribe(summaryWidgetObject)) - .toBe(true); - }); + return loader.promise; + }); + openmct.composition.get.and.returnValue(composition); - it("supports requests for summary widgets", function () { - expect(telemetryProvider.supportsRequest(summaryWidgetObject)) - .toBe(true); - }); + telemUnsubscribes = []; + openmct.telemetry.subscribe.and.callFake(function () { + const unsubscriber = jasmine.createSpy('unsubscriber' + telemUnsubscribes.length); + telemUnsubscribes.push(unsubscriber); - it("does not support other requests or subscriptions", function () { - expect(telemetryProvider.supportsSubscribe(telemObjectA)) - .toBe(false); - expect(telemetryProvider.supportsRequest(telemObjectA)) - .toBe(false); - }); + return unsubscriber; + }); - it("Returns no results for basic requests", function () { - return telemetryProvider.request(summaryWidgetObject, {}) - .then(function (result) { - expect(result).toEqual([]); - }); - }); + openmct.telemetry.getMetadata.and.callFake(function (object) { + return { + name: 'fake metadata manager', + object: object, + keys: ['timestamp', 'measurement'] + }; + }); - it('provides realtime telemetry', function () { - const callback = jasmine.createSpy('callback'); - telemetryProvider.subscribe(summaryWidgetObject, callback); - - return loader.promise.then(function () { - return new Promise(function (resolve) { - setTimeout(resolve); - }); - }).then(function () { - expect(openmct.telemetry.subscribe.calls.count()).toBe(2); - expect(openmct.telemetry.subscribe) - .toHaveBeenCalledWith(telemObjectA, jasmine.any(Function)); - expect(openmct.telemetry.subscribe) - .toHaveBeenCalledWith(telemObjectB, jasmine.any(Function)); - - const aCallback = openmct.telemetry.subscribe.calls.all()[0].args[1]; - const bCallback = openmct.telemetry.subscribe.calls.all()[1].args[1]; - - aCallback({ - t: 123, - m: 25 - }); - expect(callback).not.toHaveBeenCalled(); - bCallback({ - t: 123, - m: 25 - }); - expect(callback).toHaveBeenCalledWith({ - timestamp: 123, - ruleLabel: "Don't Worry", - ruleName: "safe", - message: "It's Ok", - ruleIndex: 0, - backgroundColor: '#38761d', - textColor: '#ffffff', - borderColor: 'rgba(0,0,0,0)', - icon: 'a-ok' - }); - - aCallback({ - t: 140, - m: 55 - }); - expect(callback).toHaveBeenCalledWith({ - timestamp: 140, - ruleLabel: "Start Worrying", - ruleName: "A High", - message: "A is a little high...", - ruleIndex: 1, - backgroundColor: '#ffff00', - textColor: '#000000', - borderColor: 'rgba(1,1,0,0)', - icon: 'a-high' - }); - - bCallback({ - t: 140, - m: -10 - }); - expect(callback).toHaveBeenCalledWith({ - timestamp: 140, - ruleLabel: "WORRY!", - ruleName: "B Low", - message: "B is Low", - ruleIndex: 2, - backgroundColor: '#ff0000', - textColor: '#ff00ff', - borderColor: 'rgba(1,0,0,0)', - icon: 'b-low' - }); - - aCallback({ - t: 160, - m: 25 - }); - expect(callback).toHaveBeenCalledWith({ - timestamp: 160, - ruleLabel: "WORRY!", - ruleName: "B Low", - message: "B is Low", - ruleIndex: 2, - backgroundColor: '#ff0000', - textColor: '#ff00ff', - borderColor: 'rgba(1,0,0,0)', - icon: 'b-low' - }); - - bCallback({ - t: 160, - m: 25 - }); - expect(callback).toHaveBeenCalledWith({ - timestamp: 160, - ruleLabel: "Don't Worry", - ruleName: "safe", - message: "It's Ok", - ruleIndex: 0, - backgroundColor: '#38761d', - textColor: '#ffffff', - borderColor: 'rgba(0,0,0,0)', - icon: 'a-ok' - }); - }); - }); - - describe('providing lad telemetry', function () { - let responseDatums; - let resultsShouldBe; - - beforeEach(function () { - openmct.telemetry.request.and.callFake(function (rObj, options) { - expect(rObj).toEqual(jasmine.any(Object)); - expect(options).toEqual({ - size: 1, - strategy: 'latest', - domain: 'timestamp' - }); - expect(responseDatums[rObj.identifier.namespace]).toBeDefined(); - - return Promise.resolve([responseDatums[rObj.identifier.namespace]]); - }); - responseDatums = {}; - - resultsShouldBe = function (results) { - return telemetryProvider - .request(summaryWidgetObject, { - size: 1, - strategy: 'latest', - domain: 'timestamp' - }) - .then(function (r) { - expect(r).toEqual(results); - }); - }; - }); - - it("returns default when no rule matches", function () { - responseDatums = { - a: { - t: 122, - m: 25 - }, - b: { - t: 111, - m: 25 - } - }; - - return resultsShouldBe([{ - timestamp: 122, - ruleLabel: "Don't Worry", - ruleName: "safe", - message: "It's Ok", - ruleIndex: 0, - backgroundColor: '#38761d', - textColor: '#ffffff', - borderColor: 'rgba(0,0,0,0)', - icon: 'a-ok' - }]); - }); - - it("returns highest priority when multiple match", function () { - responseDatums = { - a: { - t: 131, - m: 55 - }, - b: { - t: 139, - m: 5 - } - }; - - return resultsShouldBe([{ - timestamp: 139, - ruleLabel: "WORRY!", - ruleName: "B Low", - message: "B is Low", - ruleIndex: 2, - backgroundColor: '#ff0000', - textColor: '#ff00ff', - borderColor: 'rgba(1,0,0,0)', - icon: 'b-low' - }]); - }); - - it("returns matching rule", function () { - responseDatums = { - a: { - t: 144, - m: 55 - }, - b: { - t: 141, - m: 15 - } - }; - - return resultsShouldBe([{ - timestamp: 144, - ruleLabel: "Start Worrying", - ruleName: "A High", - message: "A is a little high...", - ruleIndex: 1, - backgroundColor: '#ffff00', - textColor: '#000000', - borderColor: 'rgba(1,1,0,0)', - icon: 'a-high' - }]); - }); - - }); + openmct.telemetry.getFormatMap.and.callFake(function (metadata) { + expect(metadata.name).toBe('fake metadata manager'); + return { + metadata: metadata, + timestamp: { + parse: function (datum) { + return datum.t; + } + }, + measurement: { + parse: function (datum) { + return datum.m; + } + } + }; + }); + telemetryProvider = new SummaryWidgetTelemetryProvider(openmct); }); + + it('supports subscription for summary widgets', function () { + expect(telemetryProvider.supportsSubscribe(summaryWidgetObject)).toBe(true); + }); + + it('supports requests for summary widgets', function () { + expect(telemetryProvider.supportsRequest(summaryWidgetObject)).toBe(true); + }); + + it('does not support other requests or subscriptions', function () { + expect(telemetryProvider.supportsSubscribe(telemObjectA)).toBe(false); + expect(telemetryProvider.supportsRequest(telemObjectA)).toBe(false); + }); + + it('Returns no results for basic requests', function () { + return telemetryProvider.request(summaryWidgetObject, {}).then(function (result) { + expect(result).toEqual([]); + }); + }); + + it('provides realtime telemetry', function () { + const callback = jasmine.createSpy('callback'); + telemetryProvider.subscribe(summaryWidgetObject, callback); + + return loader.promise + .then(function () { + return new Promise(function (resolve) { + setTimeout(resolve); + }); + }) + .then(function () { + expect(openmct.telemetry.subscribe.calls.count()).toBe(2); + expect(openmct.telemetry.subscribe).toHaveBeenCalledWith( + telemObjectA, + jasmine.any(Function) + ); + expect(openmct.telemetry.subscribe).toHaveBeenCalledWith( + telemObjectB, + jasmine.any(Function) + ); + + const aCallback = openmct.telemetry.subscribe.calls.all()[0].args[1]; + const bCallback = openmct.telemetry.subscribe.calls.all()[1].args[1]; + + aCallback({ + t: 123, + m: 25 + }); + expect(callback).not.toHaveBeenCalled(); + bCallback({ + t: 123, + m: 25 + }); + expect(callback).toHaveBeenCalledWith({ + timestamp: 123, + ruleLabel: "Don't Worry", + ruleName: 'safe', + message: "It's Ok", + ruleIndex: 0, + backgroundColor: '#38761d', + textColor: '#ffffff', + borderColor: 'rgba(0,0,0,0)', + icon: 'a-ok' + }); + + aCallback({ + t: 140, + m: 55 + }); + expect(callback).toHaveBeenCalledWith({ + timestamp: 140, + ruleLabel: 'Start Worrying', + ruleName: 'A High', + message: 'A is a little high...', + ruleIndex: 1, + backgroundColor: '#ffff00', + textColor: '#000000', + borderColor: 'rgba(1,1,0,0)', + icon: 'a-high' + }); + + bCallback({ + t: 140, + m: -10 + }); + expect(callback).toHaveBeenCalledWith({ + timestamp: 140, + ruleLabel: 'WORRY!', + ruleName: 'B Low', + message: 'B is Low', + ruleIndex: 2, + backgroundColor: '#ff0000', + textColor: '#ff00ff', + borderColor: 'rgba(1,0,0,0)', + icon: 'b-low' + }); + + aCallback({ + t: 160, + m: 25 + }); + expect(callback).toHaveBeenCalledWith({ + timestamp: 160, + ruleLabel: 'WORRY!', + ruleName: 'B Low', + message: 'B is Low', + ruleIndex: 2, + backgroundColor: '#ff0000', + textColor: '#ff00ff', + borderColor: 'rgba(1,0,0,0)', + icon: 'b-low' + }); + + bCallback({ + t: 160, + m: 25 + }); + expect(callback).toHaveBeenCalledWith({ + timestamp: 160, + ruleLabel: "Don't Worry", + ruleName: 'safe', + message: "It's Ok", + ruleIndex: 0, + backgroundColor: '#38761d', + textColor: '#ffffff', + borderColor: 'rgba(0,0,0,0)', + icon: 'a-ok' + }); + }); + }); + + describe('providing lad telemetry', function () { + let responseDatums; + let resultsShouldBe; + + beforeEach(function () { + openmct.telemetry.request.and.callFake(function (rObj, options) { + expect(rObj).toEqual(jasmine.any(Object)); + expect(options).toEqual({ + size: 1, + strategy: 'latest', + domain: 'timestamp' + }); + expect(responseDatums[rObj.identifier.namespace]).toBeDefined(); + + return Promise.resolve([responseDatums[rObj.identifier.namespace]]); + }); + responseDatums = {}; + + resultsShouldBe = function (results) { + return telemetryProvider + .request(summaryWidgetObject, { + size: 1, + strategy: 'latest', + domain: 'timestamp' + }) + .then(function (r) { + expect(r).toEqual(results); + }); + }; + }); + + it('returns default when no rule matches', function () { + responseDatums = { + a: { + t: 122, + m: 25 + }, + b: { + t: 111, + m: 25 + } + }; + + return resultsShouldBe([ + { + timestamp: 122, + ruleLabel: "Don't Worry", + ruleName: 'safe', + message: "It's Ok", + ruleIndex: 0, + backgroundColor: '#38761d', + textColor: '#ffffff', + borderColor: 'rgba(0,0,0,0)', + icon: 'a-ok' + } + ]); + }); + + it('returns highest priority when multiple match', function () { + responseDatums = { + a: { + t: 131, + m: 55 + }, + b: { + t: 139, + m: 5 + } + }; + + return resultsShouldBe([ + { + timestamp: 139, + ruleLabel: 'WORRY!', + ruleName: 'B Low', + message: 'B is Low', + ruleIndex: 2, + backgroundColor: '#ff0000', + textColor: '#ff00ff', + borderColor: 'rgba(1,0,0,0)', + icon: 'b-low' + } + ]); + }); + + it('returns matching rule', function () { + responseDatums = { + a: { + t: 144, + m: 55 + }, + b: { + t: 141, + m: 15 + } + }; + + return resultsShouldBe([ + { + timestamp: 144, + ruleLabel: 'Start Worrying', + ruleName: 'A High', + message: 'A is a little high...', + ruleIndex: 1, + backgroundColor: '#ffff00', + textColor: '#000000', + borderColor: 'rgba(1,1,0,0)', + icon: 'a-high' + } + ]); + }); + }); + }); }); diff --git a/src/plugins/summaryWidget/src/telemetry/operations.js b/src/plugins/summaryWidget/src/telemetry/operations.js index 534a5b1d87..01f87c2179 100644 --- a/src/plugins/summaryWidget/src/telemetry/operations.js +++ b/src/plugins/summaryWidget/src/telemetry/operations.js @@ -20,200 +20,196 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ +define([], function () { + const OPERATIONS = { + equalTo: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'is equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' == ' + values[0]; + } + }, + notEqualTo: { + operation: function (input) { + return input[0] !== input[1]; + }, + text: 'is not equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' != ' + values[0]; + } + }, + greaterThan: { + operation: function (input) { + return input[0] > input[1]; + }, + text: 'is greater than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' > ' + values[0]; + } + }, + lessThan: { + operation: function (input) { + return input[0] < input[1]; + }, + text: 'is less than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' < ' + values[0]; + } + }, + greaterThanOrEq: { + operation: function (input) { + return input[0] >= input[1]; + }, + text: 'is greater than or equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' >= ' + values[0]; + } + }, + lessThanOrEq: { + operation: function (input) { + return input[0] <= input[1]; + }, + text: 'is less than or equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' <= ' + values[0]; + } + }, + between: { + operation: function (input) { + return input[0] > input[1] && input[0] < input[2]; + }, + text: 'is between', + appliesTo: ['number'], + inputCount: 2, + getDescription: function (values) { + return ' between ' + values[0] + ' and ' + values[1]; + } + }, + notBetween: { + operation: function (input) { + return input[0] < input[1] || input[0] > input[2]; + }, + text: 'is not between', + appliesTo: ['number'], + inputCount: 2, + getDescription: function (values) { + return ' not between ' + values[0] + ' and ' + values[1]; + } + }, + textContains: { + operation: function (input) { + return input[0] && input[1] && input[0].includes(input[1]); + }, + text: 'text contains', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' contains ' + values[0]; + } + }, + textDoesNotContain: { + operation: function (input) { + return input[0] && input[1] && !input[0].includes(input[1]); + }, + text: 'text does not contain', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' does not contain ' + values[0]; + } + }, + textStartsWith: { + operation: function (input) { + return input[0].startsWith(input[1]); + }, + text: 'text starts with', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' starts with ' + values[0]; + } + }, + textEndsWith: { + operation: function (input) { + return input[0].endsWith(input[1]); + }, + text: 'text ends with', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' ends with ' + values[0]; + } + }, + textIsExactly: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'text is exactly', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' is exactly ' + values[0]; + } + }, + isUndefined: { + operation: function (input) { + return typeof input[0] === 'undefined'; + }, + text: 'is undefined', + appliesTo: ['string', 'number', 'enum'], + inputCount: 0, + getDescription: function () { + return ' is undefined'; + } + }, + isDefined: { + operation: function (input) { + return typeof input[0] !== 'undefined'; + }, + text: 'is defined', + appliesTo: ['string', 'number', 'enum'], + inputCount: 0, + getDescription: function () { + return ' is defined'; + } + }, + enumValueIs: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'is', + appliesTo: ['enum'], + inputCount: 1, + getDescription: function (values) { + return ' == ' + values[0]; + } + }, + enumValueIsNot: { + operation: function (input) { + return input[0] !== input[1]; + }, + text: 'is not', + appliesTo: ['enum'], + inputCount: 1, + getDescription: function (values) { + return ' != ' + values[0]; + } + } + }; -], function ( - -) { - const OPERATIONS = { - equalTo: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'is equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' == ' + values[0]; - } - }, - notEqualTo: { - operation: function (input) { - return input[0] !== input[1]; - }, - text: 'is not equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' != ' + values[0]; - } - }, - greaterThan: { - operation: function (input) { - return input[0] > input[1]; - }, - text: 'is greater than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' > ' + values[0]; - } - }, - lessThan: { - operation: function (input) { - return input[0] < input[1]; - }, - text: 'is less than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' < ' + values[0]; - } - }, - greaterThanOrEq: { - operation: function (input) { - return input[0] >= input[1]; - }, - text: 'is greater than or equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' >= ' + values[0]; - } - }, - lessThanOrEq: { - operation: function (input) { - return input[0] <= input[1]; - }, - text: 'is less than or equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' <= ' + values[0]; - } - }, - between: { - operation: function (input) { - return input[0] > input[1] && input[0] < input[2]; - }, - text: 'is between', - appliesTo: ['number'], - inputCount: 2, - getDescription: function (values) { - return ' between ' + values[0] + ' and ' + values[1]; - } - }, - notBetween: { - operation: function (input) { - return input[0] < input[1] || input[0] > input[2]; - }, - text: 'is not between', - appliesTo: ['number'], - inputCount: 2, - getDescription: function (values) { - return ' not between ' + values[0] + ' and ' + values[1]; - } - }, - textContains: { - operation: function (input) { - return input[0] && input[1] && input[0].includes(input[1]); - }, - text: 'text contains', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' contains ' + values[0]; - } - }, - textDoesNotContain: { - operation: function (input) { - return input[0] && input[1] && !input[0].includes(input[1]); - }, - text: 'text does not contain', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' does not contain ' + values[0]; - } - }, - textStartsWith: { - operation: function (input) { - return input[0].startsWith(input[1]); - }, - text: 'text starts with', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' starts with ' + values[0]; - } - }, - textEndsWith: { - operation: function (input) { - return input[0].endsWith(input[1]); - }, - text: 'text ends with', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' ends with ' + values[0]; - } - }, - textIsExactly: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'text is exactly', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' is exactly ' + values[0]; - } - }, - isUndefined: { - operation: function (input) { - return typeof input[0] === 'undefined'; - }, - text: 'is undefined', - appliesTo: ['string', 'number', 'enum'], - inputCount: 0, - getDescription: function () { - return ' is undefined'; - } - }, - isDefined: { - operation: function (input) { - return typeof input[0] !== 'undefined'; - }, - text: 'is defined', - appliesTo: ['string', 'number', 'enum'], - inputCount: 0, - getDescription: function () { - return ' is defined'; - } - }, - enumValueIs: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'is', - appliesTo: ['enum'], - inputCount: 1, - getDescription: function (values) { - return ' == ' + values[0]; - } - }, - enumValueIsNot: { - operation: function (input) { - return input[0] !== input[1]; - }, - text: 'is not', - appliesTo: ['enum'], - inputCount: 1, - getDescription: function (values) { - return ' != ' + values[0]; - } - } - }; - - return OPERATIONS; + return OPERATIONS; }); diff --git a/src/plugins/summaryWidget/src/views/SummaryWidgetView.js b/src/plugins/summaryWidget/src/views/SummaryWidgetView.js index b59e521fd3..a3dff12703 100644 --- a/src/plugins/summaryWidget/src/views/SummaryWidgetView.js +++ b/src/plugins/summaryWidget/src/views/SummaryWidgetView.js @@ -1,103 +1,106 @@ -define([ - './summary-widget.html', - '@braintree/sanitize-url' -], function ( - summaryWidgetTemplate, - urlSanitizeLib +define(['./summary-widget.html', '@braintree/sanitize-url'], function ( + summaryWidgetTemplate, + urlSanitizeLib ) { - const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon'; + const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon'; - function SummaryWidgetView(domainObject, openmct) { - this.openmct = openmct; - this.domainObject = domainObject; - this.hasUpdated = false; - this.render = this.render.bind(this); + function SummaryWidgetView(domainObject, openmct) { + this.openmct = openmct; + this.domainObject = domainObject; + this.hasUpdated = false; + this.render = this.render.bind(this); + } + + SummaryWidgetView.prototype.updateState = function (datum) { + this.hasUpdated = true; + this.widget.style.color = datum.textColor; + this.widget.style.backgroundColor = datum.backgroundColor; + this.widget.style.borderColor = datum.borderColor; + this.widget.title = datum.message; + this.label.title = datum.message; + this.label.innerHTML = datum.ruleLabel; + this.icon.className = WIDGET_ICON_CLASS + ' ' + datum.icon; + }; + + SummaryWidgetView.prototype.render = function () { + if (this.unsubscribe) { + this.unsubscribe(); } - SummaryWidgetView.prototype.updateState = function (datum) { - this.hasUpdated = true; - this.widget.style.color = datum.textColor; - this.widget.style.backgroundColor = datum.backgroundColor; - this.widget.style.borderColor = datum.borderColor; - this.widget.title = datum.message; - this.label.title = datum.message; - this.label.innerHTML = datum.ruleLabel; - this.icon.className = WIDGET_ICON_CLASS + ' ' + datum.icon; - }; + this.hasUpdated = false; - SummaryWidgetView.prototype.render = function () { - if (this.unsubscribe) { - this.unsubscribe(); - } + this.container.innerHTML = summaryWidgetTemplate; + this.widget = this.container.querySelector('a'); + this.icon = this.container.querySelector('#widgetIcon'); + this.label = this.container.querySelector('.js-sw__label'); - this.hasUpdated = false; + let url = this.domainObject.url; + if (url) { + this.widget.setAttribute('href', urlSanitizeLib.sanitizeUrl(url)); + } else { + this.widget.removeAttribute('href'); + } - this.container.innerHTML = summaryWidgetTemplate; - this.widget = this.container.querySelector('a'); - this.icon = this.container.querySelector('#widgetIcon'); - this.label = this.container.querySelector('.js-sw__label'); + if (this.domainObject.openNewTab === 'newTab') { + this.widget.setAttribute('target', '_blank'); + } else { + this.widget.removeAttribute('target'); + } - let url = this.domainObject.url; - if (url) { - this.widget.setAttribute('href', urlSanitizeLib.sanitizeUrl(url)); - } else { - this.widget.removeAttribute('href'); - } + const renderTracker = {}; + this.renderTracker = renderTracker; + this.openmct.telemetry + .request(this.domainObject, { + strategy: 'latest', + size: 1 + }) + .then( + function (results) { + if ( + this.destroyed || + this.hasUpdated || + this.renderTracker !== renderTracker || + results.length === 0 + ) { + return; + } - if (this.domainObject.openNewTab === 'newTab') { - this.widget.setAttribute('target', '_blank'); - } else { - this.widget.removeAttribute('target'); - } + this.updateState(results[results.length - 1]); + }.bind(this) + ); - const renderTracker = {}; - this.renderTracker = renderTracker; - this.openmct.telemetry.request(this.domainObject, { - strategy: 'latest', - size: 1 - }).then(function (results) { - if (this.destroyed - || this.hasUpdated - || this.renderTracker !== renderTracker - || results.length === 0) { - return; - } + this.unsubscribe = this.openmct.telemetry.subscribe( + this.domainObject, + this.updateState.bind(this) + ); + }; - this.updateState(results[results.length - 1]); - }.bind(this)); + SummaryWidgetView.prototype.show = function (container) { + this.container = container; + this.render(); + this.removeMutationListener = this.openmct.objects.observe( + this.domainObject, + '*', + this.onMutation.bind(this) + ); + this.openmct.time.on('timeSystem', this.render); + }; - this.unsubscribe = this.openmct - .telemetry - .subscribe(this.domainObject, this.updateState.bind(this)); - }; + SummaryWidgetView.prototype.onMutation = function (domainObject) { + this.domainObject = domainObject; + this.render(); + }; - SummaryWidgetView.prototype.show = function (container) { - this.container = container; - this.render(); - this.removeMutationListener = this.openmct.objects.observe( - this.domainObject, - '*', - this.onMutation.bind(this) - ); - this.openmct.time.on('timeSystem', this.render); - }; - - SummaryWidgetView.prototype.onMutation = function (domainObject) { - this.domainObject = domainObject; - this.render(); - }; - - SummaryWidgetView.prototype.destroy = function (container) { - this.unsubscribe(); - this.removeMutationListener(); - this.openmct.time.off('timeSystem', this.render); - this.destroyed = true; - delete this.widget; - delete this.label; - delete this.openmct; - delete this.domainObject; - }; - - return SummaryWidgetView; + SummaryWidgetView.prototype.destroy = function (container) { + this.unsubscribe(); + this.removeMutationListener(); + this.openmct.time.off('timeSystem', this.render); + this.destroyed = true; + delete this.widget; + delete this.label; + delete this.openmct; + delete this.domainObject; + }; + return SummaryWidgetView; }); diff --git a/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js b/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js index db569a0e58..aa90739e64 100644 --- a/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js +++ b/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js @@ -1,43 +1,38 @@ -define([ - '../SummaryWidget', - './SummaryWidgetView', - 'objectUtils' -], function ( - SummaryWidgetEditView, - SummaryWidgetView, - objectUtils +define(['../SummaryWidget', './SummaryWidgetView', 'objectUtils'], function ( + SummaryWidgetEditView, + SummaryWidgetView, + objectUtils ) { + const DEFAULT_VIEW_PRIORITY = 100; + /** + * + */ + function SummaryWidgetViewProvider(openmct) { + return { + key: 'summary-widget-viewer', + name: 'Summary View', + cssClass: 'icon-summary-widget', + canView: function (domainObject) { + return domainObject.type === 'summary-widget'; + }, + canEdit: function (domainObject) { + return domainObject.type === 'summary-widget'; + }, + view: function (domainObject) { + return new SummaryWidgetView(domainObject, openmct); + }, + edit: function (domainObject) { + return new SummaryWidgetEditView(domainObject, openmct); + }, + priority: function (domainObject) { + if (domainObject.type === 'summary-widget') { + return Number.MAX_VALUE; + } else { + return DEFAULT_VIEW_PRIORITY; + } + } + }; + } - const DEFAULT_VIEW_PRIORITY = 100; - /** - * - */ - function SummaryWidgetViewProvider(openmct) { - return { - key: 'summary-widget-viewer', - name: 'Summary View', - cssClass: 'icon-summary-widget', - canView: function (domainObject) { - return domainObject.type === 'summary-widget'; - }, - canEdit: function (domainObject) { - return domainObject.type === 'summary-widget'; - }, - view: function (domainObject) { - return new SummaryWidgetView(domainObject, openmct); - }, - edit: function (domainObject) { - return new SummaryWidgetEditView(domainObject, openmct); - }, - priority: function (domainObject) { - if (domainObject.type === 'summary-widget') { - return Number.MAX_VALUE; - } else { - return DEFAULT_VIEW_PRIORITY; - } - } - }; - } - - return SummaryWidgetViewProvider; + return SummaryWidgetViewProvider; }); diff --git a/src/plugins/summaryWidget/src/views/summary-widget.html b/src/plugins/summaryWidget/src/views/summary-widget.html index 738fbaf08b..3278bf2781 100644 --- a/src/plugins/summaryWidget/src/views/summary-widget.html +++ b/src/plugins/summaryWidget/src/views/summary-widget.html @@ -1,4 +1,4 @@ -
-
Loading...
-
\ No newline at end of file +
+
Loading...
+ diff --git a/src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js b/src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js index 49ff638a49..b9fddbb463 100644 --- a/src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js +++ b/src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js @@ -1,340 +1,363 @@ define(['../src/ConditionEvaluator'], function (ConditionEvaluator) { - describe('A Summary Widget Rule Evaluator', function () { - let evaluator; - let testEvaluator; - let testOperation; - let mockCache; - let mockTestCache; - let mockComposition; - let mockConditions; - let mockConditionsEmpty; - let mockConditionsUndefined; - let mockConditionsAnyTrue; - let mockConditionsAllTrue; - let mockConditionsAnyFalse; - let mockConditionsAllFalse; - let mockOperations; + describe('A Summary Widget Rule Evaluator', function () { + let evaluator; + let testEvaluator; + let testOperation; + let mockCache; + let mockTestCache; + let mockComposition; + let mockConditions; + let mockConditionsEmpty; + let mockConditionsUndefined; + let mockConditionsAnyTrue; + let mockConditionsAllTrue; + let mockConditionsAnyFalse; + let mockConditionsAllFalse; + let mockOperations; - beforeEach(function () { - mockCache = { - a: { - alpha: 3, - beta: 9, - gamma: 'Testing 1 2 3' - }, - b: { - alpha: 44, - beta: 23, - gamma: 'Hello World' - }, - c: { - foo: 'bar', - iAm: 'The Walrus', - creature: { - type: 'Centaur' - } - } - }; - mockTestCache = { - a: { - alpha: 1, - beta: 1, - gamma: 'Testing 4 5 6' - }, - b: { - alpha: 2, - beta: 2, - gamma: 'Goodbye world' - } - }; - mockComposition = { - a: {}, - b: {}, - c: {} - }; - mockConditions = [{ - object: 'a', - key: 'alpha', - operation: 'greaterThan', - values: [2] - }, { - object: 'b', - key: 'gamma', - operation: 'lessThan', - values: [5] - }]; - mockConditionsEmpty = [{ - object: '', - key: '', - operation: '', - values: [] - }]; - mockConditionsUndefined = [{ - object: 'No Such Object', - key: '', - operation: '', - values: [] - }, { - object: 'a', - key: 'No Such Key', - operation: '', - values: [] - }, { - object: 'a', - key: 'alpha', - operation: 'No Such Operation', - values: [] - }, { - object: 'all', - key: 'Nonexistent Field', - operation: 'Random Operation', - values: [] - }, { - object: 'any', - key: 'Nonexistent Field', - operation: 'Whatever Operation', - values: [] - }]; - mockConditionsAnyTrue = [{ - object: 'any', - key: 'alpha', - operation: 'greaterThan', - values: [5] - }]; - mockConditionsAnyFalse = [{ - object: 'any', - key: 'alpha', - operation: 'greaterThan', - values: [1000] - }]; - mockConditionsAllFalse = [{ - object: 'all', - key: 'alpha', - operation: 'greaterThan', - values: [5] - }]; - mockConditionsAllTrue = [{ - object: 'all', - key: 'alpha', - operation: 'greaterThan', - values: [0] - }]; - mockOperations = { - greaterThan: { - operation: function (input) { - return input[0] > input[1]; - }, - text: 'is greater than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' > ' + values[0]; - } - }, - lessThan: { - operation: function (input) { - return input[0] < input[1]; - }, - text: 'is less than', - appliesTo: ['number'], - inputCount: 1 - }, - textContains: { - operation: function (input) { - return input[0] && input[1] && input[0].includes(input[1]); - }, - text: 'text contains', - appliesTo: ['string'], - inputCount: 1 - }, - textIsExactly: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'text is exactly', - appliesTo: ['string'], - inputCount: 1 - }, - isHalfHorse: { - operation: function (input) { - return input[0].type === 'Centaur'; - }, - text: 'is Half Horse', - appliesTo: ['mythicalCreature'], - inputCount: 0, - getDescription: function () { - return 'is half horse'; - } - } - }; - evaluator = new ConditionEvaluator(mockCache, mockComposition); - testEvaluator = new ConditionEvaluator(mockCache, mockComposition); - evaluator.operations = mockOperations; - }); - - it('evaluates a condition when it has no configuration', function () { - expect(evaluator.execute(mockConditionsEmpty, 'any')).toEqual(false); - expect(evaluator.execute(mockConditionsEmpty, 'all')).toEqual(false); - }); - - it('correctly evaluates a set of conditions', function () { - expect(evaluator.execute(mockConditions, 'any')).toEqual(true); - expect(evaluator.execute(mockConditions, 'all')).toEqual(false); - }); - - it('correctly evaluates conditions involving "any telemetry"', function () { - expect(evaluator.execute(mockConditionsAnyTrue, 'any')).toEqual(true); - expect(evaluator.execute(mockConditionsAnyFalse, 'any')).toEqual(false); - }); - - it('correctly evaluates conditions involving "all telemetry"', function () { - expect(evaluator.execute(mockConditionsAllTrue, 'any')).toEqual(true); - expect(evaluator.execute(mockConditionsAllFalse, 'any')).toEqual(false); - }); - - it('handles malformed conditions gracefully', function () { - //if no conditions are fully defined, should return false for any mode - expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(false); - expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(false); - //these conditions are true: evaluator should ignore undefined conditions, - //and evaluate the rule as true - mockConditionsUndefined.push({ - object: 'a', - key: 'gamma', - operation: 'textContains', - values: ['Testing'] - }); - expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(true); - mockConditionsUndefined.push({ - object: 'c', - key: 'iAm', - operation: 'textContains', - values: ['Walrus'] - }); - expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(true); - }); - - it('gets the keys for possible operations', function () { - expect(evaluator.getOperationKeys()).toEqual( - ['greaterThan', 'lessThan', 'textContains', 'textIsExactly', 'isHalfHorse'] - ); - }); - - it('gets output text for a given operation', function () { - expect(evaluator.getOperationText('isHalfHorse')).toEqual('is Half Horse'); - }); - - it('correctly returns whether an operation applies to a given type', function () { - expect(evaluator.operationAppliesTo('isHalfHorse', 'mythicalCreature')).toEqual(true); - expect(evaluator.operationAppliesTo('isHalfHorse', 'spaceJunk')).toEqual(false); - }); - - it('returns the HTML input type associated with a given data type', function () { - expect(evaluator.getInputTypeById('string')).toEqual('text'); - }); - - it('gets the number of inputs required for a given operation', function () { - expect(evaluator.getInputCount('isHalfHorse')).toEqual(0); - expect(evaluator.getInputCount('greaterThan')).toEqual(1); - }); - - it('gets a human-readable description of a condition', function () { - expect(evaluator.getOperationDescription('isHalfHorse')).toEqual('is half horse'); - expect(evaluator.getOperationDescription('greaterThan', [1])).toEqual(' > 1'); - }); - - it('allows setting a substitute cache for testing purposes, and toggling its use', function () { - evaluator.setTestDataCache(mockTestCache); - evaluator.useTestData(true); - expect(evaluator.execute(mockConditions, 'any')).toEqual(false); - expect(evaluator.execute(mockConditions, 'all')).toEqual(false); - mockConditions.push({ - object: 'a', - key: 'gamma', - operation: 'textContains', - values: ['4 5 6'] - }); - expect(evaluator.execute(mockConditions, 'any')).toEqual(true); - expect(evaluator.execute(mockConditions, 'all')).toEqual(false); - mockConditions.pop(); - evaluator.useTestData(false); - expect(evaluator.execute(mockConditions, 'any')).toEqual(true); - expect(evaluator.execute(mockConditions, 'all')).toEqual(false); - }); - - it('supports all required operations', function () { - //equal to - testOperation = testEvaluator.operations.equalTo.operation; - expect(testOperation([33, 33])).toEqual(true); - expect(testOperation([55, 147])).toEqual(false); - //not equal to - testOperation = testEvaluator.operations.notEqualTo.operation; - expect(testOperation([33, 33])).toEqual(false); - expect(testOperation([55, 147])).toEqual(true); - //greater than - testOperation = testEvaluator.operations.greaterThan.operation; - expect(testOperation([100, 33])).toEqual(true); - expect(testOperation([33, 33])).toEqual(false); - expect(testOperation([55, 147])).toEqual(false); - //less than - testOperation = testEvaluator.operations.lessThan.operation; - expect(testOperation([100, 33])).toEqual(false); - expect(testOperation([33, 33])).toEqual(false); - expect(testOperation([55, 147])).toEqual(true); - //greater than or equal to - testOperation = testEvaluator.operations.greaterThanOrEq.operation; - expect(testOperation([100, 33])).toEqual(true); - expect(testOperation([33, 33])).toEqual(true); - expect(testOperation([55, 147])).toEqual(false); - //less than or equal to - testOperation = testEvaluator.operations.lessThanOrEq.operation; - expect(testOperation([100, 33])).toEqual(false); - expect(testOperation([33, 33])).toEqual(true); - expect(testOperation([55, 147])).toEqual(true); - //between - testOperation = testEvaluator.operations.between.operation; - expect(testOperation([100, 33, 66])).toEqual(false); - expect(testOperation([1, 33, 66])).toEqual(false); - expect(testOperation([45, 33, 66])).toEqual(true); - //not between - testOperation = testEvaluator.operations.notBetween.operation; - expect(testOperation([100, 33, 66])).toEqual(true); - expect(testOperation([1, 33, 66])).toEqual(true); - expect(testOperation([45, 33, 66])).toEqual(false); - //text contains - testOperation = testEvaluator.operations.textContains.operation; - expect(testOperation(['Testing', 'tin'])).toEqual(true); - expect(testOperation(['Testing', 'bind'])).toEqual(false); - //text does not contain - testOperation = testEvaluator.operations.textDoesNotContain.operation; - expect(testOperation(['Testing', 'tin'])).toEqual(false); - expect(testOperation(['Testing', 'bind'])).toEqual(true); - //text starts with - testOperation = testEvaluator.operations.textStartsWith.operation; - expect(testOperation(['Testing', 'Tes'])).toEqual(true); - expect(testOperation(['Testing', 'ting'])).toEqual(false); - //text ends with - testOperation = testEvaluator.operations.textEndsWith.operation; - expect(testOperation(['Testing', 'Tes'])).toEqual(false); - expect(testOperation(['Testing', 'ting'])).toEqual(true); - //text is exactly - testOperation = testEvaluator.operations.textIsExactly.operation; - expect(testOperation(['Testing', 'Testing'])).toEqual(true); - expect(testOperation(['Testing', 'Test'])).toEqual(false); - //undefined - testOperation = testEvaluator.operations.isUndefined.operation; - expect(testOperation([1])).toEqual(false); - expect(testOperation([])).toEqual(true); - //isDefined - testOperation = testEvaluator.operations.isDefined.operation; - expect(testOperation([1])).toEqual(true); - expect(testOperation([])).toEqual(false); - }); - - it('can produce a description for all supported operations', function () { - testEvaluator.getOperationKeys().forEach(function (key) { - expect(testEvaluator.getOperationDescription(key, [])).toBeDefined(); - }); - }); + beforeEach(function () { + mockCache = { + a: { + alpha: 3, + beta: 9, + gamma: 'Testing 1 2 3' + }, + b: { + alpha: 44, + beta: 23, + gamma: 'Hello World' + }, + c: { + foo: 'bar', + iAm: 'The Walrus', + creature: { + type: 'Centaur' + } + } + }; + mockTestCache = { + a: { + alpha: 1, + beta: 1, + gamma: 'Testing 4 5 6' + }, + b: { + alpha: 2, + beta: 2, + gamma: 'Goodbye world' + } + }; + mockComposition = { + a: {}, + b: {}, + c: {} + }; + mockConditions = [ + { + object: 'a', + key: 'alpha', + operation: 'greaterThan', + values: [2] + }, + { + object: 'b', + key: 'gamma', + operation: 'lessThan', + values: [5] + } + ]; + mockConditionsEmpty = [ + { + object: '', + key: '', + operation: '', + values: [] + } + ]; + mockConditionsUndefined = [ + { + object: 'No Such Object', + key: '', + operation: '', + values: [] + }, + { + object: 'a', + key: 'No Such Key', + operation: '', + values: [] + }, + { + object: 'a', + key: 'alpha', + operation: 'No Such Operation', + values: [] + }, + { + object: 'all', + key: 'Nonexistent Field', + operation: 'Random Operation', + values: [] + }, + { + object: 'any', + key: 'Nonexistent Field', + operation: 'Whatever Operation', + values: [] + } + ]; + mockConditionsAnyTrue = [ + { + object: 'any', + key: 'alpha', + operation: 'greaterThan', + values: [5] + } + ]; + mockConditionsAnyFalse = [ + { + object: 'any', + key: 'alpha', + operation: 'greaterThan', + values: [1000] + } + ]; + mockConditionsAllFalse = [ + { + object: 'all', + key: 'alpha', + operation: 'greaterThan', + values: [5] + } + ]; + mockConditionsAllTrue = [ + { + object: 'all', + key: 'alpha', + operation: 'greaterThan', + values: [0] + } + ]; + mockOperations = { + greaterThan: { + operation: function (input) { + return input[0] > input[1]; + }, + text: 'is greater than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' > ' + values[0]; + } + }, + lessThan: { + operation: function (input) { + return input[0] < input[1]; + }, + text: 'is less than', + appliesTo: ['number'], + inputCount: 1 + }, + textContains: { + operation: function (input) { + return input[0] && input[1] && input[0].includes(input[1]); + }, + text: 'text contains', + appliesTo: ['string'], + inputCount: 1 + }, + textIsExactly: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'text is exactly', + appliesTo: ['string'], + inputCount: 1 + }, + isHalfHorse: { + operation: function (input) { + return input[0].type === 'Centaur'; + }, + text: 'is Half Horse', + appliesTo: ['mythicalCreature'], + inputCount: 0, + getDescription: function () { + return 'is half horse'; + } + } + }; + evaluator = new ConditionEvaluator(mockCache, mockComposition); + testEvaluator = new ConditionEvaluator(mockCache, mockComposition); + evaluator.operations = mockOperations; }); + + it('evaluates a condition when it has no configuration', function () { + expect(evaluator.execute(mockConditionsEmpty, 'any')).toEqual(false); + expect(evaluator.execute(mockConditionsEmpty, 'all')).toEqual(false); + }); + + it('correctly evaluates a set of conditions', function () { + expect(evaluator.execute(mockConditions, 'any')).toEqual(true); + expect(evaluator.execute(mockConditions, 'all')).toEqual(false); + }); + + it('correctly evaluates conditions involving "any telemetry"', function () { + expect(evaluator.execute(mockConditionsAnyTrue, 'any')).toEqual(true); + expect(evaluator.execute(mockConditionsAnyFalse, 'any')).toEqual(false); + }); + + it('correctly evaluates conditions involving "all telemetry"', function () { + expect(evaluator.execute(mockConditionsAllTrue, 'any')).toEqual(true); + expect(evaluator.execute(mockConditionsAllFalse, 'any')).toEqual(false); + }); + + it('handles malformed conditions gracefully', function () { + //if no conditions are fully defined, should return false for any mode + expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(false); + expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(false); + //these conditions are true: evaluator should ignore undefined conditions, + //and evaluate the rule as true + mockConditionsUndefined.push({ + object: 'a', + key: 'gamma', + operation: 'textContains', + values: ['Testing'] + }); + expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(true); + mockConditionsUndefined.push({ + object: 'c', + key: 'iAm', + operation: 'textContains', + values: ['Walrus'] + }); + expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(true); + }); + + it('gets the keys for possible operations', function () { + expect(evaluator.getOperationKeys()).toEqual([ + 'greaterThan', + 'lessThan', + 'textContains', + 'textIsExactly', + 'isHalfHorse' + ]); + }); + + it('gets output text for a given operation', function () { + expect(evaluator.getOperationText('isHalfHorse')).toEqual('is Half Horse'); + }); + + it('correctly returns whether an operation applies to a given type', function () { + expect(evaluator.operationAppliesTo('isHalfHorse', 'mythicalCreature')).toEqual(true); + expect(evaluator.operationAppliesTo('isHalfHorse', 'spaceJunk')).toEqual(false); + }); + + it('returns the HTML input type associated with a given data type', function () { + expect(evaluator.getInputTypeById('string')).toEqual('text'); + }); + + it('gets the number of inputs required for a given operation', function () { + expect(evaluator.getInputCount('isHalfHorse')).toEqual(0); + expect(evaluator.getInputCount('greaterThan')).toEqual(1); + }); + + it('gets a human-readable description of a condition', function () { + expect(evaluator.getOperationDescription('isHalfHorse')).toEqual('is half horse'); + expect(evaluator.getOperationDescription('greaterThan', [1])).toEqual(' > 1'); + }); + + it('allows setting a substitute cache for testing purposes, and toggling its use', function () { + evaluator.setTestDataCache(mockTestCache); + evaluator.useTestData(true); + expect(evaluator.execute(mockConditions, 'any')).toEqual(false); + expect(evaluator.execute(mockConditions, 'all')).toEqual(false); + mockConditions.push({ + object: 'a', + key: 'gamma', + operation: 'textContains', + values: ['4 5 6'] + }); + expect(evaluator.execute(mockConditions, 'any')).toEqual(true); + expect(evaluator.execute(mockConditions, 'all')).toEqual(false); + mockConditions.pop(); + evaluator.useTestData(false); + expect(evaluator.execute(mockConditions, 'any')).toEqual(true); + expect(evaluator.execute(mockConditions, 'all')).toEqual(false); + }); + + it('supports all required operations', function () { + //equal to + testOperation = testEvaluator.operations.equalTo.operation; + expect(testOperation([33, 33])).toEqual(true); + expect(testOperation([55, 147])).toEqual(false); + //not equal to + testOperation = testEvaluator.operations.notEqualTo.operation; + expect(testOperation([33, 33])).toEqual(false); + expect(testOperation([55, 147])).toEqual(true); + //greater than + testOperation = testEvaluator.operations.greaterThan.operation; + expect(testOperation([100, 33])).toEqual(true); + expect(testOperation([33, 33])).toEqual(false); + expect(testOperation([55, 147])).toEqual(false); + //less than + testOperation = testEvaluator.operations.lessThan.operation; + expect(testOperation([100, 33])).toEqual(false); + expect(testOperation([33, 33])).toEqual(false); + expect(testOperation([55, 147])).toEqual(true); + //greater than or equal to + testOperation = testEvaluator.operations.greaterThanOrEq.operation; + expect(testOperation([100, 33])).toEqual(true); + expect(testOperation([33, 33])).toEqual(true); + expect(testOperation([55, 147])).toEqual(false); + //less than or equal to + testOperation = testEvaluator.operations.lessThanOrEq.operation; + expect(testOperation([100, 33])).toEqual(false); + expect(testOperation([33, 33])).toEqual(true); + expect(testOperation([55, 147])).toEqual(true); + //between + testOperation = testEvaluator.operations.between.operation; + expect(testOperation([100, 33, 66])).toEqual(false); + expect(testOperation([1, 33, 66])).toEqual(false); + expect(testOperation([45, 33, 66])).toEqual(true); + //not between + testOperation = testEvaluator.operations.notBetween.operation; + expect(testOperation([100, 33, 66])).toEqual(true); + expect(testOperation([1, 33, 66])).toEqual(true); + expect(testOperation([45, 33, 66])).toEqual(false); + //text contains + testOperation = testEvaluator.operations.textContains.operation; + expect(testOperation(['Testing', 'tin'])).toEqual(true); + expect(testOperation(['Testing', 'bind'])).toEqual(false); + //text does not contain + testOperation = testEvaluator.operations.textDoesNotContain.operation; + expect(testOperation(['Testing', 'tin'])).toEqual(false); + expect(testOperation(['Testing', 'bind'])).toEqual(true); + //text starts with + testOperation = testEvaluator.operations.textStartsWith.operation; + expect(testOperation(['Testing', 'Tes'])).toEqual(true); + expect(testOperation(['Testing', 'ting'])).toEqual(false); + //text ends with + testOperation = testEvaluator.operations.textEndsWith.operation; + expect(testOperation(['Testing', 'Tes'])).toEqual(false); + expect(testOperation(['Testing', 'ting'])).toEqual(true); + //text is exactly + testOperation = testEvaluator.operations.textIsExactly.operation; + expect(testOperation(['Testing', 'Testing'])).toEqual(true); + expect(testOperation(['Testing', 'Test'])).toEqual(false); + //undefined + testOperation = testEvaluator.operations.isUndefined.operation; + expect(testOperation([1])).toEqual(false); + expect(testOperation([])).toEqual(true); + //isDefined + testOperation = testEvaluator.operations.isDefined.operation; + expect(testOperation([1])).toEqual(true); + expect(testOperation([])).toEqual(false); + }); + + it('can produce a description for all supported operations', function () { + testEvaluator.getOperationKeys().forEach(function (key) { + expect(testEvaluator.getOperationDescription(key, [])).toBeDefined(); + }); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/ConditionManagerSpec.js b/src/plugins/summaryWidget/test/ConditionManagerSpec.js index fad16ae2b7..c1b4413d36 100644 --- a/src/plugins/summaryWidget/test/ConditionManagerSpec.js +++ b/src/plugins/summaryWidget/test/ConditionManagerSpec.js @@ -21,412 +21,423 @@ *****************************************************************************/ define(['../src/ConditionManager'], function (ConditionManager) { - xdescribe('A Summary Widget Condition Manager', function () { - let conditionManager; - let mockDomainObject; - let mockCompObject1; - let mockCompObject2; - let mockCompObject3; - let mockMetadata; - let mockTelemetryCallbacks; - let mockEventCallbacks; - let unsubscribeSpies; - let unregisterSpies; - let mockMetadataManagers; - let mockComposition; - let mockOpenMCT; - let mockTelemetryAPI; - let addCallbackSpy; - let loadCallbackSpy; - let removeCallbackSpy; - let telemetryCallbackSpy; - let metadataCallbackSpy; - let telemetryRequests; - let mockTelemetryValues; - let mockTelemetryValues2; - let mockConditionEvaluator; + xdescribe('A Summary Widget Condition Manager', function () { + let conditionManager; + let mockDomainObject; + let mockCompObject1; + let mockCompObject2; + let mockCompObject3; + let mockMetadata; + let mockTelemetryCallbacks; + let mockEventCallbacks; + let unsubscribeSpies; + let unregisterSpies; + let mockMetadataManagers; + let mockComposition; + let mockOpenMCT; + let mockTelemetryAPI; + let addCallbackSpy; + let loadCallbackSpy; + let removeCallbackSpy; + let telemetryCallbackSpy; + let metadataCallbackSpy; + let telemetryRequests; + let mockTelemetryValues; + let mockTelemetryValues2; + let mockConditionEvaluator; - beforeEach(function () { - mockDomainObject = { - identifier: { - key: 'testKey' - }, - name: 'Test Object', - composition: [{ - mockCompObject1: { - key: 'mockCompObject1' - }, - mockCompObject2: { - key: 'mockCompObject2' - } - }], - configuration: {} - }; - mockCompObject1 = { - identifier: { - key: 'mockCompObject1' - }, - name: 'Object 1' - }; - mockCompObject2 = { - identifier: { - key: 'mockCompObject2' - }, - name: 'Object 2' - }; - mockCompObject3 = { - identifier: { - key: 'mockCompObject3' - }, - name: 'Object 3' - }; - mockMetadata = { - mockCompObject1: { - property1: { - key: 'property1', - name: 'Property 1', - format: 'string', - hints: {} - }, - property2: { - key: 'property2', - name: 'Property 2', - hints: { - domain: 1 - } - } - }, - mockCompObject2: { - property3: { - key: 'property3', - name: 'Property 3', - format: 'string', - hints: {} - }, - property4: { - key: 'property4', - name: 'Property 4', - hints: { - range: 1 - } - } - }, - mockCompObject3: { - property1: { - key: 'property1', - name: 'Property 1', - hints: {} - }, - property2: { - key: 'property2', - name: 'Property 2', - hints: {} - } - } - }; - mockTelemetryCallbacks = {}; - mockEventCallbacks = {}; - unsubscribeSpies = jasmine.createSpyObj('mockUnsubscribeFunction', [ - 'mockCompObject1', - 'mockCompObject2', - 'mockCompObject3' - ]); - unregisterSpies = jasmine.createSpyObj('mockUnregisterFunctions', [ - 'load', - 'remove', - 'add' - ]); - mockTelemetryValues = { - mockCompObject1: { - property1: 'Its a string', - property2: 42 - }, - mockCompObject2: { - property3: 'Execute order:', - property4: 66 - }, - mockCompObject3: { - property1: 'Testing 1 2 3', - property2: 9000 - } - }; - mockTelemetryValues2 = { - mockCompObject1: { - property1: 'Its a different string', - property2: 44 - }, - mockCompObject2: { - property3: 'Execute catch:', - property4: 22 - }, - mockCompObject3: { - property1: 'Walrus', - property2: 22 - } - }; - mockMetadataManagers = { - mockCompObject1: { - values: jasmine.createSpy('metadataManager').and.returnValue( - Object.values(mockMetadata.mockCompObject1) - ) - }, - mockCompObject2: { - values: jasmine.createSpy('metadataManager').and.returnValue( - Object.values(mockMetadata.mockCompObject2) - ) - }, - mockCompObject3: { - values: jasmine.createSpy('metadataManager').and.returnValue( - Object.values(mockMetadata.mockCompObject2) - ) - } - }; + beforeEach(function () { + mockDomainObject = { + identifier: { + key: 'testKey' + }, + name: 'Test Object', + composition: [ + { + mockCompObject1: { + key: 'mockCompObject1' + }, + mockCompObject2: { + key: 'mockCompObject2' + } + } + ], + configuration: {} + }; + mockCompObject1 = { + identifier: { + key: 'mockCompObject1' + }, + name: 'Object 1' + }; + mockCompObject2 = { + identifier: { + key: 'mockCompObject2' + }, + name: 'Object 2' + }; + mockCompObject3 = { + identifier: { + key: 'mockCompObject3' + }, + name: 'Object 3' + }; + mockMetadata = { + mockCompObject1: { + property1: { + key: 'property1', + name: 'Property 1', + format: 'string', + hints: {} + }, + property2: { + key: 'property2', + name: 'Property 2', + hints: { + domain: 1 + } + } + }, + mockCompObject2: { + property3: { + key: 'property3', + name: 'Property 3', + format: 'string', + hints: {} + }, + property4: { + key: 'property4', + name: 'Property 4', + hints: { + range: 1 + } + } + }, + mockCompObject3: { + property1: { + key: 'property1', + name: 'Property 1', + hints: {} + }, + property2: { + key: 'property2', + name: 'Property 2', + hints: {} + } + } + }; + mockTelemetryCallbacks = {}; + mockEventCallbacks = {}; + unsubscribeSpies = jasmine.createSpyObj('mockUnsubscribeFunction', [ + 'mockCompObject1', + 'mockCompObject2', + 'mockCompObject3' + ]); + unregisterSpies = jasmine.createSpyObj('mockUnregisterFunctions', ['load', 'remove', 'add']); + mockTelemetryValues = { + mockCompObject1: { + property1: 'Its a string', + property2: 42 + }, + mockCompObject2: { + property3: 'Execute order:', + property4: 66 + }, + mockCompObject3: { + property1: 'Testing 1 2 3', + property2: 9000 + } + }; + mockTelemetryValues2 = { + mockCompObject1: { + property1: 'Its a different string', + property2: 44 + }, + mockCompObject2: { + property3: 'Execute catch:', + property4: 22 + }, + mockCompObject3: { + property1: 'Walrus', + property2: 22 + } + }; + mockMetadataManagers = { + mockCompObject1: { + values: jasmine + .createSpy('metadataManager') + .and.returnValue(Object.values(mockMetadata.mockCompObject1)) + }, + mockCompObject2: { + values: jasmine + .createSpy('metadataManager') + .and.returnValue(Object.values(mockMetadata.mockCompObject2)) + }, + mockCompObject3: { + values: jasmine + .createSpy('metadataManager') + .and.returnValue(Object.values(mockMetadata.mockCompObject2)) + } + }; - mockComposition = jasmine.createSpyObj('composition', [ - 'on', - 'off', - 'load', - 'triggerCallback' - ]); - mockComposition.on.and.callFake(function (event, callback, context) { - mockEventCallbacks[event] = callback.bind(context); - }); - mockComposition.off.and.callFake(function (event) { - unregisterSpies[event](); - }); - mockComposition.load.and.callFake(function () { - mockComposition.triggerCallback('add', mockCompObject1); - mockComposition.triggerCallback('add', mockCompObject2); - mockComposition.triggerCallback('load'); - }); - mockComposition.triggerCallback.and.callFake(function (event, obj) { - if (event === 'add') { - mockEventCallbacks.add(obj); - } else if (event === 'remove') { - mockEventCallbacks.remove(obj.identifier); - } else { - mockEventCallbacks[event](); - } - }); - telemetryRequests = []; - mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [ - 'request', - 'isTelemetryObject', - 'getMetadata', - 'subscribe', - 'triggerTelemetryCallback' - ]); - mockTelemetryAPI.request.and.callFake(function (obj) { - const req = { - object: obj - }; - req.promise = new Promise(function (resolve, reject) { - req.resolve = resolve; - req.reject = reject; - }); - telemetryRequests.push(req); - - return req.promise; - }); - mockTelemetryAPI.isTelemetryObject.and.returnValue(true); - mockTelemetryAPI.getMetadata.and.callFake(function (obj) { - return mockMetadataManagers[obj.identifier.key]; - }); - mockTelemetryAPI.subscribe.and.callFake(function (obj, callback) { - mockTelemetryCallbacks[obj.identifier.key] = callback; - - return unsubscribeSpies[obj.identifier.key]; - }); - mockTelemetryAPI.triggerTelemetryCallback.and.callFake(function (key) { - mockTelemetryCallbacks[key](mockTelemetryValues2[key]); - }); - - mockOpenMCT = { - telemetry: mockTelemetryAPI, - composition: {} - }; - mockOpenMCT.composition.get = jasmine.createSpy('get').and.returnValue(mockComposition); - - loadCallbackSpy = jasmine.createSpy('loadCallbackSpy'); - addCallbackSpy = jasmine.createSpy('addCallbackSpy'); - removeCallbackSpy = jasmine.createSpy('removeCallbackSpy'); - metadataCallbackSpy = jasmine.createSpy('metadataCallbackSpy'); - telemetryCallbackSpy = jasmine.createSpy('telemetryCallbackSpy'); - - conditionManager = new ConditionManager(mockDomainObject, mockOpenMCT); - conditionManager.on('load', loadCallbackSpy); - conditionManager.on('add', addCallbackSpy); - conditionManager.on('remove', removeCallbackSpy); - conditionManager.on('metadata', metadataCallbackSpy); - conditionManager.on('receiveTelemetry', telemetryCallbackSpy); - - mockConditionEvaluator = jasmine.createSpy('mockConditionEvaluator'); - mockConditionEvaluator.execute = jasmine.createSpy('execute'); - conditionManager.evaluator = mockConditionEvaluator; + mockComposition = jasmine.createSpyObj('composition', [ + 'on', + 'off', + 'load', + 'triggerCallback' + ]); + mockComposition.on.and.callFake(function (event, callback, context) { + mockEventCallbacks[event] = callback.bind(context); + }); + mockComposition.off.and.callFake(function (event) { + unregisterSpies[event](); + }); + mockComposition.load.and.callFake(function () { + mockComposition.triggerCallback('add', mockCompObject1); + mockComposition.triggerCallback('add', mockCompObject2); + mockComposition.triggerCallback('load'); + }); + mockComposition.triggerCallback.and.callFake(function (event, obj) { + if (event === 'add') { + mockEventCallbacks.add(obj); + } else if (event === 'remove') { + mockEventCallbacks.remove(obj.identifier); + } else { + mockEventCallbacks[event](); + } + }); + telemetryRequests = []; + mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [ + 'request', + 'isTelemetryObject', + 'getMetadata', + 'subscribe', + 'triggerTelemetryCallback' + ]); + mockTelemetryAPI.request.and.callFake(function (obj) { + const req = { + object: obj + }; + req.promise = new Promise(function (resolve, reject) { + req.resolve = resolve; + req.reject = reject; }); + telemetryRequests.push(req); - it('loads the initial composition and invokes the appropriate handlers', function () { - mockComposition.triggerCallback('load'); - expect(conditionManager.getComposition()).toEqual({ - mockCompObject1: mockCompObject1, - mockCompObject2: mockCompObject2 - }); - expect(loadCallbackSpy).toHaveBeenCalled(); - expect(conditionManager.loadCompleted()).toEqual(true); - }); + return req.promise; + }); + mockTelemetryAPI.isTelemetryObject.and.returnValue(true); + mockTelemetryAPI.getMetadata.and.callFake(function (obj) { + return mockMetadataManagers[obj.identifier.key]; + }); + mockTelemetryAPI.subscribe.and.callFake(function (obj, callback) { + mockTelemetryCallbacks[obj.identifier.key] = callback; - it('loads metadata from composition and gets it upon request', function () { - expect(conditionManager.getTelemetryMetadata('mockCompObject1')) - .toEqual(mockMetadata.mockCompObject1); - expect(conditionManager.getTelemetryMetadata('mockCompObject2')) - .toEqual(mockMetadata.mockCompObject2); - }); + return unsubscribeSpies[obj.identifier.key]; + }); + mockTelemetryAPI.triggerTelemetryCallback.and.callFake(function (key) { + mockTelemetryCallbacks[key](mockTelemetryValues2[key]); + }); - it('maintains lists of global metadata, and does not duplicate repeated fields', function () { - const allKeys = { - property1: { - key: 'property1', - name: 'Property 1', - format: 'string', - hints: {} - }, - property2: { - key: 'property2', - name: 'Property 2', - hints: { - domain: 1 - } - }, - property3: { - key: 'property3', - name: 'Property 3', - format: 'string', - hints: {} - }, - property4: { - key: 'property4', - name: 'Property 4', - hints: { - range: 1 - } - } - }; - expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); - expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); - mockComposition.triggerCallback('add', mockCompObject3); - expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); - expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); - }); + mockOpenMCT = { + telemetry: mockTelemetryAPI, + composition: {} + }; + mockOpenMCT.composition.get = jasmine.createSpy('get').and.returnValue(mockComposition); - it('loads and gets telemetry property types', function () { - conditionManager.parseAllPropertyTypes(); - expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1')) - .toEqual('string'); - expect(conditionManager.getTelemetryPropertyType('mockCompObject2', 'property4')) - .toEqual('number'); - expect(conditionManager.metadataLoadCompleted()).toEqual(true); - expect(metadataCallbackSpy).toHaveBeenCalled(); - }); + loadCallbackSpy = jasmine.createSpy('loadCallbackSpy'); + addCallbackSpy = jasmine.createSpy('addCallbackSpy'); + removeCallbackSpy = jasmine.createSpy('removeCallbackSpy'); + metadataCallbackSpy = jasmine.createSpy('metadataCallbackSpy'); + telemetryCallbackSpy = jasmine.createSpy('telemetryCallbackSpy'); - it('responds to a composition add event and invokes the appropriate handlers', function () { - mockComposition.triggerCallback('add', mockCompObject3); - expect(addCallbackSpy).toHaveBeenCalledWith(mockCompObject3); - expect(conditionManager.getComposition()).toEqual({ - mockCompObject1: mockCompObject1, - mockCompObject2: mockCompObject2, - mockCompObject3: mockCompObject3 - }); - }); + conditionManager = new ConditionManager(mockDomainObject, mockOpenMCT); + conditionManager.on('load', loadCallbackSpy); + conditionManager.on('add', addCallbackSpy); + conditionManager.on('remove', removeCallbackSpy); + conditionManager.on('metadata', metadataCallbackSpy); + conditionManager.on('receiveTelemetry', telemetryCallbackSpy); - it('responds to a composition remove event and invokes the appropriate handlers', function () { - mockComposition.triggerCallback('remove', mockCompObject2); - expect(removeCallbackSpy).toHaveBeenCalledWith({ - key: 'mockCompObject2' - }); - expect(unsubscribeSpies.mockCompObject2).toHaveBeenCalled(); - expect(conditionManager.getComposition()).toEqual({ - mockCompObject1: mockCompObject1 - }); - }); - - it('unregisters telemetry subscriptions and composition listeners on destroy', function () { - mockComposition.triggerCallback('add', mockCompObject3); - conditionManager.destroy(); - Object.values(unsubscribeSpies).forEach(function (spy) { - expect(spy).toHaveBeenCalled(); - }); - Object.values(unregisterSpies).forEach(function (spy) { - expect(spy).toHaveBeenCalled(); - }); - }); - - it('populates its LAD cache with historial data on load, if available', function (done) { - expect(telemetryRequests.length).toBe(2); - expect(telemetryRequests[0].object).toBe(mockCompObject1); - expect(telemetryRequests[1].object).toBe(mockCompObject2); - - expect(telemetryCallbackSpy).not.toHaveBeenCalled(); - - telemetryCallbackSpy.and.callFake(function () { - if (telemetryCallbackSpy.calls.count() === 2) { - expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a string'); - expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(66); - done(); - } - }); - - telemetryRequests[0].resolve([mockTelemetryValues.mockCompObject1]); - telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]); - }); - - it('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () { - mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1'); - expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a different string'); - mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2'); - expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(22); - expect(telemetryCallbackSpy).toHaveBeenCalled(); - }); - - it('evalutes a set of rules and returns the id of the' - + 'last active rule, or the first if no rules are active', function () { - const mockRuleOrder = ['default', 'rule0', 'rule1']; - const mockRules = { - default: { - getProperty: function () {} - }, - rule0: { - getProperty: function () {} - }, - rule1: { - getProperty: function () {} - } - }; - - mockConditionEvaluator.execute.and.returnValue(false); - expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('default'); - mockConditionEvaluator.execute.and.returnValue(true); - expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('rule1'); - }); - - it('gets the human-readable name of a composition object', function () { - expect(conditionManager.getObjectName('mockCompObject1')).toEqual('Object 1'); - expect(conditionManager.getObjectName('all')).toEqual('all Telemetry'); - }); - - it('gets the human-readable name of a telemetry field', function () { - expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1')) - .toEqual('Property 1'); - expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4')) - .toEqual('Property 4'); - }); - - it('gets its associated ConditionEvaluator', function () { - expect(conditionManager.getEvaluator()).toEqual(mockConditionEvaluator); - }); - - it('allows forcing a receive telemetry event', function () { - conditionManager.triggerTelemetryCallback(); - expect(telemetryCallbackSpy).toHaveBeenCalled(); - }); + mockConditionEvaluator = jasmine.createSpy('mockConditionEvaluator'); + mockConditionEvaluator.execute = jasmine.createSpy('execute'); + conditionManager.evaluator = mockConditionEvaluator; }); + + it('loads the initial composition and invokes the appropriate handlers', function () { + mockComposition.triggerCallback('load'); + expect(conditionManager.getComposition()).toEqual({ + mockCompObject1: mockCompObject1, + mockCompObject2: mockCompObject2 + }); + expect(loadCallbackSpy).toHaveBeenCalled(); + expect(conditionManager.loadCompleted()).toEqual(true); + }); + + it('loads metadata from composition and gets it upon request', function () { + expect(conditionManager.getTelemetryMetadata('mockCompObject1')).toEqual( + mockMetadata.mockCompObject1 + ); + expect(conditionManager.getTelemetryMetadata('mockCompObject2')).toEqual( + mockMetadata.mockCompObject2 + ); + }); + + it('maintains lists of global metadata, and does not duplicate repeated fields', function () { + const allKeys = { + property1: { + key: 'property1', + name: 'Property 1', + format: 'string', + hints: {} + }, + property2: { + key: 'property2', + name: 'Property 2', + hints: { + domain: 1 + } + }, + property3: { + key: 'property3', + name: 'Property 3', + format: 'string', + hints: {} + }, + property4: { + key: 'property4', + name: 'Property 4', + hints: { + range: 1 + } + } + }; + expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); + expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); + mockComposition.triggerCallback('add', mockCompObject3); + expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); + expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); + }); + + it('loads and gets telemetry property types', function () { + conditionManager.parseAllPropertyTypes(); + expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1')).toEqual( + 'string' + ); + expect(conditionManager.getTelemetryPropertyType('mockCompObject2', 'property4')).toEqual( + 'number' + ); + expect(conditionManager.metadataLoadCompleted()).toEqual(true); + expect(metadataCallbackSpy).toHaveBeenCalled(); + }); + + it('responds to a composition add event and invokes the appropriate handlers', function () { + mockComposition.triggerCallback('add', mockCompObject3); + expect(addCallbackSpy).toHaveBeenCalledWith(mockCompObject3); + expect(conditionManager.getComposition()).toEqual({ + mockCompObject1: mockCompObject1, + mockCompObject2: mockCompObject2, + mockCompObject3: mockCompObject3 + }); + }); + + it('responds to a composition remove event and invokes the appropriate handlers', function () { + mockComposition.triggerCallback('remove', mockCompObject2); + expect(removeCallbackSpy).toHaveBeenCalledWith({ + key: 'mockCompObject2' + }); + expect(unsubscribeSpies.mockCompObject2).toHaveBeenCalled(); + expect(conditionManager.getComposition()).toEqual({ + mockCompObject1: mockCompObject1 + }); + }); + + it('unregisters telemetry subscriptions and composition listeners on destroy', function () { + mockComposition.triggerCallback('add', mockCompObject3); + conditionManager.destroy(); + Object.values(unsubscribeSpies).forEach(function (spy) { + expect(spy).toHaveBeenCalled(); + }); + Object.values(unregisterSpies).forEach(function (spy) { + expect(spy).toHaveBeenCalled(); + }); + }); + + it('populates its LAD cache with historial data on load, if available', function (done) { + expect(telemetryRequests.length).toBe(2); + expect(telemetryRequests[0].object).toBe(mockCompObject1); + expect(telemetryRequests[1].object).toBe(mockCompObject2); + + expect(telemetryCallbackSpy).not.toHaveBeenCalled(); + + telemetryCallbackSpy.and.callFake(function () { + if (telemetryCallbackSpy.calls.count() === 2) { + expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual( + 'Its a string' + ); + expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(66); + done(); + } + }); + + telemetryRequests[0].resolve([mockTelemetryValues.mockCompObject1]); + telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]); + }); + + it('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () { + mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1'); + expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual( + 'Its a different string' + ); + mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2'); + expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(22); + expect(telemetryCallbackSpy).toHaveBeenCalled(); + }); + + it( + 'evalutes a set of rules and returns the id of the' + + 'last active rule, or the first if no rules are active', + function () { + const mockRuleOrder = ['default', 'rule0', 'rule1']; + const mockRules = { + default: { + getProperty: function () {} + }, + rule0: { + getProperty: function () {} + }, + rule1: { + getProperty: function () {} + } + }; + + mockConditionEvaluator.execute.and.returnValue(false); + expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('default'); + mockConditionEvaluator.execute.and.returnValue(true); + expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('rule1'); + } + ); + + it('gets the human-readable name of a composition object', function () { + expect(conditionManager.getObjectName('mockCompObject1')).toEqual('Object 1'); + expect(conditionManager.getObjectName('all')).toEqual('all Telemetry'); + }); + + it('gets the human-readable name of a telemetry field', function () { + expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1')).toEqual( + 'Property 1' + ); + expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4')).toEqual( + 'Property 4' + ); + }); + + it('gets its associated ConditionEvaluator', function () { + expect(conditionManager.getEvaluator()).toEqual(mockConditionEvaluator); + }); + + it('allows forcing a receive telemetry event', function () { + conditionManager.triggerTelemetryCallback(); + expect(telemetryCallbackSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/ConditionSpec.js b/src/plugins/summaryWidget/test/ConditionSpec.js index 29a21a40b3..657fd3b843 100644 --- a/src/plugins/summaryWidget/test/ConditionSpec.js +++ b/src/plugins/summaryWidget/test/ConditionSpec.js @@ -21,185 +21,185 @@ *****************************************************************************/ define(['../src/Condition'], function (Condition) { - xdescribe('A summary widget condition', function () { - let testCondition; - let mockConfig; - let mockConditionManager; - let mockContainer; - let mockEvaluator; - let changeSpy; - let duplicateSpy; - let removeSpy; - let generateValuesSpy; + xdescribe('A summary widget condition', function () { + let testCondition; + let mockConfig; + let mockConditionManager; + let mockContainer; + let mockEvaluator; + let changeSpy; + let duplicateSpy; + let removeSpy; + let generateValuesSpy; - beforeEach(function () { - mockContainer = document.createElement('div'); + beforeEach(function () { + mockContainer = document.createElement('div'); - mockConfig = { - object: 'object1', - key: 'property1', - operation: 'operation1', - values: [1, 2, 3] - }; + mockConfig = { + object: 'object1', + key: 'property1', + operation: 'operation1', + values: [1, 2, 3] + }; - mockEvaluator = {}; - mockEvaluator.getInputCount = jasmine.createSpy('inputCount'); - mockEvaluator.getInputType = jasmine.createSpy('inputType'); + mockEvaluator = {}; + mockEvaluator.getInputCount = jasmine.createSpy('inputCount'); + mockEvaluator.getInputType = jasmine.createSpy('inputType'); - mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ - 'on', - 'getComposition', - 'loadCompleted', - 'getEvaluator', - 'getTelemetryMetadata', - 'metadataLoadCompleted', - 'getObjectName', - 'getTelemetryPropertyName' - ]); - mockConditionManager.loadCompleted.and.returnValue(false); - mockConditionManager.metadataLoadCompleted.and.returnValue(false); - mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); - mockConditionManager.getComposition.and.returnValue({}); - mockConditionManager.getTelemetryMetadata.and.returnValue({}); - mockConditionManager.getObjectName.and.returnValue('Object Name'); - mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); + mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ + 'on', + 'getComposition', + 'loadCompleted', + 'getEvaluator', + 'getTelemetryMetadata', + 'metadataLoadCompleted', + 'getObjectName', + 'getTelemetryPropertyName' + ]); + mockConditionManager.loadCompleted.and.returnValue(false); + mockConditionManager.metadataLoadCompleted.and.returnValue(false); + mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); + mockConditionManager.getComposition.and.returnValue({}); + mockConditionManager.getTelemetryMetadata.and.returnValue({}); + mockConditionManager.getObjectName.and.returnValue('Object Name'); + mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); - duplicateSpy = jasmine.createSpy('duplicate'); - removeSpy = jasmine.createSpy('remove'); - changeSpy = jasmine.createSpy('change'); - generateValuesSpy = jasmine.createSpy('generateValueInputs'); + duplicateSpy = jasmine.createSpy('duplicate'); + removeSpy = jasmine.createSpy('remove'); + changeSpy = jasmine.createSpy('change'); + generateValuesSpy = jasmine.createSpy('generateValueInputs'); - testCondition = new Condition(mockConfig, 54, mockConditionManager); + testCondition = new Condition(mockConfig, 54, mockConditionManager); - testCondition.on('duplicate', duplicateSpy); - testCondition.on('remove', removeSpy); - testCondition.on('change', changeSpy); - }); - - it('exposes a DOM element to represent itself in the view', function () { - mockContainer.append(testCondition.getDOM()); - expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(1); - }); - - it('responds to a change in its object select', function () { - testCondition.selects.object.setSelected(''); - expect(changeSpy).toHaveBeenCalledWith({ - value: '', - property: 'object', - index: 54 - }); - }); - - it('responds to a change in its key select', function () { - testCondition.selects.key.setSelected(''); - expect(changeSpy).toHaveBeenCalledWith({ - value: '', - property: 'key', - index: 54 - }); - }); - - it('responds to a change in its operation select', function () { - testCondition.generateValueInputs = generateValuesSpy; - testCondition.selects.operation.setSelected(''); - expect(changeSpy).toHaveBeenCalledWith({ - value: '', - property: 'operation', - index: 54 - }); - expect(generateValuesSpy).toHaveBeenCalledWith(''); - }); - - it('generates value inputs of the appropriate type and quantity', function () { - let inputs; - - mockContainer.append(testCondition.getDOM()); - mockEvaluator.getInputType.and.returnValue('number'); - mockEvaluator.getInputCount.and.returnValue(3); - testCondition.generateValueInputs(''); - - inputs = mockContainer.querySelectorAll('input'); - const numberInputs = Array.from(inputs).filter(input => input.type === 'number'); - - expect(numberInputs.length).toEqual(3); - expect(numberInputs[0].valueAsNumber).toEqual(1); - expect(numberInputs[1].valueAsNumber).toEqual(2); - expect(numberInputs[2].valueAsNumber).toEqual(3); - - mockEvaluator.getInputType.and.returnValue('text'); - mockEvaluator.getInputCount.and.returnValue(2); - testCondition.config.values = ['Text I Am', 'Text It Is']; - testCondition.generateValueInputs(''); - - inputs = mockContainer.querySelectorAll('input'); - const textInputs = Array.from(inputs).filter(input => input.type === 'text'); - - expect(textInputs.length).toEqual(2); - expect(textInputs[0].value).toEqual('Text I Am'); - expect(textInputs[1].value).toEqual('Text It Is'); - }); - - it('ensures reasonable defaults on values if none are provided', function () { - let inputs; - - mockContainer.append(testCondition.getDOM()); - mockEvaluator.getInputType.and.returnValue('number'); - mockEvaluator.getInputCount.and.returnValue(3); - testCondition.config.values = []; - testCondition.generateValueInputs(''); - - inputs = Array.from(mockContainer.querySelectorAll('input')); - - expect(inputs[0].valueAsNumber).toEqual(0); - expect(inputs[1].valueAsNumber).toEqual(0); - expect(inputs[2].valueAsNumber).toEqual(0); - expect(testCondition.config.values).toEqual([0, 0, 0]); - - mockEvaluator.getInputType.and.returnValue('text'); - mockEvaluator.getInputCount.and.returnValue(2); - testCondition.config.values = []; - testCondition.generateValueInputs(''); - - inputs = Array.from(mockContainer.querySelectorAll('input')); - - expect(inputs[0].value).toEqual(''); - expect(inputs[1].value).toEqual(''); - expect(testCondition.config.values).toEqual(['', '']); - }); - - it('responds to a change in its value inputs', function () { - mockContainer.append(testCondition.getDOM()); - mockEvaluator.getInputType.and.returnValue('number'); - mockEvaluator.getInputCount.and.returnValue(3); - testCondition.generateValueInputs(''); - - const event = new Event('input', { - bubbles: true, - cancelable: true - }); - const inputs = mockContainer.querySelectorAll('input'); - - inputs[1].value = 9001; - inputs[1].dispatchEvent(event); - - expect(changeSpy).toHaveBeenCalledWith({ - value: 9001, - property: 'values[1]', - index: 54 - }); - }); - - it('can remove itself from the configuration', function () { - testCondition.remove(); - expect(removeSpy).toHaveBeenCalledWith(54); - }); - - it('can duplicate itself', function () { - testCondition.duplicate(); - expect(duplicateSpy).toHaveBeenCalledWith({ - sourceCondition: mockConfig, - index: 54 - }); - }); + testCondition.on('duplicate', duplicateSpy); + testCondition.on('remove', removeSpy); + testCondition.on('change', changeSpy); }); + + it('exposes a DOM element to represent itself in the view', function () { + mockContainer.append(testCondition.getDOM()); + expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(1); + }); + + it('responds to a change in its object select', function () { + testCondition.selects.object.setSelected(''); + expect(changeSpy).toHaveBeenCalledWith({ + value: '', + property: 'object', + index: 54 + }); + }); + + it('responds to a change in its key select', function () { + testCondition.selects.key.setSelected(''); + expect(changeSpy).toHaveBeenCalledWith({ + value: '', + property: 'key', + index: 54 + }); + }); + + it('responds to a change in its operation select', function () { + testCondition.generateValueInputs = generateValuesSpy; + testCondition.selects.operation.setSelected(''); + expect(changeSpy).toHaveBeenCalledWith({ + value: '', + property: 'operation', + index: 54 + }); + expect(generateValuesSpy).toHaveBeenCalledWith(''); + }); + + it('generates value inputs of the appropriate type and quantity', function () { + let inputs; + + mockContainer.append(testCondition.getDOM()); + mockEvaluator.getInputType.and.returnValue('number'); + mockEvaluator.getInputCount.and.returnValue(3); + testCondition.generateValueInputs(''); + + inputs = mockContainer.querySelectorAll('input'); + const numberInputs = Array.from(inputs).filter((input) => input.type === 'number'); + + expect(numberInputs.length).toEqual(3); + expect(numberInputs[0].valueAsNumber).toEqual(1); + expect(numberInputs[1].valueAsNumber).toEqual(2); + expect(numberInputs[2].valueAsNumber).toEqual(3); + + mockEvaluator.getInputType.and.returnValue('text'); + mockEvaluator.getInputCount.and.returnValue(2); + testCondition.config.values = ['Text I Am', 'Text It Is']; + testCondition.generateValueInputs(''); + + inputs = mockContainer.querySelectorAll('input'); + const textInputs = Array.from(inputs).filter((input) => input.type === 'text'); + + expect(textInputs.length).toEqual(2); + expect(textInputs[0].value).toEqual('Text I Am'); + expect(textInputs[1].value).toEqual('Text It Is'); + }); + + it('ensures reasonable defaults on values if none are provided', function () { + let inputs; + + mockContainer.append(testCondition.getDOM()); + mockEvaluator.getInputType.and.returnValue('number'); + mockEvaluator.getInputCount.and.returnValue(3); + testCondition.config.values = []; + testCondition.generateValueInputs(''); + + inputs = Array.from(mockContainer.querySelectorAll('input')); + + expect(inputs[0].valueAsNumber).toEqual(0); + expect(inputs[1].valueAsNumber).toEqual(0); + expect(inputs[2].valueAsNumber).toEqual(0); + expect(testCondition.config.values).toEqual([0, 0, 0]); + + mockEvaluator.getInputType.and.returnValue('text'); + mockEvaluator.getInputCount.and.returnValue(2); + testCondition.config.values = []; + testCondition.generateValueInputs(''); + + inputs = Array.from(mockContainer.querySelectorAll('input')); + + expect(inputs[0].value).toEqual(''); + expect(inputs[1].value).toEqual(''); + expect(testCondition.config.values).toEqual(['', '']); + }); + + it('responds to a change in its value inputs', function () { + mockContainer.append(testCondition.getDOM()); + mockEvaluator.getInputType.and.returnValue('number'); + mockEvaluator.getInputCount.and.returnValue(3); + testCondition.generateValueInputs(''); + + const event = new Event('input', { + bubbles: true, + cancelable: true + }); + const inputs = mockContainer.querySelectorAll('input'); + + inputs[1].value = 9001; + inputs[1].dispatchEvent(event); + + expect(changeSpy).toHaveBeenCalledWith({ + value: 9001, + property: 'values[1]', + index: 54 + }); + }); + + it('can remove itself from the configuration', function () { + testCondition.remove(); + expect(removeSpy).toHaveBeenCalledWith(54); + }); + + it('can duplicate itself', function () { + testCondition.duplicate(); + expect(duplicateSpy).toHaveBeenCalledWith({ + sourceCondition: mockConfig, + index: 54 + }); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/RuleSpec.js b/src/plugins/summaryWidget/test/RuleSpec.js index df5108f7ae..880708529a 100644 --- a/src/plugins/summaryWidget/test/RuleSpec.js +++ b/src/plugins/summaryWidget/test/RuleSpec.js @@ -1,278 +1,293 @@ define(['../src/Rule'], function (Rule) { - describe('A Summary Widget Rule', function () { - let mockRuleConfig; - let mockDomainObject; - let mockOpenMCT; - let mockConditionManager; - let mockWidgetDnD; - let mockEvaluator; - let mockContainer; - let testRule; - let removeSpy; - let duplicateSpy; - let changeSpy; - let conditionChangeSpy; + describe('A Summary Widget Rule', function () { + let mockRuleConfig; + let mockDomainObject; + let mockOpenMCT; + let mockConditionManager; + let mockWidgetDnD; + let mockEvaluator; + let mockContainer; + let testRule; + let removeSpy; + let duplicateSpy; + let changeSpy; + let conditionChangeSpy; - beforeEach(function () { - mockRuleConfig = { - name: 'Name', - id: 'mockRule', - icon: 'test-icon-name', - style: { - 'background-color': '', - 'border-color': '', - 'color': '' - }, - expanded: true, - conditions: [{ - object: '', - key: '', - operation: '', - values: [] - }, { - object: 'blah', - key: 'blah', - operation: 'blah', - values: ['blah.', 'blah!', 'blah?'] - }] - }; - mockDomainObject = { - configuration: { - ruleConfigById: { - mockRule: mockRuleConfig, - otherRule: {} - }, - ruleOrder: ['default', 'mockRule', 'otherRule'] - } - }; + beforeEach(function () { + mockRuleConfig = { + name: 'Name', + id: 'mockRule', + icon: 'test-icon-name', + style: { + 'background-color': '', + 'border-color': '', + color: '' + }, + expanded: true, + conditions: [ + { + object: '', + key: '', + operation: '', + values: [] + }, + { + object: 'blah', + key: 'blah', + operation: 'blah', + values: ['blah.', 'blah!', 'blah?'] + } + ] + }; + mockDomainObject = { + configuration: { + ruleConfigById: { + mockRule: mockRuleConfig, + otherRule: {} + }, + ruleOrder: ['default', 'mockRule', 'otherRule'] + } + }; - mockOpenMCT = {}; - mockOpenMCT.objects = {}; - mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); + mockOpenMCT = {}; + mockOpenMCT.objects = {}; + mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); - mockEvaluator = {}; - mockEvaluator.getOperationDescription = jasmine.createSpy('evaluator') - .and.returnValue('Operation Description'); + mockEvaluator = {}; + mockEvaluator.getOperationDescription = jasmine + .createSpy('evaluator') + .and.returnValue('Operation Description'); - mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ - 'on', - 'getComposition', - 'loadCompleted', - 'getEvaluator', - 'getTelemetryMetadata', - 'metadataLoadCompleted', - 'getObjectName', - 'getTelemetryPropertyName' - ]); - mockConditionManager.loadCompleted.and.returnValue(false); - mockConditionManager.metadataLoadCompleted.and.returnValue(false); - mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); - mockConditionManager.getComposition.and.returnValue({}); - mockConditionManager.getTelemetryMetadata.and.returnValue({}); - mockConditionManager.getObjectName.and.returnValue('Object Name'); - mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); + mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ + 'on', + 'getComposition', + 'loadCompleted', + 'getEvaluator', + 'getTelemetryMetadata', + 'metadataLoadCompleted', + 'getObjectName', + 'getTelemetryPropertyName' + ]); + mockConditionManager.loadCompleted.and.returnValue(false); + mockConditionManager.metadataLoadCompleted.and.returnValue(false); + mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); + mockConditionManager.getComposition.and.returnValue({}); + mockConditionManager.getTelemetryMetadata.and.returnValue({}); + mockConditionManager.getObjectName.and.returnValue('Object Name'); + mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); - mockWidgetDnD = jasmine.createSpyObj('dnd', [ - 'on', - 'setDragImage', - 'dragStart' - ]); + mockWidgetDnD = jasmine.createSpyObj('dnd', ['on', 'setDragImage', 'dragStart']); - mockContainer = document.createElement('div'); + mockContainer = document.createElement('div'); - removeSpy = jasmine.createSpy('removeCallback'); - duplicateSpy = jasmine.createSpy('duplicateCallback'); - changeSpy = jasmine.createSpy('changeCallback'); - conditionChangeSpy = jasmine.createSpy('conditionChangeCallback'); + removeSpy = jasmine.createSpy('removeCallback'); + duplicateSpy = jasmine.createSpy('duplicateCallback'); + changeSpy = jasmine.createSpy('changeCallback'); + conditionChangeSpy = jasmine.createSpy('conditionChangeCallback'); - testRule = new Rule(mockRuleConfig, mockDomainObject, mockOpenMCT, mockConditionManager, - mockWidgetDnD); - testRule.on('remove', removeSpy); - testRule.on('duplicate', duplicateSpy); - testRule.on('change', changeSpy); - testRule.on('conditionChange', conditionChangeSpy); - }); - - it('closes its configuration panel on initial load', function () { - expect(testRule.getProperty('expanded')).toEqual(false); - }); - - it('gets its DOM element', function () { - mockContainer.append(testRule.getDOM()); - expect(mockContainer.querySelectorAll('.l-widget-rule').length).toBeGreaterThan(0); - }); - - it('gets its configuration properties', function () { - expect(testRule.getProperty('name')).toEqual('Name'); - expect(testRule.getProperty('icon')).toEqual('test-icon-name'); - }); - - it('can duplicate itself', function () { - testRule.duplicate(); - mockRuleConfig.expanded = true; - expect(duplicateSpy).toHaveBeenCalledWith(mockRuleConfig); - }); - - it('can remove itself from the configuration', function () { - testRule.remove(); - expect(removeSpy).toHaveBeenCalled(); - expect(mockDomainObject.configuration.ruleConfigById.mockRule).not.toBeDefined(); - expect(mockDomainObject.configuration.ruleOrder).toEqual(['default', 'otherRule']); - }); - - it('updates its configuration on a condition change and invokes callbacks', function () { - testRule.onConditionChange({ - value: 'newValue', - property: 'object', - index: 0 - }); - expect(testRule.getProperty('conditions')[0].object).toEqual('newValue'); - expect(conditionChangeSpy).toHaveBeenCalled(); - }); - - it('allows initializing a new condition with a default configuration', function () { - testRule.initCondition(); - expect(mockRuleConfig.conditions).toEqual([{ - object: '', - key: '', - operation: '', - values: [] - }, { - object: 'blah', - key: 'blah', - operation: 'blah', - values: ['blah.', 'blah!', 'blah?'] - }, { - object: '', - key: '', - operation: '', - values: [] - }]); - }); - - it('allows initializing a new condition from a given configuration', function () { - testRule.initCondition({ - sourceCondition: { - object: 'object1', - key: 'key1', - operation: 'operation1', - values: [1, 2, 3] - }, - index: 0 - }); - expect(mockRuleConfig.conditions).toEqual([{ - object: '', - key: '', - operation: '', - values: [] - }, { - object: 'object1', - key: 'key1', - operation: 'operation1', - values: [1, 2, 3] - }, { - object: 'blah', - key: 'blah', - operation: 'blah', - values: ['blah.', 'blah!', 'blah?'] - }]); - }); - - it('invokes mutate when updating the domain object', function () { - testRule.updateDomainObject(); - expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); - }); - - it('builds condition view from condition configuration', function () { - mockContainer.append(testRule.getDOM()); - expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(2); - }); - - it('responds to input of style properties, and updates the preview', function () { - testRule.colorInputs['background-color'].set('#434343'); - expect(mockRuleConfig.style['background-color']).toEqual('#434343'); - testRule.colorInputs['border-color'].set('#666666'); - expect(mockRuleConfig.style['border-color']).toEqual('#666666'); - testRule.colorInputs.color.set('#999999'); - expect(mockRuleConfig.style.color).toEqual('#999999'); - - expect(testRule.thumbnail.style['background-color']).toEqual('rgb(67, 67, 67)'); - expect(testRule.thumbnail.style['border-color']).toEqual('rgb(102, 102, 102)'); - expect(testRule.thumbnail.style.color).toEqual('rgb(153, 153, 153)'); - - expect(changeSpy).toHaveBeenCalled(); - }); - - it('responds to input for the icon property', function () { - testRule.iconInput.set('icon-alert-rect'); - expect(mockRuleConfig.icon).toEqual('icon-alert-rect'); - expect(changeSpy).toHaveBeenCalled(); - }); - - /* - test for js condition commented out for v1 - */ - - // it('responds to input of text properties', function () { - // var testInputs = ['name', 'label', 'message', 'jsCondition'], - // input; - - // testInputs.forEach(function (key) { - // input = testRule.textInputs[key]; - // input.prop('value', 'A new ' + key); - // input.trigger('input'); - // expect(mockRuleConfig[key]).toEqual('A new ' + key); - // }); - - // expect(changeSpy).toHaveBeenCalled(); - // }); - - it('allows input for when the rule triggers', function () { - testRule.trigger.value = 'all'; - const event = new Event('change', { - bubbles: true, - cancelable: true - }); - testRule.trigger.dispatchEvent(event); - expect(testRule.config.trigger).toEqual('all'); - expect(conditionChangeSpy).toHaveBeenCalled(); - }); - - it('generates a human-readable description from its conditions', function () { - testRule.generateDescription(); - expect(testRule.config.description).toContain( - 'Object Name\'s Property Name Operation Description' - ); - testRule.config.trigger = 'js'; - testRule.generateDescription(); - expect(testRule.config.description).toContain( - 'when a custom JavaScript condition evaluates to true' - ); - }); - - it('initiates a drag event when its grippy is clicked', function () { - const event = new Event('mousedown', { - bubbles: true, - cancelable: true - }); - testRule.grippy.dispatchEvent(event); - - expect(mockWidgetDnD.setDragImage).toHaveBeenCalled(); - expect(mockWidgetDnD.dragStart).toHaveBeenCalledWith('mockRule'); - }); - - /* - test for js condition commented out for v1 - */ - - it('can remove a condition from its configuration', function () { - testRule.removeCondition(0); - expect(testRule.config.conditions).toEqual([{ - object: 'blah', - key: 'blah', - operation: 'blah', - values: ['blah.', 'blah!', 'blah?'] - }]); - }); + testRule = new Rule( + mockRuleConfig, + mockDomainObject, + mockOpenMCT, + mockConditionManager, + mockWidgetDnD + ); + testRule.on('remove', removeSpy); + testRule.on('duplicate', duplicateSpy); + testRule.on('change', changeSpy); + testRule.on('conditionChange', conditionChangeSpy); }); + + it('closes its configuration panel on initial load', function () { + expect(testRule.getProperty('expanded')).toEqual(false); + }); + + it('gets its DOM element', function () { + mockContainer.append(testRule.getDOM()); + expect(mockContainer.querySelectorAll('.l-widget-rule').length).toBeGreaterThan(0); + }); + + it('gets its configuration properties', function () { + expect(testRule.getProperty('name')).toEqual('Name'); + expect(testRule.getProperty('icon')).toEqual('test-icon-name'); + }); + + it('can duplicate itself', function () { + testRule.duplicate(); + mockRuleConfig.expanded = true; + expect(duplicateSpy).toHaveBeenCalledWith(mockRuleConfig); + }); + + it('can remove itself from the configuration', function () { + testRule.remove(); + expect(removeSpy).toHaveBeenCalled(); + expect(mockDomainObject.configuration.ruleConfigById.mockRule).not.toBeDefined(); + expect(mockDomainObject.configuration.ruleOrder).toEqual(['default', 'otherRule']); + }); + + it('updates its configuration on a condition change and invokes callbacks', function () { + testRule.onConditionChange({ + value: 'newValue', + property: 'object', + index: 0 + }); + expect(testRule.getProperty('conditions')[0].object).toEqual('newValue'); + expect(conditionChangeSpy).toHaveBeenCalled(); + }); + + it('allows initializing a new condition with a default configuration', function () { + testRule.initCondition(); + expect(mockRuleConfig.conditions).toEqual([ + { + object: '', + key: '', + operation: '', + values: [] + }, + { + object: 'blah', + key: 'blah', + operation: 'blah', + values: ['blah.', 'blah!', 'blah?'] + }, + { + object: '', + key: '', + operation: '', + values: [] + } + ]); + }); + + it('allows initializing a new condition from a given configuration', function () { + testRule.initCondition({ + sourceCondition: { + object: 'object1', + key: 'key1', + operation: 'operation1', + values: [1, 2, 3] + }, + index: 0 + }); + expect(mockRuleConfig.conditions).toEqual([ + { + object: '', + key: '', + operation: '', + values: [] + }, + { + object: 'object1', + key: 'key1', + operation: 'operation1', + values: [1, 2, 3] + }, + { + object: 'blah', + key: 'blah', + operation: 'blah', + values: ['blah.', 'blah!', 'blah?'] + } + ]); + }); + + it('invokes mutate when updating the domain object', function () { + testRule.updateDomainObject(); + expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); + }); + + it('builds condition view from condition configuration', function () { + mockContainer.append(testRule.getDOM()); + expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(2); + }); + + it('responds to input of style properties, and updates the preview', function () { + testRule.colorInputs['background-color'].set('#434343'); + expect(mockRuleConfig.style['background-color']).toEqual('#434343'); + testRule.colorInputs['border-color'].set('#666666'); + expect(mockRuleConfig.style['border-color']).toEqual('#666666'); + testRule.colorInputs.color.set('#999999'); + expect(mockRuleConfig.style.color).toEqual('#999999'); + + expect(testRule.thumbnail.style['background-color']).toEqual('rgb(67, 67, 67)'); + expect(testRule.thumbnail.style['border-color']).toEqual('rgb(102, 102, 102)'); + expect(testRule.thumbnail.style.color).toEqual('rgb(153, 153, 153)'); + + expect(changeSpy).toHaveBeenCalled(); + }); + + it('responds to input for the icon property', function () { + testRule.iconInput.set('icon-alert-rect'); + expect(mockRuleConfig.icon).toEqual('icon-alert-rect'); + expect(changeSpy).toHaveBeenCalled(); + }); + + /* + test for js condition commented out for v1 + */ + + // it('responds to input of text properties', function () { + // var testInputs = ['name', 'label', 'message', 'jsCondition'], + // input; + + // testInputs.forEach(function (key) { + // input = testRule.textInputs[key]; + // input.prop('value', 'A new ' + key); + // input.trigger('input'); + // expect(mockRuleConfig[key]).toEqual('A new ' + key); + // }); + + // expect(changeSpy).toHaveBeenCalled(); + // }); + + it('allows input for when the rule triggers', function () { + testRule.trigger.value = 'all'; + const event = new Event('change', { + bubbles: true, + cancelable: true + }); + testRule.trigger.dispatchEvent(event); + expect(testRule.config.trigger).toEqual('all'); + expect(conditionChangeSpy).toHaveBeenCalled(); + }); + + it('generates a human-readable description from its conditions', function () { + testRule.generateDescription(); + expect(testRule.config.description).toContain( + "Object Name's Property Name Operation Description" + ); + testRule.config.trigger = 'js'; + testRule.generateDescription(); + expect(testRule.config.description).toContain( + 'when a custom JavaScript condition evaluates to true' + ); + }); + + it('initiates a drag event when its grippy is clicked', function () { + const event = new Event('mousedown', { + bubbles: true, + cancelable: true + }); + testRule.grippy.dispatchEvent(event); + + expect(mockWidgetDnD.setDragImage).toHaveBeenCalled(); + expect(mockWidgetDnD.dragStart).toHaveBeenCalledWith('mockRule'); + }); + + /* + test for js condition commented out for v1 + */ + + it('can remove a condition from its configuration', function () { + testRule.removeCondition(0); + expect(testRule.config.conditions).toEqual([ + { + object: 'blah', + key: 'blah', + operation: 'blah', + values: ['blah.', 'blah!', 'blah?'] + } + ]); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/SummaryWidgetSpec.js b/src/plugins/summaryWidget/test/SummaryWidgetSpec.js index 819eb0b069..94b7646514 100644 --- a/src/plugins/summaryWidget/test/SummaryWidgetSpec.js +++ b/src/plugins/summaryWidget/test/SummaryWidgetSpec.js @@ -21,175 +21,172 @@ *****************************************************************************/ define(['../src/SummaryWidget'], function (SummaryWidget) { - xdescribe('The Summary Widget', function () { - let summaryWidget; - let mockDomainObject; - let mockOldDomainObject; - let mockOpenMCT; - let mockObjectService; - let mockStatusCapability; - let mockComposition; - let mockContainer; - let listenCallback; - let listenCallbackSpy; + xdescribe('The Summary Widget', function () { + let summaryWidget; + let mockDomainObject; + let mockOldDomainObject; + let mockOpenMCT; + let mockObjectService; + let mockStatusCapability; + let mockComposition; + let mockContainer; + let listenCallback; + let listenCallbackSpy; - beforeEach(function () { - mockDomainObject = { - identifier: { - key: 'testKey', - namespace: 'testNamespace' - }, - name: 'testName', - composition: [], - configuration: {} - }; - mockComposition = jasmine.createSpyObj('composition', [ - 'on', - 'off', - 'load' - ]); - mockStatusCapability = jasmine.createSpyObj('statusCapability', [ - 'get', - 'listen', - 'triggerCallback' - ]); + beforeEach(function () { + mockDomainObject = { + identifier: { + key: 'testKey', + namespace: 'testNamespace' + }, + name: 'testName', + composition: [], + configuration: {} + }; + mockComposition = jasmine.createSpyObj('composition', ['on', 'off', 'load']); + mockStatusCapability = jasmine.createSpyObj('statusCapability', [ + 'get', + 'listen', + 'triggerCallback' + ]); - listenCallbackSpy = jasmine.createSpy('listenCallbackSpy', function () {}); - mockStatusCapability.get.and.returnValue([]); - mockStatusCapability.listen.and.callFake(function (callback) { - listenCallback = callback; + listenCallbackSpy = jasmine.createSpy('listenCallbackSpy', function () {}); + mockStatusCapability.get.and.returnValue([]); + mockStatusCapability.listen.and.callFake(function (callback) { + listenCallback = callback; - return listenCallbackSpy; - }); - mockStatusCapability.triggerCallback.and.callFake(function () { - listenCallback(['editing']); - }); + return listenCallbackSpy; + }); + mockStatusCapability.triggerCallback.and.callFake(function () { + listenCallback(['editing']); + }); - mockOldDomainObject = {}; - mockOldDomainObject.getCapability = jasmine.createSpy('capability'); - mockOldDomainObject.getCapability.and.returnValue(mockStatusCapability); + mockOldDomainObject = {}; + mockOldDomainObject.getCapability = jasmine.createSpy('capability'); + mockOldDomainObject.getCapability.and.returnValue(mockStatusCapability); - mockObjectService = {}; - mockObjectService.getObjects = jasmine.createSpy('objectService'); - mockObjectService.getObjects.and.returnValue(new Promise(function (resolve, reject) { - resolve({ - 'testNamespace:testKey': mockOldDomainObject - }); - })); - mockOpenMCT = jasmine.createSpyObj('openmct', [ - '$injector', - 'composition', - 'objects' - ]); - mockOpenMCT.$injector.get = jasmine.createSpy('get'); - mockOpenMCT.$injector.get.and.returnValue(mockObjectService); - mockOpenMCT.composition = jasmine.createSpyObj('composition', [ - 'get', - 'on' - ]); - mockOpenMCT.composition.get.and.returnValue(mockComposition); - mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); - mockOpenMCT.objects.observe = jasmine.createSpy('observe'); - mockOpenMCT.objects.observe.and.returnValue(function () {}); + mockObjectService = {}; + mockObjectService.getObjects = jasmine.createSpy('objectService'); + mockObjectService.getObjects.and.returnValue( + new Promise(function (resolve, reject) { + resolve({ + 'testNamespace:testKey': mockOldDomainObject + }); + }) + ); + mockOpenMCT = jasmine.createSpyObj('openmct', ['$injector', 'composition', 'objects']); + mockOpenMCT.$injector.get = jasmine.createSpy('get'); + mockOpenMCT.$injector.get.and.returnValue(mockObjectService); + mockOpenMCT.composition = jasmine.createSpyObj('composition', ['get', 'on']); + mockOpenMCT.composition.get.and.returnValue(mockComposition); + mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); + mockOpenMCT.objects.observe = jasmine.createSpy('observe'); + mockOpenMCT.objects.observe.and.returnValue(function () {}); - summaryWidget = new SummaryWidget(mockDomainObject, mockOpenMCT); - mockContainer = document.createElement('div'); - summaryWidget.show(mockContainer); - }); - - it('queries with legacyId', function () { - expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']); - }); - - it('adds its DOM element to the view', function () { - expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0); - }); - - it('initialzes a default rule', function () { - expect(mockDomainObject.configuration.ruleConfigById.default).toBeDefined(); - expect(mockDomainObject.configuration.ruleOrder).toEqual(['default']); - }); - - it('builds rules and rule placeholders in view from configuration', function () { - expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(2); - }); - - it('allows initializing a new rule with a particular identifier', function () { - summaryWidget.initRule('rule0', 'Rule'); - expect(mockDomainObject.configuration.ruleConfigById.rule0).toBeDefined(); - }); - - it('allows adding a new rule with a unique identifier to the configuration and view', function () { - summaryWidget.addRule(); - expect(mockDomainObject.configuration.ruleOrder.length).toEqual(2); - mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) { - expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined(); - }); - summaryWidget.addRule(); - expect(mockDomainObject.configuration.ruleOrder.length).toEqual(3); - mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) { - expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined(); - }); - expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(6); - }); - - it('allows duplicating a rule from source configuration', function () { - const sourceConfig = JSON.parse(JSON.stringify(mockDomainObject.configuration.ruleConfigById.default)); - summaryWidget.duplicateRule(sourceConfig); - expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(2); - }); - - it('does not duplicate an existing rule in the configuration', function () { - summaryWidget.initRule('default', 'Default'); - expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(1); - }); - - it('uses mutate when updating the domain object only when in edit mode', function () { - summaryWidget.editing = true; - summaryWidget.updateDomainObject(); - expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); - }); - - it('shows configuration interfaces when in edit mode, and hides them otherwise', function () { - setTimeout(function () { - summaryWidget.onEdit([]); - expect(summaryWidget.editing).toEqual(false); - expect(summaryWidget.ruleArea.css('display')).toEqual('none'); - expect(summaryWidget.testDataArea.css('display')).toEqual('none'); - expect(summaryWidget.addRuleButton.css('display')).toEqual('none'); - summaryWidget.onEdit(['editing']); - expect(summaryWidget.editing).toEqual(true); - expect(summaryWidget.ruleArea.css('display')).not.toEqual('none'); - expect(summaryWidget.testDataArea.css('display')).not.toEqual('none'); - expect(summaryWidget.addRuleButton.css('display')).not.toEqual('none'); - }, 100); - }); - - it('unregisters any registered listeners on a destroy', function () { - setTimeout(function () { - summaryWidget.destroy(); - expect(listenCallbackSpy).toHaveBeenCalled(); - }, 100); - }); - - it('allows reorders of rules', function () { - summaryWidget.initRule('rule0'); - summaryWidget.initRule('rule1'); - summaryWidget.domainObject.configuration.ruleOrder = ['default', 'rule0', 'rule1']; - summaryWidget.reorder({ - draggingId: 'rule1', - dropTarget: 'default' - }); - expect(summaryWidget.domainObject.configuration.ruleOrder).toEqual(['default', 'rule1', 'rule0']); - }); - - it('adds hyperlink to the widget button and sets newTab preference', function () { - summaryWidget.addHyperlink('https://www.nasa.gov', 'newTab'); - - const widgetButton = mockContainer.querySelector('#widget'); - - expect(widgetButton.href).toEqual('https://www.nasa.gov/'); - expect(widgetButton.target).toEqual('_blank'); - }); + summaryWidget = new SummaryWidget(mockDomainObject, mockOpenMCT); + mockContainer = document.createElement('div'); + summaryWidget.show(mockContainer); }); + + it('queries with legacyId', function () { + expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']); + }); + + it('adds its DOM element to the view', function () { + expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0); + }); + + it('initialzes a default rule', function () { + expect(mockDomainObject.configuration.ruleConfigById.default).toBeDefined(); + expect(mockDomainObject.configuration.ruleOrder).toEqual(['default']); + }); + + it('builds rules and rule placeholders in view from configuration', function () { + expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(2); + }); + + it('allows initializing a new rule with a particular identifier', function () { + summaryWidget.initRule('rule0', 'Rule'); + expect(mockDomainObject.configuration.ruleConfigById.rule0).toBeDefined(); + }); + + it('allows adding a new rule with a unique identifier to the configuration and view', function () { + summaryWidget.addRule(); + expect(mockDomainObject.configuration.ruleOrder.length).toEqual(2); + mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) { + expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined(); + }); + summaryWidget.addRule(); + expect(mockDomainObject.configuration.ruleOrder.length).toEqual(3); + mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) { + expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined(); + }); + expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(6); + }); + + it('allows duplicating a rule from source configuration', function () { + const sourceConfig = JSON.parse( + JSON.stringify(mockDomainObject.configuration.ruleConfigById.default) + ); + summaryWidget.duplicateRule(sourceConfig); + expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(2); + }); + + it('does not duplicate an existing rule in the configuration', function () { + summaryWidget.initRule('default', 'Default'); + expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(1); + }); + + it('uses mutate when updating the domain object only when in edit mode', function () { + summaryWidget.editing = true; + summaryWidget.updateDomainObject(); + expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); + }); + + it('shows configuration interfaces when in edit mode, and hides them otherwise', function () { + setTimeout(function () { + summaryWidget.onEdit([]); + expect(summaryWidget.editing).toEqual(false); + expect(summaryWidget.ruleArea.css('display')).toEqual('none'); + expect(summaryWidget.testDataArea.css('display')).toEqual('none'); + expect(summaryWidget.addRuleButton.css('display')).toEqual('none'); + summaryWidget.onEdit(['editing']); + expect(summaryWidget.editing).toEqual(true); + expect(summaryWidget.ruleArea.css('display')).not.toEqual('none'); + expect(summaryWidget.testDataArea.css('display')).not.toEqual('none'); + expect(summaryWidget.addRuleButton.css('display')).not.toEqual('none'); + }, 100); + }); + + it('unregisters any registered listeners on a destroy', function () { + setTimeout(function () { + summaryWidget.destroy(); + expect(listenCallbackSpy).toHaveBeenCalled(); + }, 100); + }); + + it('allows reorders of rules', function () { + summaryWidget.initRule('rule0'); + summaryWidget.initRule('rule1'); + summaryWidget.domainObject.configuration.ruleOrder = ['default', 'rule0', 'rule1']; + summaryWidget.reorder({ + draggingId: 'rule1', + dropTarget: 'default' + }); + expect(summaryWidget.domainObject.configuration.ruleOrder).toEqual([ + 'default', + 'rule1', + 'rule0' + ]); + }); + + it('adds hyperlink to the widget button and sets newTab preference', function () { + summaryWidget.addHyperlink('https://www.nasa.gov', 'newTab'); + + const widgetButton = mockContainer.querySelector('#widget'); + + expect(widgetButton.href).toEqual('https://www.nasa.gov/'); + expect(widgetButton.target).toEqual('_blank'); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/SummaryWidgetViewPolicySpec.js b/src/plugins/summaryWidget/test/SummaryWidgetViewPolicySpec.js index 431de187dd..e10a5caaa4 100644 --- a/src/plugins/summaryWidget/test/SummaryWidgetViewPolicySpec.js +++ b/src/plugins/summaryWidget/test/SummaryWidgetViewPolicySpec.js @@ -20,47 +20,39 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - '../SummaryWidgetViewPolicy' -], function ( - SummaryWidgetViewPolicy -) { - - describe('SummaryWidgetViewPolicy', function () { - let policy; - let domainObject; - let view; - beforeEach(function () { - policy = new SummaryWidgetViewPolicy(); - domainObject = jasmine.createSpyObj('domainObject', [ - 'getModel' - ]); - domainObject.getModel.and.returnValue({}); - view = {}; - }); - - it('returns true for other object types', function () { - domainObject.getModel.and.returnValue({ - type: 'random' - }); - expect(policy.allow(view, domainObject)).toBe(true); - }); - - it('allows summary widget view for summary widgets', function () { - domainObject.getModel.and.returnValue({ - type: 'summary-widget' - }); - view.key = 'summary-widget-viewer'; - expect(policy.allow(view, domainObject)).toBe(true); - }); - - it('disallows other views for summary widgets', function () { - domainObject.getModel.and.returnValue({ - type: 'summary-widget' - }); - view.key = 'other view'; - expect(policy.allow(view, domainObject)).toBe(false); - }); - +define(['../SummaryWidgetViewPolicy'], function (SummaryWidgetViewPolicy) { + describe('SummaryWidgetViewPolicy', function () { + let policy; + let domainObject; + let view; + beforeEach(function () { + policy = new SummaryWidgetViewPolicy(); + domainObject = jasmine.createSpyObj('domainObject', ['getModel']); + domainObject.getModel.and.returnValue({}); + view = {}; }); + + it('returns true for other object types', function () { + domainObject.getModel.and.returnValue({ + type: 'random' + }); + expect(policy.allow(view, domainObject)).toBe(true); + }); + + it('allows summary widget view for summary widgets', function () { + domainObject.getModel.and.returnValue({ + type: 'summary-widget' + }); + view.key = 'summary-widget-viewer'; + expect(policy.allow(view, domainObject)).toBe(true); + }); + + it('disallows other views for summary widgets', function () { + domainObject.getModel.and.returnValue({ + type: 'summary-widget' + }); + view.key = 'other view'; + expect(policy.allow(view, domainObject)).toBe(false); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/TestDataItemSpec.js b/src/plugins/summaryWidget/test/TestDataItemSpec.js index 171753efe4..3e9ac3d31f 100644 --- a/src/plugins/summaryWidget/test/TestDataItemSpec.js +++ b/src/plugins/summaryWidget/test/TestDataItemSpec.js @@ -1,167 +1,167 @@ define(['../src/TestDataItem'], function (TestDataItem) { - describe('A summary widget test data item', function () { - let testDataItem; - let mockConfig; - let mockConditionManager; - let mockContainer; - let mockEvaluator; - let changeSpy; - let duplicateSpy; - let removeSpy; - let generateValueSpy; + describe('A summary widget test data item', function () { + let testDataItem; + let mockConfig; + let mockConditionManager; + let mockContainer; + let mockEvaluator; + let changeSpy; + let duplicateSpy; + let removeSpy; + let generateValueSpy; - beforeEach(function () { - mockContainer = document.createElement('div'); + beforeEach(function () { + mockContainer = document.createElement('div'); - mockConfig = { - object: 'object1', - key: 'property1', - value: 1 - }; + mockConfig = { + object: 'object1', + key: 'property1', + value: 1 + }; - mockEvaluator = {}; - mockEvaluator.getInputTypeById = jasmine.createSpy('inputType'); + mockEvaluator = {}; + mockEvaluator.getInputTypeById = jasmine.createSpy('inputType'); - mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ - 'on', - 'getComposition', - 'loadCompleted', - 'getEvaluator', - 'getTelemetryMetadata', - 'metadataLoadCompleted', - 'getObjectName', - 'getTelemetryPropertyName', - 'getTelemetryPropertyType' - ]); - mockConditionManager.loadCompleted.and.returnValue(false); - mockConditionManager.metadataLoadCompleted.and.returnValue(false); - mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); - mockConditionManager.getComposition.and.returnValue({}); - mockConditionManager.getTelemetryMetadata.and.returnValue({}); - mockConditionManager.getObjectName.and.returnValue('Object Name'); - mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); - mockConditionManager.getTelemetryPropertyType.and.returnValue(''); + mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ + 'on', + 'getComposition', + 'loadCompleted', + 'getEvaluator', + 'getTelemetryMetadata', + 'metadataLoadCompleted', + 'getObjectName', + 'getTelemetryPropertyName', + 'getTelemetryPropertyType' + ]); + mockConditionManager.loadCompleted.and.returnValue(false); + mockConditionManager.metadataLoadCompleted.and.returnValue(false); + mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); + mockConditionManager.getComposition.and.returnValue({}); + mockConditionManager.getTelemetryMetadata.and.returnValue({}); + mockConditionManager.getObjectName.and.returnValue('Object Name'); + mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); + mockConditionManager.getTelemetryPropertyType.and.returnValue(''); - duplicateSpy = jasmine.createSpy('duplicate'); - removeSpy = jasmine.createSpy('remove'); - changeSpy = jasmine.createSpy('change'); - generateValueSpy = jasmine.createSpy('generateValueInput'); + duplicateSpy = jasmine.createSpy('duplicate'); + removeSpy = jasmine.createSpy('remove'); + changeSpy = jasmine.createSpy('change'); + generateValueSpy = jasmine.createSpy('generateValueInput'); - testDataItem = new TestDataItem(mockConfig, 54, mockConditionManager); + testDataItem = new TestDataItem(mockConfig, 54, mockConditionManager); - testDataItem.on('duplicate', duplicateSpy); - testDataItem.on('remove', removeSpy); - testDataItem.on('change', changeSpy); - }); - - it('exposes a DOM element to represent itself in the view', function () { - mockContainer.append(testDataItem.getDOM()); - expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(1); - }); - - it('responds to a change in its object select', function () { - testDataItem.selects.object.setSelected(''); - expect(changeSpy).toHaveBeenCalledWith({ - value: '', - property: 'object', - index: 54 - }); - }); - - it('responds to a change in its key select', function () { - testDataItem.generateValueInput = generateValueSpy; - testDataItem.selects.key.setSelected(''); - expect(changeSpy).toHaveBeenCalledWith({ - value: '', - property: 'key', - index: 54 - }); - expect(generateValueSpy).toHaveBeenCalledWith(''); - }); - - it('generates a value input of the appropriate type', function () { - let inputs; - - mockContainer.append(testDataItem.getDOM()); - mockEvaluator.getInputTypeById.and.returnValue('number'); - testDataItem.generateValueInput(''); - - inputs = mockContainer.querySelectorAll('input'); - const numberInputs = Array.from(inputs).filter(input => input.type === 'number'); - - expect(numberInputs.length).toEqual(1); - expect(inputs[0].valueAsNumber).toEqual(1); - - mockEvaluator.getInputTypeById.and.returnValue('text'); - testDataItem.config.value = 'Text I Am'; - testDataItem.generateValueInput(''); - - inputs = mockContainer.querySelectorAll('input'); - const textInputs = Array.from(inputs).filter(input => input.type === 'text'); - - expect(textInputs.length).toEqual(1); - expect(inputs[0].value).toEqual('Text I Am'); - }); - - it('ensures reasonable defaults on values if none are provided', function () { - let inputs; - - mockContainer.append(testDataItem.getDOM()); - - mockEvaluator.getInputTypeById.and.returnValue('number'); - testDataItem.config.value = undefined; - testDataItem.generateValueInput(''); - - inputs = mockContainer.querySelectorAll('input'); - const numberInputs = Array.from(inputs).filter(input => input.type === 'number'); - - expect(numberInputs.length).toEqual(1); - expect(inputs[0].valueAsNumber).toEqual(0); - expect(testDataItem.config.value).toEqual(0); - - mockEvaluator.getInputTypeById.and.returnValue('text'); - testDataItem.config.value = undefined; - testDataItem.generateValueInput(''); - - inputs = mockContainer.querySelectorAll('input'); - const textInputs = Array.from(inputs).filter(input => input.type === 'text'); - - expect(textInputs.length).toEqual(1); - expect(inputs[0].value).toEqual(''); - expect(testDataItem.config.value).toEqual(''); - }); - - it('responds to a change in its value inputs', function () { - mockContainer.append(testDataItem.getDOM()); - mockEvaluator.getInputTypeById.and.returnValue('number'); - testDataItem.generateValueInput(''); - - const event = new Event('input', { - bubbles: true, - cancelable: true - }); - - mockContainer.querySelector('input').value = 9001; - mockContainer.querySelector('input').dispatchEvent(event); - - expect(changeSpy).toHaveBeenCalledWith({ - value: 9001, - property: 'value', - index: 54 - }); - }); - - it('can remove itself from the configuration', function () { - testDataItem.remove(); - expect(removeSpy).toHaveBeenCalledWith(54); - }); - - it('can duplicate itself', function () { - testDataItem.duplicate(); - expect(duplicateSpy).toHaveBeenCalledWith({ - sourceItem: mockConfig, - index: 54 - }); - }); + testDataItem.on('duplicate', duplicateSpy); + testDataItem.on('remove', removeSpy); + testDataItem.on('change', changeSpy); }); + + it('exposes a DOM element to represent itself in the view', function () { + mockContainer.append(testDataItem.getDOM()); + expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(1); + }); + + it('responds to a change in its object select', function () { + testDataItem.selects.object.setSelected(''); + expect(changeSpy).toHaveBeenCalledWith({ + value: '', + property: 'object', + index: 54 + }); + }); + + it('responds to a change in its key select', function () { + testDataItem.generateValueInput = generateValueSpy; + testDataItem.selects.key.setSelected(''); + expect(changeSpy).toHaveBeenCalledWith({ + value: '', + property: 'key', + index: 54 + }); + expect(generateValueSpy).toHaveBeenCalledWith(''); + }); + + it('generates a value input of the appropriate type', function () { + let inputs; + + mockContainer.append(testDataItem.getDOM()); + mockEvaluator.getInputTypeById.and.returnValue('number'); + testDataItem.generateValueInput(''); + + inputs = mockContainer.querySelectorAll('input'); + const numberInputs = Array.from(inputs).filter((input) => input.type === 'number'); + + expect(numberInputs.length).toEqual(1); + expect(inputs[0].valueAsNumber).toEqual(1); + + mockEvaluator.getInputTypeById.and.returnValue('text'); + testDataItem.config.value = 'Text I Am'; + testDataItem.generateValueInput(''); + + inputs = mockContainer.querySelectorAll('input'); + const textInputs = Array.from(inputs).filter((input) => input.type === 'text'); + + expect(textInputs.length).toEqual(1); + expect(inputs[0].value).toEqual('Text I Am'); + }); + + it('ensures reasonable defaults on values if none are provided', function () { + let inputs; + + mockContainer.append(testDataItem.getDOM()); + + mockEvaluator.getInputTypeById.and.returnValue('number'); + testDataItem.config.value = undefined; + testDataItem.generateValueInput(''); + + inputs = mockContainer.querySelectorAll('input'); + const numberInputs = Array.from(inputs).filter((input) => input.type === 'number'); + + expect(numberInputs.length).toEqual(1); + expect(inputs[0].valueAsNumber).toEqual(0); + expect(testDataItem.config.value).toEqual(0); + + mockEvaluator.getInputTypeById.and.returnValue('text'); + testDataItem.config.value = undefined; + testDataItem.generateValueInput(''); + + inputs = mockContainer.querySelectorAll('input'); + const textInputs = Array.from(inputs).filter((input) => input.type === 'text'); + + expect(textInputs.length).toEqual(1); + expect(inputs[0].value).toEqual(''); + expect(testDataItem.config.value).toEqual(''); + }); + + it('responds to a change in its value inputs', function () { + mockContainer.append(testDataItem.getDOM()); + mockEvaluator.getInputTypeById.and.returnValue('number'); + testDataItem.generateValueInput(''); + + const event = new Event('input', { + bubbles: true, + cancelable: true + }); + + mockContainer.querySelector('input').value = 9001; + mockContainer.querySelector('input').dispatchEvent(event); + + expect(changeSpy).toHaveBeenCalledWith({ + value: 9001, + property: 'value', + index: 54 + }); + }); + + it('can remove itself from the configuration', function () { + testDataItem.remove(); + expect(removeSpy).toHaveBeenCalledWith(54); + }); + + it('can duplicate itself', function () { + testDataItem.duplicate(); + expect(duplicateSpy).toHaveBeenCalledWith({ + sourceItem: mockConfig, + index: 54 + }); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/TestDataManagerSpec.js b/src/plugins/summaryWidget/test/TestDataManagerSpec.js index 59ce37d92c..3cf488ca09 100644 --- a/src/plugins/summaryWidget/test/TestDataManagerSpec.js +++ b/src/plugins/summaryWidget/test/TestDataManagerSpec.js @@ -1,230 +1,250 @@ define(['../src/TestDataManager'], function (TestDataManager) { - describe('A Summary Widget Rule', function () { - let mockDomainObject; - let mockOpenMCT; - let mockConditionManager; - let mockEvaluator; - let mockContainer; - let mockTelemetryMetadata; - let testDataManager; - let mockCompObject1; - let mockCompObject2; + describe('A Summary Widget Rule', function () { + let mockDomainObject; + let mockOpenMCT; + let mockConditionManager; + let mockEvaluator; + let mockContainer; + let mockTelemetryMetadata; + let testDataManager; + let mockCompObject1; + let mockCompObject2; - beforeEach(function () { - mockDomainObject = { - configuration: { - testDataConfig: [{ - object: '', - key: '', - value: '' - }, { - object: 'object1', - key: 'property1', - value: 66 - }, { - object: 'object2', - key: 'property4', - value: 'Text It Is' - }] - }, - composition: [{ - object1: { - key: 'object1', - name: 'Object 1' - }, - object2: { - key: 'object2', - name: 'Object 2' - } - }] - }; + beforeEach(function () { + mockDomainObject = { + configuration: { + testDataConfig: [ + { + object: '', + key: '', + value: '' + }, + { + object: 'object1', + key: 'property1', + value: 66 + }, + { + object: 'object2', + key: 'property4', + value: 'Text It Is' + } + ] + }, + composition: [ + { + object1: { + key: 'object1', + name: 'Object 1' + }, + object2: { + key: 'object2', + name: 'Object 2' + } + } + ] + }; - mockTelemetryMetadata = { - object1: { - property1: { - key: 'property1' - }, - property2: { - key: 'property2' - } - }, - object2: { - property3: { - key: 'property3' - }, - property4: { - key: 'property4' - } - } - }; + mockTelemetryMetadata = { + object1: { + property1: { + key: 'property1' + }, + property2: { + key: 'property2' + } + }, + object2: { + property3: { + key: 'property3' + }, + property4: { + key: 'property4' + } + } + }; - mockCompObject1 = { - identifier: { - key: 'object1' - }, - name: 'Object 1' - }; - mockCompObject2 = { - identifier: { - key: 'object2' - }, - name: 'Object 2' - }; + mockCompObject1 = { + identifier: { + key: 'object1' + }, + name: 'Object 1' + }; + mockCompObject2 = { + identifier: { + key: 'object2' + }, + name: 'Object 2' + }; - mockOpenMCT = {}; - mockOpenMCT.objects = {}; - mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); + mockOpenMCT = {}; + mockOpenMCT.objects = {}; + mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); - mockEvaluator = {}; - mockEvaluator.setTestDataCache = jasmine.createSpy('testDataCache'); - mockEvaluator.useTestData = jasmine.createSpy('useTestData'); + mockEvaluator = {}; + mockEvaluator.setTestDataCache = jasmine.createSpy('testDataCache'); + mockEvaluator.useTestData = jasmine.createSpy('useTestData'); - mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ - 'on', - 'getComposition', - 'loadCompleted', - 'getEvaluator', - 'getTelemetryMetadata', - 'metadataLoadCompleted', - 'getObjectName', - 'getTelemetryPropertyName', - 'triggerTelemetryCallback' - ]); - mockConditionManager.loadCompleted.and.returnValue(false); - mockConditionManager.metadataLoadCompleted.and.returnValue(false); - mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); - mockConditionManager.getComposition.and.returnValue({ - object1: mockCompObject1, - object2: mockCompObject2 - }); - mockConditionManager.getTelemetryMetadata.and.callFake(function (id) { - return mockTelemetryMetadata[id]; - }); - mockConditionManager.getObjectName.and.returnValue('Object Name'); - mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); + mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ + 'on', + 'getComposition', + 'loadCompleted', + 'getEvaluator', + 'getTelemetryMetadata', + 'metadataLoadCompleted', + 'getObjectName', + 'getTelemetryPropertyName', + 'triggerTelemetryCallback' + ]); + mockConditionManager.loadCompleted.and.returnValue(false); + mockConditionManager.metadataLoadCompleted.and.returnValue(false); + mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); + mockConditionManager.getComposition.and.returnValue({ + object1: mockCompObject1, + object2: mockCompObject2 + }); + mockConditionManager.getTelemetryMetadata.and.callFake(function (id) { + return mockTelemetryMetadata[id]; + }); + mockConditionManager.getObjectName.and.returnValue('Object Name'); + mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); - mockContainer = document.createElement('div'); + mockContainer = document.createElement('div'); - testDataManager = new TestDataManager(mockDomainObject, mockConditionManager, mockOpenMCT); - }); - - it('closes its configuration panel on initial load', function () { - - }); - - it('exposes a DOM element to represent itself in the view', function () { - mockContainer.append(testDataManager.getDOM()); - expect(mockContainer.querySelectorAll('.t-widget-test-data-content').length).toBeGreaterThan(0); - }); - - it('generates a test cache in the format expected by a condition evaluator', function () { - testDataManager.updateTestCache(); - expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({ - object1: { - property1: 66, - property2: '' - }, - object2: { - property3: '', - property4: 'Text It Is' - } - }); - }); - - it('updates its configuration on a item change and provides an updated' - + 'cache to the evaluator', function () { - testDataManager.onItemChange({ - value: 26, - property: 'value', - index: 1 - }); - expect(testDataManager.config[1].value).toEqual(26); - expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({ - object1: { - property1: 26, - property2: '' - }, - object2: { - property3: '', - property4: 'Text It Is' - } - }); - }); - - it('allows initializing a new item with a default configuration', function () { - testDataManager.initItem(); - expect(mockDomainObject.configuration.testDataConfig).toEqual([{ - object: '', - key: '', - value: '' - }, { - object: 'object1', - key: 'property1', - value: 66 - }, { - object: 'object2', - key: 'property4', - value: 'Text It Is' - }, { - object: '', - key: '', - value: '' - }]); - }); - - it('allows initializing a new item from a given configuration', function () { - testDataManager.initItem({ - sourceItem: { - object: 'object2', - key: 'property3', - value: 1 - }, - index: 0 - }); - expect(mockDomainObject.configuration.testDataConfig).toEqual([{ - object: '', - key: '', - value: '' - }, { - object: 'object2', - key: 'property3', - value: 1 - }, { - object: 'object1', - key: 'property1', - value: 66 - }, { - object: 'object2', - key: 'property4', - value: 'Text It Is' - }]); - }); - - it('invokes mutate when updating the domain object', function () { - testDataManager.updateDomainObject(); - expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); - }); - - it('builds item view from item configuration', function () { - mockContainer.append(testDataManager.getDOM()); - expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(3); - }); - - it('can remove a item from its configuration', function () { - testDataManager.removeItem(0); - expect(mockDomainObject.configuration.testDataConfig).toEqual([{ - object: 'object1', - key: 'property1', - value: 66 - }, { - object: 'object2', - key: 'property4', - value: 'Text It Is' - }]); - }); - - it('exposes a UI element to toggle test data on and off', function () { - - }); + testDataManager = new TestDataManager(mockDomainObject, mockConditionManager, mockOpenMCT); }); + + it('closes its configuration panel on initial load', function () {}); + + it('exposes a DOM element to represent itself in the view', function () { + mockContainer.append(testDataManager.getDOM()); + expect(mockContainer.querySelectorAll('.t-widget-test-data-content').length).toBeGreaterThan( + 0 + ); + }); + + it('generates a test cache in the format expected by a condition evaluator', function () { + testDataManager.updateTestCache(); + expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({ + object1: { + property1: 66, + property2: '' + }, + object2: { + property3: '', + property4: 'Text It Is' + } + }); + }); + + it( + 'updates its configuration on a item change and provides an updated' + + 'cache to the evaluator', + function () { + testDataManager.onItemChange({ + value: 26, + property: 'value', + index: 1 + }); + expect(testDataManager.config[1].value).toEqual(26); + expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({ + object1: { + property1: 26, + property2: '' + }, + object2: { + property3: '', + property4: 'Text It Is' + } + }); + } + ); + + it('allows initializing a new item with a default configuration', function () { + testDataManager.initItem(); + expect(mockDomainObject.configuration.testDataConfig).toEqual([ + { + object: '', + key: '', + value: '' + }, + { + object: 'object1', + key: 'property1', + value: 66 + }, + { + object: 'object2', + key: 'property4', + value: 'Text It Is' + }, + { + object: '', + key: '', + value: '' + } + ]); + }); + + it('allows initializing a new item from a given configuration', function () { + testDataManager.initItem({ + sourceItem: { + object: 'object2', + key: 'property3', + value: 1 + }, + index: 0 + }); + expect(mockDomainObject.configuration.testDataConfig).toEqual([ + { + object: '', + key: '', + value: '' + }, + { + object: 'object2', + key: 'property3', + value: 1 + }, + { + object: 'object1', + key: 'property1', + value: 66 + }, + { + object: 'object2', + key: 'property4', + value: 'Text It Is' + } + ]); + }); + + it('invokes mutate when updating the domain object', function () { + testDataManager.updateDomainObject(); + expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); + }); + + it('builds item view from item configuration', function () { + mockContainer.append(testDataManager.getDOM()); + expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(3); + }); + + it('can remove a item from its configuration', function () { + testDataManager.removeItem(0); + expect(mockDomainObject.configuration.testDataConfig).toEqual([ + { + object: 'object1', + key: 'property1', + value: 66 + }, + { + object: 'object2', + key: 'property4', + value: 'Text It Is' + } + ]); + }); + + it('exposes a UI element to toggle test data on and off', function () {}); + }); }); diff --git a/src/plugins/summaryWidget/test/input/ColorPaletteSpec.js b/src/plugins/summaryWidget/test/input/ColorPaletteSpec.js index d169ef748a..0470c0f0f3 100644 --- a/src/plugins/summaryWidget/test/input/ColorPaletteSpec.js +++ b/src/plugins/summaryWidget/test/input/ColorPaletteSpec.js @@ -1,24 +1,24 @@ define(['../../src/input/ColorPalette'], function (ColorPalette) { - describe('An Open MCT color palette', function () { - let colorPalette; - let changeCallback; + describe('An Open MCT color palette', function () { + let colorPalette; + let changeCallback; - beforeEach(function () { - changeCallback = jasmine.createSpy('changeCallback'); - }); - - it('allows defining a custom color set', function () { - colorPalette = new ColorPalette('someClass', 'someContainer', ['color1', 'color2', 'color3']); - expect(colorPalette.getCurrent()).toEqual('color1'); - colorPalette.on('change', changeCallback); - colorPalette.set('color2'); - expect(colorPalette.getCurrent()).toEqual('color2'); - expect(changeCallback).toHaveBeenCalledWith('color2'); - }); - - it('loads with a default color set if one is not provided', function () { - colorPalette = new ColorPalette('someClass', 'someContainer'); - expect(colorPalette.getCurrent()).toBeDefined(); - }); + beforeEach(function () { + changeCallback = jasmine.createSpy('changeCallback'); }); + + it('allows defining a custom color set', function () { + colorPalette = new ColorPalette('someClass', 'someContainer', ['color1', 'color2', 'color3']); + expect(colorPalette.getCurrent()).toEqual('color1'); + colorPalette.on('change', changeCallback); + colorPalette.set('color2'); + expect(colorPalette.getCurrent()).toEqual('color2'); + expect(changeCallback).toHaveBeenCalledWith('color2'); + }); + + it('loads with a default color set if one is not provided', function () { + colorPalette = new ColorPalette('someClass', 'someContainer'); + expect(colorPalette.getCurrent()).toBeDefined(); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/IconPaletteSpec.js b/src/plugins/summaryWidget/test/input/IconPaletteSpec.js index 6bb80a6be5..3a9128c17d 100644 --- a/src/plugins/summaryWidget/test/input/IconPaletteSpec.js +++ b/src/plugins/summaryWidget/test/input/IconPaletteSpec.js @@ -1,24 +1,24 @@ define(['../../src/input/IconPalette'], function (IconPalette) { - describe('An Open MCT icon palette', function () { - let iconPalette; - let changeCallback; + describe('An Open MCT icon palette', function () { + let iconPalette; + let changeCallback; - beforeEach(function () { - changeCallback = jasmine.createSpy('changeCallback'); - }); - - it('allows defining a custom icon set', function () { - iconPalette = new IconPalette('', 'someContainer', ['icon1', 'icon2', 'icon3']); - expect(iconPalette.getCurrent()).toEqual('icon1'); - iconPalette.on('change', changeCallback); - iconPalette.set('icon2'); - expect(iconPalette.getCurrent()).toEqual('icon2'); - expect(changeCallback).toHaveBeenCalledWith('icon2'); - }); - - it('loads with a default icon set if one is not provided', function () { - iconPalette = new IconPalette('someClass', 'someContainer'); - expect(iconPalette.getCurrent()).toBeDefined(); - }); + beforeEach(function () { + changeCallback = jasmine.createSpy('changeCallback'); }); + + it('allows defining a custom icon set', function () { + iconPalette = new IconPalette('', 'someContainer', ['icon1', 'icon2', 'icon3']); + expect(iconPalette.getCurrent()).toEqual('icon1'); + iconPalette.on('change', changeCallback); + iconPalette.set('icon2'); + expect(iconPalette.getCurrent()).toEqual('icon2'); + expect(changeCallback).toHaveBeenCalledWith('icon2'); + }); + + it('loads with a default icon set if one is not provided', function () { + iconPalette = new IconPalette('someClass', 'someContainer'); + expect(iconPalette.getCurrent()).toBeDefined(); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/KeySelectSpec.js b/src/plugins/summaryWidget/test/input/KeySelectSpec.js index 374b310d38..d5c22ce3b7 100644 --- a/src/plugins/summaryWidget/test/input/KeySelectSpec.js +++ b/src/plugins/summaryWidget/test/input/KeySelectSpec.js @@ -1,127 +1,123 @@ define(['../../src/input/KeySelect'], function (KeySelect) { - describe('A select for choosing composition object properties', function () { - let mockConfig; - let mockBadConfig; - let mockManager; - let keySelect; - let mockMetadata; - let mockObjectSelect; - beforeEach(function () { - mockConfig = { - object: 'object1', - key: 'a' - }; + describe('A select for choosing composition object properties', function () { + let mockConfig; + let mockBadConfig; + let mockManager; + let keySelect; + let mockMetadata; + let mockObjectSelect; + beforeEach(function () { + mockConfig = { + object: 'object1', + key: 'a' + }; - mockBadConfig = { - object: 'object1', - key: 'someNonexistentKey' - }; + mockBadConfig = { + object: 'object1', + key: 'someNonexistentKey' + }; - mockMetadata = { - object1: { - a: { - name: 'A' - }, - b: { - name: 'B' - } - }, - object2: { - alpha: { - name: 'Alpha' - }, - beta: { - name: 'Beta' - } - }, - object3: { - a: { - name: 'A' - } - } - }; + mockMetadata = { + object1: { + a: { + name: 'A' + }, + b: { + name: 'B' + } + }, + object2: { + alpha: { + name: 'Alpha' + }, + beta: { + name: 'Beta' + } + }, + object3: { + a: { + name: 'A' + } + } + }; - mockManager = jasmine.createSpyObj('mockManager', [ - 'on', - 'metadataLoadCompleted', - 'triggerCallback', - 'getTelemetryMetadata' - ]); + mockManager = jasmine.createSpyObj('mockManager', [ + 'on', + 'metadataLoadCompleted', + 'triggerCallback', + 'getTelemetryMetadata' + ]); - mockObjectSelect = jasmine.createSpyObj('mockObjectSelect', [ - 'on', - 'triggerCallback' - ]); + mockObjectSelect = jasmine.createSpyObj('mockObjectSelect', ['on', 'triggerCallback']); - mockObjectSelect.on.and.callFake((event, callback) => { - mockObjectSelect.callbacks = mockObjectSelect.callbacks || {}; - mockObjectSelect.callbacks[event] = callback; - }); + mockObjectSelect.on.and.callFake((event, callback) => { + mockObjectSelect.callbacks = mockObjectSelect.callbacks || {}; + mockObjectSelect.callbacks[event] = callback; + }); - mockObjectSelect.triggerCallback.and.callFake((event, key) => { - mockObjectSelect.callbacks[event](key); - }); + mockObjectSelect.triggerCallback.and.callFake((event, key) => { + mockObjectSelect.callbacks[event](key); + }); - mockManager.on.and.callFake((event, callback) => { - mockManager.callbacks = mockManager.callbacks || {}; - mockManager.callbacks[event] = callback; - }); + mockManager.on.and.callFake((event, callback) => { + mockManager.callbacks = mockManager.callbacks || {}; + mockManager.callbacks[event] = callback; + }); - mockManager.triggerCallback.and.callFake(event => { - mockManager.callbacks[event](); - }); + mockManager.triggerCallback.and.callFake((event) => { + mockManager.callbacks[event](); + }); - mockManager.getTelemetryMetadata.and.callFake(function (key) { - return mockMetadata[key]; - }); - - }); - - it('waits until the metadata fully loads to populate itself', function () { - mockManager.metadataLoadCompleted.and.returnValue(false); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - expect(keySelect.getSelected()).toEqual(''); - }); - - it('populates itself with metadata on a metadata load', function () { - mockManager.metadataLoadCompleted.and.returnValue(false); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - mockManager.triggerCallback('metadata'); - expect(keySelect.getSelected()).toEqual('a'); - }); - - it('populates itself with metadata if metadata load is already complete', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - expect(keySelect.getSelected()).toEqual('a'); - }); - - it('clears its selection state if the property in its config is not in its object', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - keySelect = new KeySelect(mockBadConfig, mockObjectSelect, mockManager); - expect(keySelect.getSelected()).toEqual(''); - }); - - it('populates with the appropriate options when its linked object changes', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - mockObjectSelect.triggerCallback('change', 'object2'); - keySelect.setSelected('alpha'); - expect(keySelect.getSelected()).toEqual('alpha'); - }); - - it('clears its selected state on change if the field is not present in the new object', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - mockObjectSelect.triggerCallback('change', 'object2'); - expect(keySelect.getSelected()).toEqual(''); - }); - - it('maintains its selected state on change if field is present in new object', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - mockObjectSelect.triggerCallback('change', 'object3'); - expect(keySelect.getSelected()).toEqual('a'); - }); + mockManager.getTelemetryMetadata.and.callFake(function (key) { + return mockMetadata[key]; + }); }); + + it('waits until the metadata fully loads to populate itself', function () { + mockManager.metadataLoadCompleted.and.returnValue(false); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + expect(keySelect.getSelected()).toEqual(''); + }); + + it('populates itself with metadata on a metadata load', function () { + mockManager.metadataLoadCompleted.and.returnValue(false); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + mockManager.triggerCallback('metadata'); + expect(keySelect.getSelected()).toEqual('a'); + }); + + it('populates itself with metadata if metadata load is already complete', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + expect(keySelect.getSelected()).toEqual('a'); + }); + + it('clears its selection state if the property in its config is not in its object', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + keySelect = new KeySelect(mockBadConfig, mockObjectSelect, mockManager); + expect(keySelect.getSelected()).toEqual(''); + }); + + it('populates with the appropriate options when its linked object changes', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + mockObjectSelect.triggerCallback('change', 'object2'); + keySelect.setSelected('alpha'); + expect(keySelect.getSelected()).toEqual('alpha'); + }); + + it('clears its selected state on change if the field is not present in the new object', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + mockObjectSelect.triggerCallback('change', 'object2'); + expect(keySelect.getSelected()).toEqual(''); + }); + + it('maintains its selected state on change if field is present in new object', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + mockObjectSelect.triggerCallback('change', 'object3'); + expect(keySelect.getSelected()).toEqual('a'); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/ObjectSelectSpec.js b/src/plugins/summaryWidget/test/input/ObjectSelectSpec.js index 90d3c8c41f..5717331376 100644 --- a/src/plugins/summaryWidget/test/input/ObjectSelectSpec.js +++ b/src/plugins/summaryWidget/test/input/ObjectSelectSpec.js @@ -1,113 +1,112 @@ define(['../../src/input/ObjectSelect'], function (ObjectSelect) { - describe('A select for choosing composition objects', function () { - let mockConfig; - let mockBadConfig; - let mockManager; - let objectSelect; - let mockComposition; - beforeEach(function () { - mockConfig = { - object: 'key1' - }; + describe('A select for choosing composition objects', function () { + let mockConfig; + let mockBadConfig; + let mockManager; + let objectSelect; + let mockComposition; + beforeEach(function () { + mockConfig = { + object: 'key1' + }; - mockBadConfig = { - object: 'someNonexistentObject' - }; + mockBadConfig = { + object: 'someNonexistentObject' + }; - mockComposition = { - key1: { - identifier: { - key: 'key1' - }, - name: 'Object 1' - }, - key2: { - identifier: { - key: 'key2' - }, - name: 'Object 2' - } - }; - mockManager = jasmine.createSpyObj('mockManager', [ - 'on', - 'loadCompleted', - 'triggerCallback', - 'getComposition' - ]); + mockComposition = { + key1: { + identifier: { + key: 'key1' + }, + name: 'Object 1' + }, + key2: { + identifier: { + key: 'key2' + }, + name: 'Object 2' + } + }; + mockManager = jasmine.createSpyObj('mockManager', [ + 'on', + 'loadCompleted', + 'triggerCallback', + 'getComposition' + ]); - mockManager.on.and.callFake((event, callback) => { - mockManager.callbacks = mockManager.callbacks || {}; - mockManager.callbacks[event] = callback; - }); + mockManager.on.and.callFake((event, callback) => { + mockManager.callbacks = mockManager.callbacks || {}; + mockManager.callbacks[event] = callback; + }); - mockManager.triggerCallback.and.callFake((event, newObj) => { - if (event === 'add') { - mockManager.callbacks.add(newObj); - } else { - mockManager.callbacks[event](); - } - }); + mockManager.triggerCallback.and.callFake((event, newObj) => { + if (event === 'add') { + mockManager.callbacks.add(newObj); + } else { + mockManager.callbacks[event](); + } + }); - mockManager.getComposition.and.callFake(function () { - return mockComposition; - }); - - }); - - it('allows setting special keyword options', function () { - mockManager.loadCompleted.and.returnValue(true); - objectSelect = new ObjectSelect(mockConfig, mockManager, [ - ['keyword1', 'A special option'], - ['keyword2', 'A special option'] - ]); - objectSelect.setSelected('keyword1'); - expect(objectSelect.getSelected()).toEqual('keyword1'); - }); - - it('waits until the composition fully loads to populate itself', function () { - mockManager.loadCompleted.and.returnValue(false); - objectSelect = new ObjectSelect(mockConfig, mockManager); - expect(objectSelect.getSelected()).toEqual(''); - }); - - it('populates itself with composition objects on a composition load', function () { - mockManager.loadCompleted.and.returnValue(false); - objectSelect = new ObjectSelect(mockConfig, mockManager); - mockManager.triggerCallback('load'); - expect(objectSelect.getSelected()).toEqual('key1'); - }); - - it('populates itself with composition objects if load is already complete', function () { - mockManager.loadCompleted.and.returnValue(true); - objectSelect = new ObjectSelect(mockConfig, mockManager); - expect(objectSelect.getSelected()).toEqual('key1'); - }); - - it('clears its selection state if the object in its config is not in the composition', function () { - mockManager.loadCompleted.and.returnValue(true); - objectSelect = new ObjectSelect(mockBadConfig, mockManager); - expect(objectSelect.getSelected()).toEqual(''); - }); - - it('adds a new option on a composition add', function () { - mockManager.loadCompleted.and.returnValue(true); - objectSelect = new ObjectSelect(mockConfig, mockManager); - mockManager.triggerCallback('add', { - identifier: { - key: 'key3' - }, - name: 'Object 3' - }); - objectSelect.setSelected('key3'); - expect(objectSelect.getSelected()).toEqual('key3'); - }); - - it('removes an option on a composition remove', function () { - mockManager.loadCompleted.and.returnValue(true); - objectSelect = new ObjectSelect(mockConfig, mockManager); - delete mockComposition.key1; - mockManager.triggerCallback('remove'); - expect(objectSelect.getSelected()).not.toEqual('key1'); - }); + mockManager.getComposition.and.callFake(function () { + return mockComposition; + }); }); + + it('allows setting special keyword options', function () { + mockManager.loadCompleted.and.returnValue(true); + objectSelect = new ObjectSelect(mockConfig, mockManager, [ + ['keyword1', 'A special option'], + ['keyword2', 'A special option'] + ]); + objectSelect.setSelected('keyword1'); + expect(objectSelect.getSelected()).toEqual('keyword1'); + }); + + it('waits until the composition fully loads to populate itself', function () { + mockManager.loadCompleted.and.returnValue(false); + objectSelect = new ObjectSelect(mockConfig, mockManager); + expect(objectSelect.getSelected()).toEqual(''); + }); + + it('populates itself with composition objects on a composition load', function () { + mockManager.loadCompleted.and.returnValue(false); + objectSelect = new ObjectSelect(mockConfig, mockManager); + mockManager.triggerCallback('load'); + expect(objectSelect.getSelected()).toEqual('key1'); + }); + + it('populates itself with composition objects if load is already complete', function () { + mockManager.loadCompleted.and.returnValue(true); + objectSelect = new ObjectSelect(mockConfig, mockManager); + expect(objectSelect.getSelected()).toEqual('key1'); + }); + + it('clears its selection state if the object in its config is not in the composition', function () { + mockManager.loadCompleted.and.returnValue(true); + objectSelect = new ObjectSelect(mockBadConfig, mockManager); + expect(objectSelect.getSelected()).toEqual(''); + }); + + it('adds a new option on a composition add', function () { + mockManager.loadCompleted.and.returnValue(true); + objectSelect = new ObjectSelect(mockConfig, mockManager); + mockManager.triggerCallback('add', { + identifier: { + key: 'key3' + }, + name: 'Object 3' + }); + objectSelect.setSelected('key3'); + expect(objectSelect.getSelected()).toEqual('key3'); + }); + + it('removes an option on a composition remove', function () { + mockManager.loadCompleted.and.returnValue(true); + objectSelect = new ObjectSelect(mockConfig, mockManager); + delete mockComposition.key1; + mockManager.triggerCallback('remove'); + expect(objectSelect.getSelected()).not.toEqual('key1'); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/OperationSelectSpec.js b/src/plugins/summaryWidget/test/input/OperationSelectSpec.js index 2f1a3c38fa..c57bc36578 100644 --- a/src/plugins/summaryWidget/test/input/OperationSelectSpec.js +++ b/src/plugins/summaryWidget/test/input/OperationSelectSpec.js @@ -1,148 +1,143 @@ define(['../../src/input/OperationSelect'], function (OperationSelect) { - describe('A select for choosing composition object properties', function () { - let mockConfig; - let mockBadConfig; - let mockManager; - let operationSelect; - let mockOperations; - let mockPropertyTypes; - let mockKeySelect; - let mockEvaluator; - beforeEach(function () { + describe('A select for choosing composition object properties', function () { + let mockConfig; + let mockBadConfig; + let mockManager; + let operationSelect; + let mockOperations; + let mockPropertyTypes; + let mockKeySelect; + let mockEvaluator; + beforeEach(function () { + mockConfig = { + object: 'object1', + key: 'a', + operation: 'operation1' + }; - mockConfig = { - object: 'object1', - key: 'a', - operation: 'operation1' - }; + mockBadConfig = { + object: 'object1', + key: 'a', + operation: 'someNonexistentOperation' + }; - mockBadConfig = { - object: 'object1', - key: 'a', - operation: 'someNonexistentOperation' - }; + mockOperations = { + operation1: { + text: 'An operation', + appliesTo: ['number'] + }, + operation2: { + text: 'Another operation', + appliesTo: ['string'] + } + }; - mockOperations = { - operation1: { - text: 'An operation', - appliesTo: ['number'] - }, - operation2: { - text: 'Another operation', - appliesTo: ['string'] - } - }; + mockPropertyTypes = { + object1: { + a: 'number', + b: 'string', + c: 'number' + } + }; - mockPropertyTypes = { - object1: { - a: 'number', - b: 'string', - c: 'number' - } - }; + mockManager = jasmine.createSpyObj('mockManager', [ + 'on', + 'metadataLoadCompleted', + 'triggerCallback', + 'getTelemetryPropertyType', + 'getEvaluator' + ]); - mockManager = jasmine.createSpyObj('mockManager', [ - 'on', - 'metadataLoadCompleted', - 'triggerCallback', - 'getTelemetryPropertyType', - 'getEvaluator' + mockKeySelect = jasmine.createSpyObj('mockKeySelect', ['on', 'triggerCallback']); - ]); + mockEvaluator = jasmine.createSpyObj('mockEvaluator', [ + 'getOperationKeys', + 'operationAppliesTo', + 'getOperationText' + ]); - mockKeySelect = jasmine.createSpyObj('mockKeySelect', [ - 'on', - 'triggerCallback' - ]); + mockEvaluator.getOperationKeys.and.returnValue(Object.keys(mockOperations)); - mockEvaluator = jasmine.createSpyObj('mockEvaluator', [ - 'getOperationKeys', - 'operationAppliesTo', - 'getOperationText' - ]); + mockEvaluator.getOperationText.and.callFake(function (key) { + return mockOperations[key].text; + }); - mockEvaluator.getOperationKeys.and.returnValue(Object.keys(mockOperations)); + mockEvaluator.operationAppliesTo.and.callFake(function (operation, type) { + return mockOperations[operation].appliesTo.includes(type); + }); - mockEvaluator.getOperationText.and.callFake(function (key) { - return mockOperations[key].text; - }); + mockKeySelect.on.and.callFake((event, callback) => { + mockKeySelect.callbacks = mockKeySelect.callbacks || {}; + mockKeySelect.callbacks[event] = callback; + }); - mockEvaluator.operationAppliesTo.and.callFake(function (operation, type) { - return (mockOperations[operation].appliesTo.includes(type)); - }); + mockKeySelect.triggerCallback.and.callFake((event, key) => { + mockKeySelect.callbacks[event](key); + }); - mockKeySelect.on.and.callFake((event, callback) => { - mockKeySelect.callbacks = mockKeySelect.callbacks || {}; - mockKeySelect.callbacks[event] = callback; - }); + mockManager.on.and.callFake((event, callback) => { + mockManager.callbacks = mockManager.callbacks || {}; + mockManager.callbacks[event] = callback; + }); - mockKeySelect.triggerCallback.and.callFake((event, key) => { - mockKeySelect.callbacks[event](key); - }); + mockManager.triggerCallback.and.callFake((event) => { + mockManager.callbacks[event](); + }); - mockManager.on.and.callFake((event, callback) => { - mockManager.callbacks = mockManager.callbacks || {}; - mockManager.callbacks[event] = callback; - }); + mockManager.getTelemetryPropertyType.and.callFake(function (object, key) { + return mockPropertyTypes[object][key]; + }); - mockManager.triggerCallback.and.callFake(event => { - mockManager.callbacks[event](); - }); - - mockManager.getTelemetryPropertyType.and.callFake(function (object, key) { - return mockPropertyTypes[object][key]; - }); - - mockManager.getEvaluator.and.returnValue(mockEvaluator); - }); - - it('waits until the metadata fully loads to populate itself', function () { - mockManager.metadataLoadCompleted.and.returnValue(false); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - expect(operationSelect.getSelected()).toEqual(''); - }); - - it('populates itself with operations on a metadata load', function () { - mockManager.metadataLoadCompleted.and.returnValue(false); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - mockManager.triggerCallback('metadata'); - expect(operationSelect.getSelected()).toEqual('operation1'); - }); - - it('populates itself with operations if metadata load is already complete', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - expect(operationSelect.getSelected()).toEqual('operation1'); - }); - - it('clears its selection state if the operation in its config does not apply', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - operationSelect = new OperationSelect(mockBadConfig, mockKeySelect, mockManager); - expect(operationSelect.getSelected()).toEqual(''); - }); - - it('populates with the appropriate options when its linked key changes', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - mockKeySelect.triggerCallback('change', 'b'); - operationSelect.setSelected('operation2'); - expect(operationSelect.getSelected()).toEqual('operation2'); - operationSelect.setSelected('operation1'); - expect(operationSelect.getSelected()).not.toEqual('operation1'); - }); - - it('clears its selection on a change if the operation does not apply', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - mockKeySelect.triggerCallback('change', 'b'); - expect(operationSelect.getSelected()).toEqual(''); - }); - - it('maintains its selected state on change if the operation does apply', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - mockKeySelect.triggerCallback('change', 'c'); - expect(operationSelect.getSelected()).toEqual('operation1'); - }); + mockManager.getEvaluator.and.returnValue(mockEvaluator); }); + + it('waits until the metadata fully loads to populate itself', function () { + mockManager.metadataLoadCompleted.and.returnValue(false); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + expect(operationSelect.getSelected()).toEqual(''); + }); + + it('populates itself with operations on a metadata load', function () { + mockManager.metadataLoadCompleted.and.returnValue(false); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + mockManager.triggerCallback('metadata'); + expect(operationSelect.getSelected()).toEqual('operation1'); + }); + + it('populates itself with operations if metadata load is already complete', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + expect(operationSelect.getSelected()).toEqual('operation1'); + }); + + it('clears its selection state if the operation in its config does not apply', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + operationSelect = new OperationSelect(mockBadConfig, mockKeySelect, mockManager); + expect(operationSelect.getSelected()).toEqual(''); + }); + + it('populates with the appropriate options when its linked key changes', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + mockKeySelect.triggerCallback('change', 'b'); + operationSelect.setSelected('operation2'); + expect(operationSelect.getSelected()).toEqual('operation2'); + operationSelect.setSelected('operation1'); + expect(operationSelect.getSelected()).not.toEqual('operation1'); + }); + + it('clears its selection on a change if the operation does not apply', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + mockKeySelect.triggerCallback('change', 'b'); + expect(operationSelect.getSelected()).toEqual(''); + }); + + it('maintains its selected state on change if the operation does apply', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + mockKeySelect.triggerCallback('change', 'c'); + expect(operationSelect.getSelected()).toEqual('operation1'); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/PaletteSpec.js b/src/plugins/summaryWidget/test/input/PaletteSpec.js index 25f6819f95..67678cb049 100644 --- a/src/plugins/summaryWidget/test/input/PaletteSpec.js +++ b/src/plugins/summaryWidget/test/input/PaletteSpec.js @@ -1,44 +1,44 @@ define(['../../src/input/Palette'], function (Palette) { - describe('A generic Open MCT palette input', function () { - let palette; - let callbackSpy1; - let callbackSpy2; + describe('A generic Open MCT palette input', function () { + let palette; + let callbackSpy1; + let callbackSpy2; - beforeEach(function () { - palette = new Palette('someClass', 'someContainer', ['item1', 'item2', 'item3']); - callbackSpy1 = jasmine.createSpy('changeCallback1'); - callbackSpy2 = jasmine.createSpy('changeCallback2'); - }); - - it('gets the current item', function () { - expect(palette.getCurrent()).toEqual('item1'); - }); - - it('allows setting the current item', function () { - palette.set('item2'); - expect(palette.getCurrent()).toEqual('item2'); - }); - - it('allows registering change callbacks, and errors when an unsupported event is registered', function () { - expect(function () { - palette.on('change', callbackSpy1); - }).not.toThrow(); - expect(function () { - palette.on('someUnsupportedEvent', callbackSpy1); - }).toThrow(); - }); - - it('injects its callbacks with the new selected item on change', function () { - palette.on('change', callbackSpy1); - palette.on('change', callbackSpy2); - palette.set('item2'); - expect(callbackSpy1).toHaveBeenCalledWith('item2'); - expect(callbackSpy2).toHaveBeenCalledWith('item2'); - }); - - it('gracefully handles being set to an item not included in its set', function () { - palette.set('foobar'); - expect(palette.getCurrent()).not.toEqual('foobar'); - }); + beforeEach(function () { + palette = new Palette('someClass', 'someContainer', ['item1', 'item2', 'item3']); + callbackSpy1 = jasmine.createSpy('changeCallback1'); + callbackSpy2 = jasmine.createSpy('changeCallback2'); }); + + it('gets the current item', function () { + expect(palette.getCurrent()).toEqual('item1'); + }); + + it('allows setting the current item', function () { + palette.set('item2'); + expect(palette.getCurrent()).toEqual('item2'); + }); + + it('allows registering change callbacks, and errors when an unsupported event is registered', function () { + expect(function () { + palette.on('change', callbackSpy1); + }).not.toThrow(); + expect(function () { + palette.on('someUnsupportedEvent', callbackSpy1); + }).toThrow(); + }); + + it('injects its callbacks with the new selected item on change', function () { + palette.on('change', callbackSpy1); + palette.on('change', callbackSpy2); + palette.set('item2'); + expect(callbackSpy1).toHaveBeenCalledWith('item2'); + expect(callbackSpy2).toHaveBeenCalledWith('item2'); + }); + + it('gracefully handles being set to an item not included in its set', function () { + palette.set('foobar'); + expect(palette.getCurrent()).not.toEqual('foobar'); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/SelectSpec.js b/src/plugins/summaryWidget/test/input/SelectSpec.js index bd895507fa..bdd0dac774 100644 --- a/src/plugins/summaryWidget/test/input/SelectSpec.js +++ b/src/plugins/summaryWidget/test/input/SelectSpec.js @@ -1,54 +1,61 @@ define(['../../src/input/Select'], function (Select) { - describe('A select wrapper', function () { - let select; - let testOptions; - let callbackSpy1; - let callbackSpy2; - beforeEach(function () { - select = new Select(); - testOptions = [['item1', 'Item 1'], ['item2', 'Item 2'], ['item3', 'Item 3']]; - select.setOptions(testOptions); - callbackSpy1 = jasmine.createSpy('callbackSpy1'); - callbackSpy2 = jasmine.createSpy('callbackSpy2'); - }); - - it('gets and sets the current item', function () { - select.setSelected('item1'); - expect(select.getSelected()).toEqual('item1'); - }); - - it('allows adding a single new option', function () { - select.addOption('newOption', 'A New Option'); - select.setSelected('newOption'); - expect(select.getSelected()).toEqual('newOption'); - }); - - it('allows populating with a new set of options', function () { - select.setOptions([['newItem1', 'Item 1'], ['newItem2', 'Item 2']]); - select.setSelected('newItem1'); - expect(select.getSelected()).toEqual('newItem1'); - }); - - it('allows registering change callbacks, and errors when an unsupported event is registered', function () { - expect(function () { - select.on('change', callbackSpy1); - }).not.toThrow(); - expect(function () { - select.on('someUnsupportedEvent', callbackSpy1); - }).toThrow(); - }); - - it('injects its callbacks with its property and value on a change', function () { - select.on('change', callbackSpy1); - select.on('change', callbackSpy2); - select.setSelected('item2'); - expect(callbackSpy1).toHaveBeenCalledWith('item2'); - expect(callbackSpy2).toHaveBeenCalledWith('item2'); - }); - - it('gracefully handles being set to an item not included in its set', function () { - select.setSelected('foobar'); - expect(select.getSelected()).not.toEqual('foobar'); - }); + describe('A select wrapper', function () { + let select; + let testOptions; + let callbackSpy1; + let callbackSpy2; + beforeEach(function () { + select = new Select(); + testOptions = [ + ['item1', 'Item 1'], + ['item2', 'Item 2'], + ['item3', 'Item 3'] + ]; + select.setOptions(testOptions); + callbackSpy1 = jasmine.createSpy('callbackSpy1'); + callbackSpy2 = jasmine.createSpy('callbackSpy2'); }); + + it('gets and sets the current item', function () { + select.setSelected('item1'); + expect(select.getSelected()).toEqual('item1'); + }); + + it('allows adding a single new option', function () { + select.addOption('newOption', 'A New Option'); + select.setSelected('newOption'); + expect(select.getSelected()).toEqual('newOption'); + }); + + it('allows populating with a new set of options', function () { + select.setOptions([ + ['newItem1', 'Item 1'], + ['newItem2', 'Item 2'] + ]); + select.setSelected('newItem1'); + expect(select.getSelected()).toEqual('newItem1'); + }); + + it('allows registering change callbacks, and errors when an unsupported event is registered', function () { + expect(function () { + select.on('change', callbackSpy1); + }).not.toThrow(); + expect(function () { + select.on('someUnsupportedEvent', callbackSpy1); + }).toThrow(); + }); + + it('injects its callbacks with its property and value on a change', function () { + select.on('change', callbackSpy1); + select.on('change', callbackSpy2); + select.setSelected('item2'); + expect(callbackSpy1).toHaveBeenCalledWith('item2'); + expect(callbackSpy2).toHaveBeenCalledWith('item2'); + }); + + it('gracefully handles being set to an item not included in its set', function () { + select.setSelected('foobar'); + expect(select.getSelected()).not.toEqual('foobar'); + }); + }); }); diff --git a/src/plugins/tabs/components/tabs.scss b/src/plugins/tabs/components/tabs.scss index bb5df6144b..1f1cb3d6b0 100644 --- a/src/plugins/tabs/components/tabs.scss +++ b/src/plugins/tabs/components/tabs.scss @@ -1,60 +1,60 @@ .c-tabs-view { - $h: 20px; - @include abs(); - display: flex; - flex-flow: column nowrap; + $h: 20px; + @include abs(); + display: flex; + flex-flow: column nowrap; + + > * + * { + margin-top: $interiorMargin; + } + + &__tabs-holder { + min-height: $h; + } + + &__tab { + justify-content: space-between; // Places remove button to far side of tab + + &__close-btn { + flex: 0 0 auto; + pointer-events: all; + } > * + * { - margin-top: $interiorMargin; + margin-left: $interiorMargin; } + } - &__tabs-holder { - min-height: $h; + &__object-holder { + flex: 1 1 auto; + display: flex; + flex-direction: column; + + &--hidden { + position: absolute; + left: -9999px; + top: -9999px; } + } - &__tab { - justify-content: space-between; // Places remove button to far side of tab + &__object-name { + font-size: 1em; + margin: $interiorMargin 0 $interiorMarginLg 0; + } - &__close-btn { - flex: 0 0 auto; - pointer-events: all; - } + &__object { + display: flex; + flex-flow: column nowrap; + flex: 1 1 auto; + height: 0; // Chrome 73 overflow bug fix + } - > * + * { - margin-left: $interiorMargin; - } - } - - &__object-holder { - flex: 1 1 auto; - display: flex; - flex-direction: column; - - &--hidden { - position: absolute; - left: -9999px; - top: -9999px; - } - } - - &__object-name { - font-size: 1em; - margin: $interiorMargin 0 $interiorMarginLg 0; - } - - &__object { - display: flex; - flex-flow: column nowrap; - flex: 1 1 auto; - height: 0; // Chrome 73 overflow bug fix - } - - &__empty-message { - background: rgba($colorBodyFg, 0.1); - color: rgba($colorBodyFg, 0.7); - font-style: italic; - text-align: center; - line-height: $h; - width: 100%; - } + &__empty-message { + background: rgba($colorBodyFg, 0.1); + color: rgba($colorBodyFg, 0.7); + font-style: italic; + text-align: center; + line-height: $h; + width: 100%; + } } diff --git a/src/plugins/tabs/components/tabs.vue b/src/plugins/tabs/components/tabs.vue index 1aec04a099..5835a62766 100644 --- a/src/plugins/tabs/components/tabs.vue +++ b/src/plugins/tabs/components/tabs.vue @@ -20,77 +20,60 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/tabs/plugin.js b/src/plugins/tabs/plugin.js index e38c47b51e..87be068713 100644 --- a/src/plugins/tabs/plugin.js +++ b/src/plugins/tabs/plugin.js @@ -20,44 +20,40 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './tabs' -], function ( - Tabs -) { - return function plugin() { - return function install(openmct) { - openmct.objectViews.addProvider(new Tabs(openmct)); +define(['./tabs'], function (Tabs) { + return function plugin() { + return function install(openmct) { + openmct.objectViews.addProvider(new Tabs(openmct)); - openmct.types.addType('tabs', { - name: "Tabs View", - description: 'Quickly navigate between multiple objects of any type using tabs.', - creatable: true, - cssClass: 'icon-tabs-view', - initialize(domainObject) { - domainObject.composition = []; - domainObject.keep_alive = true; - }, - form: [ - { - "key": "keep_alive", - "name": "Eager Load Tabs", - "control": "select", - "options": [ - { - 'name': 'True', - 'value': true - }, - { - 'name': 'False', - 'value': false - } - ], - "required": true, - "cssClass": "l-input" - } - ] - }); - }; + openmct.types.addType('tabs', { + name: 'Tabs View', + description: 'Quickly navigate between multiple objects of any type using tabs.', + creatable: true, + cssClass: 'icon-tabs-view', + initialize(domainObject) { + domainObject.composition = []; + domainObject.keep_alive = true; + }, + form: [ + { + key: 'keep_alive', + name: 'Eager Load Tabs', + control: 'select', + options: [ + { + name: 'True', + value: true + }, + { + name: 'False', + value: false + } + ], + required: true, + cssClass: 'l-input' + } + ] + }); }; + }; }); diff --git a/src/plugins/tabs/pluginSpec.js b/src/plugins/tabs/pluginSpec.js index 3261682a4d..63eef73586 100644 --- a/src/plugins/tabs/pluginSpec.js +++ b/src/plugins/tabs/pluginSpec.js @@ -20,200 +20,195 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; import TabsLayout from './plugin'; -import Vue from "vue"; -import EventEmitter from "EventEmitter"; +import Vue from 'vue'; +import EventEmitter from 'EventEmitter'; describe('the plugin', function () { - let element; - let child; - let openmct; - let tabsLayoutDefinition; - const testViewObject = { + let element; + let child; + let openmct; + let tabsLayoutDefinition; + const testViewObject = { + identifier: { + key: 'mock-tabs-object', + namespace: '' + }, + type: 'tabs', + name: 'Tabs view', + keep_alive: true, + composition: [ + { identifier: { - key: 'mock-tabs-object', - namespace: '' - }, - type: 'tabs', - name: 'Tabs view', - keep_alive: true, - composition: [ - { - 'identifier': { - 'namespace': '', - 'key': 'swg-1' - } - }, - { - 'identifier': { - 'namespace': '', - 'key': 'swg-2' - } - } - ] - }; - const telemetryItemTemplate = { - 'telemetry': { - 'period': 5, - 'amplitude': 5, - 'offset': 5, - 'dataRateInHz': 5, - 'phase': 5, - 'randomness': 0 - }, - 'type': 'generator', - 'modified': 1592851063871, - 'location': 'mine', - 'persisted': 1592851063871 - }; - let telemetryItem1 = Object.assign({}, telemetryItemTemplate, { - 'name': 'Sine Wave Generator 1', - 'identifier': { - 'namespace': '', - 'key': 'swg-1' + namespace: '', + key: 'swg-1' } - }); - let telemetryItem2 = Object.assign({}, telemetryItemTemplate, { - 'name': 'Sine Wave Generator 2', - 'identifier': { - 'namespace': '', - 'key': 'swg-2' + }, + { + identifier: { + namespace: '', + key: 'swg-2' } + } + ] + }; + const telemetryItemTemplate = { + telemetry: { + period: 5, + amplitude: 5, + offset: 5, + dataRateInHz: 5, + phase: 5, + randomness: 0 + }, + type: 'generator', + modified: 1592851063871, + location: 'mine', + persisted: 1592851063871 + }; + let telemetryItem1 = Object.assign({}, telemetryItemTemplate, { + name: 'Sine Wave Generator 1', + identifier: { + namespace: '', + key: 'swg-1' + } + }); + let telemetryItem2 = Object.assign({}, telemetryItemTemplate, { + name: 'Sine Wave Generator 2', + identifier: { + namespace: '', + key: 'swg-2' + } + }); + + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(new TabsLayout()); + tabsLayoutDefinition = openmct.types.get('tabs'); + + element = document.createElement('div'); + child = document.createElement('div'); + child.style.display = 'block'; + child.style.width = '1920px'; + child.style.height = '1080px'; + element.appendChild(child); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('defines a tabs object type with the correct key', () => { + expect(tabsLayoutDefinition.definition.name).toEqual('Tabs View'); + }); + + it('is creatable', () => { + expect(tabsLayoutDefinition.definition.creatable).toEqual(true); + }); + + describe('the view', function () { + let tabsLayoutViewProvider; + let mockComposition; + + beforeEach(() => { + mockComposition = new EventEmitter(); + mockComposition.load = () => { + return Promise.resolve([telemetryItem1]); + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + const applicableViews = openmct.objectViews.get(testViewObject, []); + tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); + let view = tabsLayoutViewProvider.view(testViewObject, []); + view.show(child, true); + + return Vue.nextTick(); }); - beforeEach((done) => { - openmct = createOpenMct(); - openmct.install(new TabsLayout()); - tabsLayoutDefinition = openmct.types.get('tabs'); + it('provides a view', () => { + expect(tabsLayoutViewProvider).toBeDefined(); + }); - element = document.createElement('div'); - child = document.createElement('div'); - child.style.display = 'block'; - child.style.width = '1920px'; - child.style.height = '1080px'; - element.appendChild(child); + it('renders tab element', () => { + const tabsElements = element.querySelectorAll('.c-tabs'); - openmct.on('start', done); - openmct.startHeadless(); + expect(tabsElements.length).toBe(1); + }); + + it('renders empty tab element with msg', () => { + const tabsElement = element.querySelector('.c-tabs'); + + expect(tabsElement.innerText.trim()).toEqual('Drag objects here to add them to this view.'); + }); + }); + + describe('the view', function () { + let tabsLayoutViewProvider; + let mockComposition; + let count = 0; + + beforeEach(() => { + mockComposition = new EventEmitter(); + mockComposition.load = () => { + if (count === 0) { + mockComposition.emit('add', telemetryItem1); + mockComposition.emit('add', telemetryItem2); + count++; + } + + return Promise.resolve([telemetryItem1, telemetryItem2]); + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + const applicableViews = openmct.objectViews.get(testViewObject, []); + tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); + let view = tabsLayoutViewProvider.view(testViewObject, []); + view.show(child, true); + + return Vue.nextTick(); }); afterEach(() => { - return resetApplicationState(openmct); + count = 0; + testViewObject.keep_alive = true; }); - it('defines a tabs object type with the correct key', () => { - expect(tabsLayoutDefinition.definition.name).toEqual('Tabs View'); + it('renders a tab for each item', () => { + let tabEls = element.querySelectorAll('.js-tab'); + + expect(tabEls.length).toEqual(2); }); - it('is creatable', () => { - expect(tabsLayoutDefinition.definition.creatable).toEqual(true); - }); - - describe('the view', function () { - let tabsLayoutViewProvider; - let mockComposition; - - beforeEach(() => { - mockComposition = new EventEmitter(); - mockComposition.load = () => { - return Promise.resolve([telemetryItem1]); - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - const applicableViews = openmct.objectViews.get(testViewObject, []); - tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); - let view = tabsLayoutViewProvider.view(testViewObject, []); - view.show(child, true); - - return Vue.nextTick(); - }); - - it('provides a view', () => { - expect(tabsLayoutViewProvider).toBeDefined(); - }); - - it('renders tab element', () => { - const tabsElements = element.querySelectorAll('.c-tabs'); - - expect(tabsElements.length).toBe(1); - }); - - it('renders empty tab element with msg', () => { - const tabsElement = element.querySelector('.c-tabs'); - - expect(tabsElement.innerText.trim()).toEqual('Drag objects here to add them to this view.'); - }); - }); - - describe('the view', function () { - let tabsLayoutViewProvider; - let mockComposition; - let count = 0; - - beforeEach(() => { - mockComposition = new EventEmitter(); - mockComposition.load = () => { - if (count === 0) { - mockComposition.emit('add', telemetryItem1); - mockComposition.emit('add', telemetryItem2); - count++; - } - - return Promise.resolve([telemetryItem1, telemetryItem2]); - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - const applicableViews = openmct.objectViews.get(testViewObject, []); - tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); - let view = tabsLayoutViewProvider.view(testViewObject, []); - view.show(child, true); - - return Vue.nextTick(); - }); - - afterEach(() => { - count = 0; - testViewObject.keep_alive = true; - }); - - it ('renders a tab for each item', () => { - let tabEls = element.querySelectorAll('.js-tab'); - - expect(tabEls.length).toEqual(2); - }); - - describe('with domainObject.keep_alive set to', () => { - - it ('true, will keep all views loaded, regardless of current tab view', async () => { - let tabEls = element.querySelectorAll('.js-tab'); - - for (let i = 0; i < tabEls.length; i++) { - const tab = tabEls[i]; - - tab.click(); - await Vue.nextTick(); - - const tabViewEls = element.querySelectorAll('.c-tabs-view__object'); - expect(tabViewEls.length).toEqual(2); - } - }); - - it ('false, will only keep the current tab view loaded', async () => { - testViewObject.keep_alive = false; - - await Vue.nextTick(); - - let tabViewEls = element.querySelectorAll('.c-tabs-view__object'); - - expect(tabViewEls.length).toEqual(1); - }); - - }); + describe('with domainObject.keep_alive set to', () => { + it('true, will keep all views loaded, regardless of current tab view', async () => { + let tabEls = element.querySelectorAll('.js-tab'); + + for (let i = 0; i < tabEls.length; i++) { + const tab = tabEls[i]; + + tab.click(); + await Vue.nextTick(); + + const tabViewEls = element.querySelectorAll('.c-tabs-view__object'); + expect(tabViewEls.length).toEqual(2); + } + }); + + it('false, will only keep the current tab view loaded', async () => { + testViewObject.keep_alive = false; + + await Vue.nextTick(); + + let tabViewEls = element.querySelectorAll('.c-tabs-view__object'); + + expect(tabViewEls.length).toEqual(1); + }); }); + }); }); diff --git a/src/plugins/tabs/tabs.js b/src/plugins/tabs/tabs.js index e6cc5a75a1..4cfe9b3afd 100644 --- a/src/plugins/tabs/tabs.js +++ b/src/plugins/tabs/tabs.js @@ -20,62 +20,56 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './components/tabs.vue', - 'vue' -], function ( - TabsComponent, - Vue -) { - function Tabs(openmct) { +define(['./components/tabs.vue', 'vue'], function (TabsComponent, Vue) { + function Tabs(openmct) { + return { + key: 'tabs', + name: 'Tabs', + cssClass: 'icon-list-view', + canView: function (domainObject) { + return domainObject.type === 'tabs'; + }, + canEdit: function (domainObject) { + return domainObject.type === 'tabs'; + }, + view: function (domainObject, objectPath) { + let component; + return { - key: 'tabs', - name: 'Tabs', - cssClass: 'icon-list-view', - canView: function (domainObject) { - return domainObject.type === 'tabs'; - }, - canEdit: function (domainObject) { - return domainObject.type === 'tabs'; - }, - view: function (domainObject, objectPath) { - let component; - + show: function (element, editMode) { + component = new Vue({ + el: element, + components: { + TabsComponent: TabsComponent.default + }, + provide: { + openmct, + domainObject, + objectPath, + composition: openmct.composition.get(domainObject) + }, + data() { return { - show: function (element, editMode) { - component = new Vue({ - el: element, - components: { - TabsComponent: TabsComponent.default - }, - provide: { - openmct, - domainObject, - objectPath, - composition: openmct.composition.get(domainObject) - }, - data() { - return { - isEditing: editMode - }; - }, - template: '' - }); - }, - onEditModeChange(editMode) { - component.isEditing = editMode; - }, - destroy: function (element) { - component.$destroy(); - component = undefined; - } + isEditing: editMode }; - }, - priority: function () { - return 1; - } + }, + template: '' + }); + }, + onEditModeChange(editMode) { + component.isEditing = editMode; + }, + destroy: function (element) { + component.$destroy(); + component = undefined; + } }; - } + }, + priority: function () { + return 1; + } + }; + } - return Tabs; + return Tabs; }); diff --git a/src/plugins/telemetryMean/plugin.js b/src/plugins/telemetryMean/plugin.js index ee49cecd03..3920d811e7 100755 --- a/src/plugins/telemetryMean/plugin.js +++ b/src/plugins/telemetryMean/plugin.js @@ -1,76 +1,78 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2023, 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. - *****************************************************************************/ - -define(['./src/MeanTelemetryProvider'], function (MeanTelemetryProvider) { - const DEFAULT_SAMPLES = 10; - - function plugin() { - return function install(openmct) { - openmct.types.addType('telemetry-mean', { - name: 'Telemetry Filter', - description: 'Provides telemetry values that represent the mean of the last N values of a telemetry stream', - creatable: true, - cssClass: 'icon-telemetry', - initialize: function (domainObject) { - domainObject.samples = DEFAULT_SAMPLES; - domainObject.telemetry = {}; - domainObject.telemetry.values = - openmct.time.getAllTimeSystems().map(function (timeSystem, index) { - return { - key: timeSystem.key, - name: timeSystem.name, - hints: { - domain: index + 1 - } - }; - }); - domainObject.telemetry.values.push({ - key: "value", - name: "Value", - hints: { - range: 1 - } - }); - }, - form: [ - { - "key": "telemetryPoint", - "name": "Telemetry Point", - "control": "textfield", - "required": true, - "cssClass": "l-input-lg" - }, - { - "key": "samples", - "name": "Samples to Average", - "control": "textfield", - "required": true, - "cssClass": "l-input-sm" - } - ] - }); - openmct.telemetry.addProvider(new MeanTelemetryProvider(openmct)); - }; - } - - return plugin; -}); +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, 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. + *****************************************************************************/ + +define(['./src/MeanTelemetryProvider'], function (MeanTelemetryProvider) { + const DEFAULT_SAMPLES = 10; + + function plugin() { + return function install(openmct) { + openmct.types.addType('telemetry-mean', { + name: 'Telemetry Filter', + description: + 'Provides telemetry values that represent the mean of the last N values of a telemetry stream', + creatable: true, + cssClass: 'icon-telemetry', + initialize: function (domainObject) { + domainObject.samples = DEFAULT_SAMPLES; + domainObject.telemetry = {}; + domainObject.telemetry.values = openmct.time + .getAllTimeSystems() + .map(function (timeSystem, index) { + return { + key: timeSystem.key, + name: timeSystem.name, + hints: { + domain: index + 1 + } + }; + }); + domainObject.telemetry.values.push({ + key: 'value', + name: 'Value', + hints: { + range: 1 + } + }); + }, + form: [ + { + key: 'telemetryPoint', + name: 'Telemetry Point', + control: 'textfield', + required: true, + cssClass: 'l-input-lg' + }, + { + key: 'samples', + name: 'Samples to Average', + control: 'textfield', + required: true, + cssClass: 'l-input-sm' + } + ] + }); + openmct.telemetry.addProvider(new MeanTelemetryProvider(openmct)); + }; + } + + return plugin; +}); diff --git a/src/plugins/telemetryMean/src/MeanTelemetryProvider.js b/src/plugins/telemetryMean/src/MeanTelemetryProvider.js index 00545f2327..65dd435d0b 100644 --- a/src/plugins/telemetryMean/src/MeanTelemetryProvider.js +++ b/src/plugins/telemetryMean/src/MeanTelemetryProvider.js @@ -20,97 +20,114 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - 'objectUtils', - './TelemetryAverager' -], function (objectUtils, TelemetryAverager) { +define(['objectUtils', './TelemetryAverager'], function (objectUtils, TelemetryAverager) { + function MeanTelemetryProvider(openmct) { + this.openmct = openmct; + this.telemetryAPI = openmct.telemetry; + this.timeAPI = openmct.time; + this.objectAPI = openmct.objects; + this.perObjectProviders = {}; + } - function MeanTelemetryProvider(openmct) { - this.openmct = openmct; - this.telemetryAPI = openmct.telemetry; - this.timeAPI = openmct.time; - this.objectAPI = openmct.objects; - this.perObjectProviders = {}; + MeanTelemetryProvider.prototype.canProvideTelemetry = function (domainObject) { + return domainObject.type === 'telemetry-mean'; + }; + + MeanTelemetryProvider.prototype.supportsRequest = + MeanTelemetryProvider.prototype.supportsSubscribe = + MeanTelemetryProvider.prototype.canProvideTelemetry; + + MeanTelemetryProvider.prototype.subscribe = function (domainObject, callback) { + let wrappedUnsubscribe; + let unsubscribeCalled = false; + const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); + const samples = domainObject.samples; + + this.objectAPI + .get(objectId) + .then( + function (linkedDomainObject) { + if (!unsubscribeCalled) { + wrappedUnsubscribe = this.subscribeToAverage(linkedDomainObject, samples, callback); + } + }.bind(this) + ) + .catch(logError); + + return function unsubscribe() { + unsubscribeCalled = true; + if (wrappedUnsubscribe !== undefined) { + wrappedUnsubscribe(); + } + }; + }; + + MeanTelemetryProvider.prototype.subscribeToAverage = function (domainObject, samples, callback) { + const telemetryAverager = new TelemetryAverager( + this.telemetryAPI, + this.timeAPI, + domainObject, + samples, + callback + ); + const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager); + + return this.telemetryAPI.subscribe(domainObject, createAverageDatum); + }; + + MeanTelemetryProvider.prototype.request = function (domainObject, request) { + const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); + const samples = domainObject.samples; + + return this.objectAPI.get(objectId).then( + function (linkedDomainObject) { + return this.requestAverageTelemetry(linkedDomainObject, request, samples); + }.bind(this) + ); + }; + + /** + * @private + */ + MeanTelemetryProvider.prototype.requestAverageTelemetry = function ( + domainObject, + request, + samples + ) { + const averageData = []; + const addToAverageData = averageData.push.bind(averageData); + const telemetryAverager = new TelemetryAverager( + this.telemetryAPI, + this.timeAPI, + domainObject, + samples, + addToAverageData + ); + const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager); + + return this.telemetryAPI.request(domainObject, request).then(function (telemetryData) { + telemetryData.forEach(createAverageDatum); + + return averageData; + }); + }; + + /** + * @private + */ + MeanTelemetryProvider.prototype.getLinkedObject = function (domainObject) { + const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); + + return this.objectAPI.get(objectId); + }; + + function logError(error) { + if (error.stack) { + console.error(error.stack); + } else { + console.error(error); } + } - MeanTelemetryProvider.prototype.canProvideTelemetry = function (domainObject) { - return domainObject.type === 'telemetry-mean'; - }; - - MeanTelemetryProvider.prototype.supportsRequest = - MeanTelemetryProvider.prototype.supportsSubscribe = - MeanTelemetryProvider.prototype.canProvideTelemetry; - - MeanTelemetryProvider.prototype.subscribe = function (domainObject, callback) { - let wrappedUnsubscribe; - let unsubscribeCalled = false; - const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); - const samples = domainObject.samples; - - this.objectAPI.get(objectId) - .then(function (linkedDomainObject) { - if (!unsubscribeCalled) { - wrappedUnsubscribe = this.subscribeToAverage(linkedDomainObject, samples, callback); - } - }.bind(this)) - .catch(logError); - - return function unsubscribe() { - unsubscribeCalled = true; - if (wrappedUnsubscribe !== undefined) { - wrappedUnsubscribe(); - } - }; - }; - - MeanTelemetryProvider.prototype.subscribeToAverage = function (domainObject, samples, callback) { - const telemetryAverager = new TelemetryAverager(this.telemetryAPI, this.timeAPI, domainObject, samples, callback); - const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager); - - return this.telemetryAPI.subscribe(domainObject, createAverageDatum); - }; - - MeanTelemetryProvider.prototype.request = function (domainObject, request) { - const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); - const samples = domainObject.samples; - - return this.objectAPI.get(objectId).then(function (linkedDomainObject) { - return this.requestAverageTelemetry(linkedDomainObject, request, samples); - }.bind(this)); - }; - - /** - * @private - */ - MeanTelemetryProvider.prototype.requestAverageTelemetry = function (domainObject, request, samples) { - const averageData = []; - const addToAverageData = averageData.push.bind(averageData); - const telemetryAverager = new TelemetryAverager(this.telemetryAPI, this.timeAPI, domainObject, samples, addToAverageData); - const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager); - - return this.telemetryAPI.request(domainObject, request).then(function (telemetryData) { - telemetryData.forEach(createAverageDatum); - - return averageData; - }); - }; - - /** - * @private - */ - MeanTelemetryProvider.prototype.getLinkedObject = function (domainObject) { - const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); - - return this.objectAPI.get(objectId); - }; - - function logError(error) { - if (error.stack) { - console.error(error.stack); - } else { - console.error(error); - } - } - - return MeanTelemetryProvider; + return MeanTelemetryProvider; }); diff --git a/src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js b/src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js index a0b402323a..53015cce7e 100644 --- a/src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js +++ b/src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js @@ -20,599 +20,604 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ /* eslint-disable no-invalid-this */ -define([ - "./MeanTelemetryProvider", - "./MockTelemetryApi" -], function ( - MeanTelemetryProvider, - MockTelemetryApi +define(['./MeanTelemetryProvider', './MockTelemetryApi'], function ( + MeanTelemetryProvider, + MockTelemetryApi ) { - const RANGE_KEY = 'value'; - - describe("The Mean Telemetry Provider", function () { - let mockApi; - let meanTelemetryProvider; - let mockDomainObject; - let associatedObject; - let allPromises; - - beforeEach(function () { - allPromises = []; - createMockApi(); - setTimeSystemTo('utc'); - createMockObjects(); - meanTelemetryProvider = new MeanTelemetryProvider(mockApi); - }); - - it("supports telemetry-mean objects only", function () { - const mockTelemetryMeanObject = mockObjectWithType('telemetry-mean'); - const mockOtherObject = mockObjectWithType('other'); - - expect(meanTelemetryProvider.canProvideTelemetry(mockTelemetryMeanObject)).toBe(true); - expect(meanTelemetryProvider.canProvideTelemetry(mockOtherObject)).toBe(false); - }); - - describe("the subscribe function", function () { - let subscriptionCallback; - - beforeEach(function () { - subscriptionCallback = jasmine.createSpy('subscriptionCallback'); - }); - - it("subscribes to telemetry for the associated object", function () { - meanTelemetryProvider.subscribe(mockDomainObject); - - return expectObjectWasSubscribedTo(associatedObject); - }); - - it("returns a function that unsubscribes from the associated object", function () { - const unsubscribe = meanTelemetryProvider.subscribe(mockDomainObject); - - return waitForPromises() - .then(unsubscribe) - .then(waitForPromises) - .then(function () { - expect(mockApi.telemetry.unsubscribe).toHaveBeenCalled(); - }); - }); - - it("returns an average only when the sample size is reached", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - } - ]; - - setSampleSize(5); - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(function () { - expect(subscriptionCallback).not.toHaveBeenCalled(); - }); - }); - - it("correctly averages a sample of five values", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - } - ]; - const expectedAverages = [{ - 'utc': 5, - 'value': 222.44888 - }]; - - setSampleSize(5); - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, expectedAverages)); - }); - - it("correctly averages a sample of ten values", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - }, - { - 'utc': 6, - 'defaultRange': 2323.12 - }, - { - 'utc': 7, - 'defaultRange': 532.12 - }, - { - 'utc': 8, - 'defaultRange': 453.543 - }, - { - 'utc': 9, - 'defaultRange': 89.2111 - }, - { - 'utc': 10, - 'defaultRange': 0.543 - } - ]; - const expectedAverages = [{ - 'utc': 10, - 'value': 451.07815 - }]; - - setSampleSize(10); - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, expectedAverages)); - }); - - it("only averages values within its sample window", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - }, - { - 'utc': 6, - 'defaultRange': 2323.12 - }, - { - 'utc': 7, - 'defaultRange': 532.12 - }, - { - 'utc': 8, - 'defaultRange': 453.543 - }, - { - 'utc': 9, - 'defaultRange': 89.2111 - }, - { - 'utc': 10, - 'defaultRange': 0.543 - } - ]; - const expectedAverages = [ - { - 'utc': 5, - 'value': 222.44888 - }, - { - 'utc': 6, - 'value': 662.4482599999999 - }, - { - 'utc': 7, - 'value': 704.6078 - }, - { - 'utc': 8, - 'value': 773.02748 - }, - { - 'utc': 9, - 'value': 679.8234399999999 - }, - { - 'utc': 10, - 'value': 679.70742 - } - ]; - - setSampleSize(5); - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, expectedAverages)); - }); - describe("given telemetry input with range values", function () { - let inputTelemetry; - - beforeEach(function () { - inputTelemetry = [{ - 'utc': 1, - 'rangeKey': 5678, - 'otherKey': 9999 - }]; - setSampleSize(1); - }); - it("uses the 'rangeKey' input range, when it is the default, to calculate the average", function () { - const averageTelemetryForRangeKey = [{ - 'utc': 1, - 'value': 5678 - }]; - - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - mockApi.telemetry.setDefaultRangeTo('rangeKey'); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, averageTelemetryForRangeKey)); - }); - - it("uses the 'otherKey' input range, when it is the default, to calculate the average", function () { - const averageTelemetryForOtherKey = [{ - 'utc': 1, - 'value': 9999 - }]; - - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - mockApi.telemetry.setDefaultRangeTo('otherKey'); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, averageTelemetryForOtherKey)); - }); - }); - describe("given telemetry input with range values", function () { - let inputTelemetry; - - beforeEach(function () { - inputTelemetry = [{ - 'utc': 1, - 'rangeKey': 5678, - 'otherKey': 9999 - }]; - setSampleSize(1); - }); - it("uses the 'rangeKey' input range, when it is the default, to calculate the average", function () { - const averageTelemetryForRangeKey = [{ - 'utc': 1, - 'value': 5678 - }]; - - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - mockApi.telemetry.setDefaultRangeTo('rangeKey'); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, averageTelemetryForRangeKey)); - }); - - it("uses the 'otherKey' input range, when it is the default, to calculate the average", function () { - const averageTelemetryForOtherKey = [{ - 'utc': 1, - 'value': 9999 - }]; - - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - mockApi.telemetry.setDefaultRangeTo('otherKey'); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, averageTelemetryForOtherKey)); - }); - }); - - function feedInputTelemetry(inputTelemetry) { - inputTelemetry.forEach(mockApi.telemetry.mockReceiveTelemetry); - } - - function expectAveragesForTelemetry(expectedAverages) { - return waitForPromises().then(function () { - expectedAverages.forEach(function (averageDatum) { - expect(subscriptionCallback).toHaveBeenCalledWith(averageDatum); - }); - }); - } - - function expectObjectWasSubscribedTo(object) { - return waitForPromises().then(function () { - expect(mockApi.telemetry.subscribe).toHaveBeenCalledWith(object, jasmine.any(Function)); - }); - } - - }); - - describe("the request function", function () { - - it("requests telemetry for the associated object", function () { - whenTelemetryRequestedReturn([]); - - return meanTelemetryProvider.request(mockDomainObject).then(function () { - expect(mockApi.telemetry.request).toHaveBeenCalledWith(associatedObject, undefined); - }); - }); - - it("returns an average only when the sample size is reached", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - } - ]; - - setSampleSize(5); - whenTelemetryRequestedReturn(inputTelemetry); - - return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) { - expect(averageData.length).toBe(0); - }); - }); - - it("correctly averages a sample of five values", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - } - ]; - - setSampleSize(5); - whenTelemetryRequestedReturn(inputTelemetry); - - return meanTelemetryProvider.request(mockDomainObject) - .then(function (averageData) { - expectAverageToBe(222.44888, averageData); - }); - }); - - it("correctly averages a sample of ten values", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - }, - { - 'utc': 6, - 'defaultRange': 2323.12 - }, - { - 'utc': 7, - 'defaultRange': 532.12 - }, - { - 'utc': 8, - 'defaultRange': 453.543 - }, - { - 'utc': 9, - 'defaultRange': 89.2111 - }, - { - 'utc': 10, - 'defaultRange': 0.543 - } - ]; - - setSampleSize(10); - whenTelemetryRequestedReturn(inputTelemetry); - - return meanTelemetryProvider.request(mockDomainObject) - .then(function (averageData) { - expectAverageToBe(451.07815, averageData); - }); - }); - - it("only averages values within its sample window", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - }, - { - 'utc': 6, - 'defaultRange': 2323.12 - }, - { - 'utc': 7, - 'defaultRange': 532.12 - }, - { - 'utc': 8, - 'defaultRange': 453.543 - }, - { - 'utc': 9, - 'defaultRange': 89.2111 - }, - { - 'utc': 10, - 'defaultRange': 0.543 - } - ]; - - setSampleSize(5); - whenTelemetryRequestedReturn(inputTelemetry); - - return meanTelemetryProvider.request(mockDomainObject) - .then(function (averageData) { - expectAverageToBe(679.70742, averageData); - }); - }); - - function expectAverageToBe(expectedValue, averageData) { - const averageDatum = averageData[averageData.length - 1]; - expect(averageDatum[RANGE_KEY]).toBe(expectedValue); - } - - function whenTelemetryRequestedReturn(telemetry) { - mockApi.telemetry.request.and.returnValue(resolvePromiseWith(telemetry)); - } - }); - - function createMockObjects() { - mockDomainObject = { - telemetryPoint: 'someTelemetryPoint' - }; - associatedObject = {}; - mockApi.objects.get.and.returnValue(resolvePromiseWith(associatedObject)); - } - - function setSampleSize(sampleSize) { - mockDomainObject.samples = sampleSize; - } - - function createMockApi() { - mockApi = { - telemetry: new MockTelemetryApi(), - objects: createMockObjectApi(), - time: createMockTimeApi() - }; - } - - function createMockObjectApi() { - return jasmine.createSpyObj('ObjectAPI', [ - 'get' - ]); - } - - function mockObjectWithType(type) { - return { - type: type - }; - } - - function resolvePromiseWith(value) { - const promise = Promise.resolve(value); - allPromises.push(promise); - - return promise; - } - - function waitForPromises() { - return Promise.all(allPromises); - } - - function createMockTimeApi() { - return jasmine.createSpyObj("timeApi", ['timeSystem']); - } - - function setTimeSystemTo(timeSystemKey) { - mockApi.time.timeSystem.and.returnValue({ - key: timeSystemKey - }); - } + const RANGE_KEY = 'value'; + + describe('The Mean Telemetry Provider', function () { + let mockApi; + let meanTelemetryProvider; + let mockDomainObject; + let associatedObject; + let allPromises; + + beforeEach(function () { + allPromises = []; + createMockApi(); + setTimeSystemTo('utc'); + createMockObjects(); + meanTelemetryProvider = new MeanTelemetryProvider(mockApi); }); + it('supports telemetry-mean objects only', function () { + const mockTelemetryMeanObject = mockObjectWithType('telemetry-mean'); + const mockOtherObject = mockObjectWithType('other'); + + expect(meanTelemetryProvider.canProvideTelemetry(mockTelemetryMeanObject)).toBe(true); + expect(meanTelemetryProvider.canProvideTelemetry(mockOtherObject)).toBe(false); + }); + + describe('the subscribe function', function () { + let subscriptionCallback; + + beforeEach(function () { + subscriptionCallback = jasmine.createSpy('subscriptionCallback'); + }); + + it('subscribes to telemetry for the associated object', function () { + meanTelemetryProvider.subscribe(mockDomainObject); + + return expectObjectWasSubscribedTo(associatedObject); + }); + + it('returns a function that unsubscribes from the associated object', function () { + const unsubscribe = meanTelemetryProvider.subscribe(mockDomainObject); + + return waitForPromises() + .then(unsubscribe) + .then(waitForPromises) + .then(function () { + expect(mockApi.telemetry.unsubscribe).toHaveBeenCalled(); + }); + }); + + it('returns an average only when the sample size is reached', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + } + ]; + + setSampleSize(5); + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(function () { + expect(subscriptionCallback).not.toHaveBeenCalled(); + }); + }); + + it('correctly averages a sample of five values', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + } + ]; + const expectedAverages = [ + { + utc: 5, + value: 222.44888 + } + ]; + + setSampleSize(5); + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, expectedAverages)); + }); + + it('correctly averages a sample of ten values', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + }, + { + utc: 6, + defaultRange: 2323.12 + }, + { + utc: 7, + defaultRange: 532.12 + }, + { + utc: 8, + defaultRange: 453.543 + }, + { + utc: 9, + defaultRange: 89.2111 + }, + { + utc: 10, + defaultRange: 0.543 + } + ]; + const expectedAverages = [ + { + utc: 10, + value: 451.07815 + } + ]; + + setSampleSize(10); + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, expectedAverages)); + }); + + it('only averages values within its sample window', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + }, + { + utc: 6, + defaultRange: 2323.12 + }, + { + utc: 7, + defaultRange: 532.12 + }, + { + utc: 8, + defaultRange: 453.543 + }, + { + utc: 9, + defaultRange: 89.2111 + }, + { + utc: 10, + defaultRange: 0.543 + } + ]; + const expectedAverages = [ + { + utc: 5, + value: 222.44888 + }, + { + utc: 6, + value: 662.4482599999999 + }, + { + utc: 7, + value: 704.6078 + }, + { + utc: 8, + value: 773.02748 + }, + { + utc: 9, + value: 679.8234399999999 + }, + { + utc: 10, + value: 679.70742 + } + ]; + + setSampleSize(5); + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, expectedAverages)); + }); + describe('given telemetry input with range values', function () { + let inputTelemetry; + + beforeEach(function () { + inputTelemetry = [ + { + utc: 1, + rangeKey: 5678, + otherKey: 9999 + } + ]; + setSampleSize(1); + }); + it("uses the 'rangeKey' input range, when it is the default, to calculate the average", function () { + const averageTelemetryForRangeKey = [ + { + utc: 1, + value: 5678 + } + ]; + + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + mockApi.telemetry.setDefaultRangeTo('rangeKey'); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, averageTelemetryForRangeKey)); + }); + + it("uses the 'otherKey' input range, when it is the default, to calculate the average", function () { + const averageTelemetryForOtherKey = [ + { + utc: 1, + value: 9999 + } + ]; + + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + mockApi.telemetry.setDefaultRangeTo('otherKey'); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, averageTelemetryForOtherKey)); + }); + }); + describe('given telemetry input with range values', function () { + let inputTelemetry; + + beforeEach(function () { + inputTelemetry = [ + { + utc: 1, + rangeKey: 5678, + otherKey: 9999 + } + ]; + setSampleSize(1); + }); + it("uses the 'rangeKey' input range, when it is the default, to calculate the average", function () { + const averageTelemetryForRangeKey = [ + { + utc: 1, + value: 5678 + } + ]; + + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + mockApi.telemetry.setDefaultRangeTo('rangeKey'); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, averageTelemetryForRangeKey)); + }); + + it("uses the 'otherKey' input range, when it is the default, to calculate the average", function () { + const averageTelemetryForOtherKey = [ + { + utc: 1, + value: 9999 + } + ]; + + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + mockApi.telemetry.setDefaultRangeTo('otherKey'); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, averageTelemetryForOtherKey)); + }); + }); + + function feedInputTelemetry(inputTelemetry) { + inputTelemetry.forEach(mockApi.telemetry.mockReceiveTelemetry); + } + + function expectAveragesForTelemetry(expectedAverages) { + return waitForPromises().then(function () { + expectedAverages.forEach(function (averageDatum) { + expect(subscriptionCallback).toHaveBeenCalledWith(averageDatum); + }); + }); + } + + function expectObjectWasSubscribedTo(object) { + return waitForPromises().then(function () { + expect(mockApi.telemetry.subscribe).toHaveBeenCalledWith(object, jasmine.any(Function)); + }); + } + }); + + describe('the request function', function () { + it('requests telemetry for the associated object', function () { + whenTelemetryRequestedReturn([]); + + return meanTelemetryProvider.request(mockDomainObject).then(function () { + expect(mockApi.telemetry.request).toHaveBeenCalledWith(associatedObject, undefined); + }); + }); + + it('returns an average only when the sample size is reached', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + } + ]; + + setSampleSize(5); + whenTelemetryRequestedReturn(inputTelemetry); + + return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) { + expect(averageData.length).toBe(0); + }); + }); + + it('correctly averages a sample of five values', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + } + ]; + + setSampleSize(5); + whenTelemetryRequestedReturn(inputTelemetry); + + return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) { + expectAverageToBe(222.44888, averageData); + }); + }); + + it('correctly averages a sample of ten values', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + }, + { + utc: 6, + defaultRange: 2323.12 + }, + { + utc: 7, + defaultRange: 532.12 + }, + { + utc: 8, + defaultRange: 453.543 + }, + { + utc: 9, + defaultRange: 89.2111 + }, + { + utc: 10, + defaultRange: 0.543 + } + ]; + + setSampleSize(10); + whenTelemetryRequestedReturn(inputTelemetry); + + return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) { + expectAverageToBe(451.07815, averageData); + }); + }); + + it('only averages values within its sample window', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + }, + { + utc: 6, + defaultRange: 2323.12 + }, + { + utc: 7, + defaultRange: 532.12 + }, + { + utc: 8, + defaultRange: 453.543 + }, + { + utc: 9, + defaultRange: 89.2111 + }, + { + utc: 10, + defaultRange: 0.543 + } + ]; + + setSampleSize(5); + whenTelemetryRequestedReturn(inputTelemetry); + + return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) { + expectAverageToBe(679.70742, averageData); + }); + }); + + function expectAverageToBe(expectedValue, averageData) { + const averageDatum = averageData[averageData.length - 1]; + expect(averageDatum[RANGE_KEY]).toBe(expectedValue); + } + + function whenTelemetryRequestedReturn(telemetry) { + mockApi.telemetry.request.and.returnValue(resolvePromiseWith(telemetry)); + } + }); + + function createMockObjects() { + mockDomainObject = { + telemetryPoint: 'someTelemetryPoint' + }; + associatedObject = {}; + mockApi.objects.get.and.returnValue(resolvePromiseWith(associatedObject)); + } + + function setSampleSize(sampleSize) { + mockDomainObject.samples = sampleSize; + } + + function createMockApi() { + mockApi = { + telemetry: new MockTelemetryApi(), + objects: createMockObjectApi(), + time: createMockTimeApi() + }; + } + + function createMockObjectApi() { + return jasmine.createSpyObj('ObjectAPI', ['get']); + } + + function mockObjectWithType(type) { + return { + type: type + }; + } + + function resolvePromiseWith(value) { + const promise = Promise.resolve(value); + allPromises.push(promise); + + return promise; + } + + function waitForPromises() { + return Promise.all(allPromises); + } + + function createMockTimeApi() { + return jasmine.createSpyObj('timeApi', ['timeSystem']); + } + + function setTimeSystemTo(timeSystemKey) { + mockApi.time.timeSystem.and.returnValue({ + key: timeSystemKey + }); + } + }); }); diff --git a/src/plugins/telemetryMean/src/MockTelemetryApi.js b/src/plugins/telemetryMean/src/MockTelemetryApi.js index 34c4e41723..85cef97571 100644 --- a/src/plugins/telemetryMean/src/MockTelemetryApi.js +++ b/src/plugins/telemetryMean/src/MockTelemetryApi.js @@ -21,87 +21,81 @@ *****************************************************************************/ define([], function () { + function MockTelemetryApi() { + this.createSpy('subscribe'); + this.createSpy('getMetadata'); - function MockTelemetryApi() { - this.createSpy('subscribe'); - this.createSpy('getMetadata'); + this.metadata = this.createMockMetadata(); + this.setDefaultRangeTo('defaultRange'); + this.unsubscribe = jasmine.createSpy('unsubscribe'); + this.mockReceiveTelemetry = this.mockReceiveTelemetry.bind(this); + } - this.metadata = this.createMockMetadata(); - this.setDefaultRangeTo('defaultRange'); - this.unsubscribe = jasmine.createSpy('unsubscribe'); - this.mockReceiveTelemetry = this.mockReceiveTelemetry.bind(this); - } + MockTelemetryApi.prototype.subscribe = function () { + return this.unsubscribe; + }; - MockTelemetryApi.prototype.subscribe = function () { - return this.unsubscribe; + MockTelemetryApi.prototype.getMetadata = function (object) { + return this.metadata; + }; + + MockTelemetryApi.prototype.request = jasmine.createSpy('request'); + + MockTelemetryApi.prototype.getValueFormatter = function (valueMetadata) { + const mockValueFormatter = jasmine.createSpyObj('valueFormatter', ['parse']); + + mockValueFormatter.parse.and.callFake(function (value) { + return value[valueMetadata.key]; + }); + + return mockValueFormatter; + }; + + MockTelemetryApi.prototype.mockReceiveTelemetry = function (newTelemetryDatum) { + const subscriptionCallback = this.subscribe.calls.mostRecent().args[1]; + subscriptionCallback(newTelemetryDatum); + }; + + /** + * @private + */ + MockTelemetryApi.prototype.onRequestReturn = function (telemetryData) { + this.requestTelemetry = telemetryData; + }; + + /** + * @private + */ + MockTelemetryApi.prototype.setDefaultRangeTo = function (rangeKey) { + const mockMetadataValue = { + key: rangeKey }; + this.metadata.valuesForHints.and.returnValue([mockMetadataValue]); + }; - MockTelemetryApi.prototype.getMetadata = function (object) { - return this.metadata; - }; + /** + * @private + */ + MockTelemetryApi.prototype.createMockMetadata = function () { + const mockMetadata = jasmine.createSpyObj('metadata', ['value', 'valuesForHints']); - MockTelemetryApi.prototype.request = jasmine.createSpy('request'); + mockMetadata.value.and.callFake(function (key) { + return { + key: key + }; + }); - MockTelemetryApi.prototype.getValueFormatter = function (valueMetadata) { - const mockValueFormatter = jasmine.createSpyObj("valueFormatter", [ - "parse" - ]); + return mockMetadata; + }; - mockValueFormatter.parse.and.callFake(function (value) { - return value[valueMetadata.key]; - }); + /** + * @private + */ + MockTelemetryApi.prototype.createSpy = function (functionName) { + this[functionName] = this[functionName].bind(this); + spyOn(this, functionName); + this[functionName].and.callThrough(); + }; - return mockValueFormatter; - }; - - MockTelemetryApi.prototype.mockReceiveTelemetry = function (newTelemetryDatum) { - const subscriptionCallback = this.subscribe.calls.mostRecent().args[1]; - subscriptionCallback(newTelemetryDatum); - }; - - /** - * @private - */ - MockTelemetryApi.prototype.onRequestReturn = function (telemetryData) { - this.requestTelemetry = telemetryData; - }; - - /** - * @private - */ - MockTelemetryApi.prototype.setDefaultRangeTo = function (rangeKey) { - const mockMetadataValue = { - key: rangeKey - }; - this.metadata.valuesForHints.and.returnValue([mockMetadataValue]); - }; - - /** - * @private - */ - MockTelemetryApi.prototype.createMockMetadata = function () { - const mockMetadata = jasmine.createSpyObj("metadata", [ - 'value', - 'valuesForHints' - ]); - - mockMetadata.value.and.callFake(function (key) { - return { - key: key - }; - }); - - return mockMetadata; - }; - - /** - * @private - */ - MockTelemetryApi.prototype.createSpy = function (functionName) { - this[functionName] = this[functionName].bind(this); - spyOn(this, functionName); - this[functionName].and.callThrough(); - }; - - return MockTelemetryApi; + return MockTelemetryApi; }); diff --git a/src/plugins/telemetryMean/src/TelemetryAverager.js b/src/plugins/telemetryMean/src/TelemetryAverager.js index a2eb10b62b..3d40deee44 100644 --- a/src/plugins/telemetryMean/src/TelemetryAverager.js +++ b/src/plugins/telemetryMean/src/TelemetryAverager.js @@ -21,100 +21,99 @@ *****************************************************************************/ define([], function () { + function TelemetryAverager(telemetryAPI, timeAPI, domainObject, samples, averageDatumCallback) { + this.telemetryAPI = telemetryAPI; + this.timeAPI = timeAPI; - function TelemetryAverager(telemetryAPI, timeAPI, domainObject, samples, averageDatumCallback) { - this.telemetryAPI = telemetryAPI; - this.timeAPI = timeAPI; + this.domainObject = domainObject; + this.samples = samples; + this.averagingWindow = []; - this.domainObject = domainObject; - this.samples = samples; - this.averagingWindow = []; + this.rangeKey = undefined; + this.rangeFormatter = undefined; + this.setRangeKeyAndFormatter(); - this.rangeKey = undefined; - this.rangeFormatter = undefined; - this.setRangeKeyAndFormatter(); + // Defined dynamically based on current time system + this.domainKey = undefined; + this.domainFormatter = undefined; - // Defined dynamically based on current time system - this.domainKey = undefined; - this.domainFormatter = undefined; + this.averageDatumCallback = averageDatumCallback; + } - this.averageDatumCallback = averageDatumCallback; + TelemetryAverager.prototype.createAverageDatum = function (telemetryDatum) { + this.setDomainKeyAndFormatter(); + + const timeValue = this.domainFormatter.parse(telemetryDatum); + const rangeValue = this.rangeFormatter.parse(telemetryDatum); + + this.averagingWindow.push(rangeValue); + + if (this.averagingWindow.length < this.samples) { + // We do not have enough data to produce an average + return; + } else if (this.averagingWindow.length > this.samples) { + //Do not let averaging window grow beyond defined sample size + this.averagingWindow.shift(); } - TelemetryAverager.prototype.createAverageDatum = function (telemetryDatum) { - this.setDomainKeyAndFormatter(); + const averageValue = this.calculateMean(); - const timeValue = this.domainFormatter.parse(telemetryDatum); - const rangeValue = this.rangeFormatter.parse(telemetryDatum); + const meanDatum = {}; + meanDatum[this.domainKey] = timeValue; + meanDatum.value = averageValue; - this.averagingWindow.push(rangeValue); + this.averageDatumCallback(meanDatum); + }; - if (this.averagingWindow.length < this.samples) { - // We do not have enough data to produce an average - return; - } else if (this.averagingWindow.length > this.samples) { - //Do not let averaging window grow beyond defined sample size - this.averagingWindow.shift(); - } + /** + * @private + */ + TelemetryAverager.prototype.calculateMean = function () { + let sum = 0; + let i = 0; - const averageValue = this.calculateMean(); + for (; i < this.averagingWindow.length; i++) { + sum += this.averagingWindow[i]; + } - const meanDatum = {}; - meanDatum[this.domainKey] = timeValue; - meanDatum.value = averageValue; + return sum / this.averagingWindow.length; + }; - this.averageDatumCallback(meanDatum); - }; + /** + * The mean telemetry filter produces domain values in whatever time + * system is currently selected from the conductor. Because this can + * change dynamically, the averager needs to be updated regularly with + * the current domain. + * @private + */ + TelemetryAverager.prototype.setDomainKeyAndFormatter = function () { + const domainKey = this.timeAPI.timeSystem().key; + if (domainKey !== this.domainKey) { + this.domainKey = domainKey; + this.domainFormatter = this.getFormatter(domainKey); + } + }; - /** - * @private - */ - TelemetryAverager.prototype.calculateMean = function () { - let sum = 0; - let i = 0; + /** + * @private + */ + TelemetryAverager.prototype.setRangeKeyAndFormatter = function () { + const metadatas = this.telemetryAPI.getMetadata(this.domainObject); + const rangeValues = metadatas.valuesForHints(['range']); - for (; i < this.averagingWindow.length; i++) { - sum += this.averagingWindow[i]; - } + this.rangeKey = rangeValues[0].key; + this.rangeFormatter = this.getFormatter(this.rangeKey); + }; - return sum / this.averagingWindow.length; - }; + /** + * @private + */ + TelemetryAverager.prototype.getFormatter = function (key) { + const objectMetadata = this.telemetryAPI.getMetadata(this.domainObject); + const valueMetadata = objectMetadata.value(key); - /** - * The mean telemetry filter produces domain values in whatever time - * system is currently selected from the conductor. Because this can - * change dynamically, the averager needs to be updated regularly with - * the current domain. - * @private - */ - TelemetryAverager.prototype.setDomainKeyAndFormatter = function () { - const domainKey = this.timeAPI.timeSystem().key; - if (domainKey !== this.domainKey) { - this.domainKey = domainKey; - this.domainFormatter = this.getFormatter(domainKey); - } - }; + return this.telemetryAPI.getValueFormatter(valueMetadata); + }; - /** - * @private - */ - TelemetryAverager.prototype.setRangeKeyAndFormatter = function () { - const metadatas = this.telemetryAPI.getMetadata(this.domainObject); - const rangeValues = metadatas.valuesForHints(['range']); - - this.rangeKey = rangeValues[0].key; - this.rangeFormatter = this.getFormatter(this.rangeKey); - }; - - /** - * @private - */ - TelemetryAverager.prototype.getFormatter = function (key) { - const objectMetadata = this.telemetryAPI.getMetadata(this.domainObject); - const valueMetadata = objectMetadata.value(key); - - return this.telemetryAPI.getValueFormatter(valueMetadata); - }; - - return TelemetryAverager; + return TelemetryAverager; }); diff --git a/src/plugins/telemetryTable/TableConfigurationViewProvider.js b/src/plugins/telemetryTable/TableConfigurationViewProvider.js index 6da6a4ce50..400eca37ba 100644 --- a/src/plugins/telemetryTable/TableConfigurationViewProvider.js +++ b/src/plugins/telemetryTable/TableConfigurationViewProvider.js @@ -21,64 +21,58 @@ *****************************************************************************/ define([ - 'objectUtils', - './components/table-configuration.vue', - './TelemetryTableConfiguration', - 'vue' -], function ( - objectUtils, - TableConfigurationComponent, - TelemetryTableConfiguration, - Vue -) { + 'objectUtils', + './components/table-configuration.vue', + './TelemetryTableConfiguration', + 'vue' +], function (objectUtils, TableConfigurationComponent, TelemetryTableConfiguration, Vue) { + function TableConfigurationViewProvider(openmct) { + return { + key: 'table-configuration', + name: 'Configuration', + canView: function (selection) { + if (selection.length !== 1 || selection[0].length === 0) { + return false; + } + + let object = selection[0][0].context.item; + + return object && object.type === 'table'; + }, + view: function (selection) { + let component; + let domainObject = selection[0][0].context.item; + let tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct); - function TableConfigurationViewProvider(openmct) { return { - key: 'table-configuration', - name: 'Configuration', - canView: function (selection) { - if (selection.length !== 1 || selection[0].length === 0) { - return false; - } - - let object = selection[0][0].context.item; - - return object && object.type === 'table'; - }, - view: function (selection) { - let component; - let domainObject = selection[0][0].context.item; - let tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct); - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - TableConfiguration: TableConfigurationComponent.default - }, - provide: { - openmct, - tableConfiguration - }, - template: '' - }); - }, - priority: function () { - return 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - - tableConfiguration = undefined; - } - }; + show: function (element) { + component = new Vue({ + el: element, + components: { + TableConfiguration: TableConfigurationComponent.default + }, + provide: { + openmct, + tableConfiguration + }, + template: '' + }); + }, + priority: function () { + return 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; } - }; - } - return TableConfigurationViewProvider; + tableConfiguration = undefined; + } + }; + } + }; + } + + return TableConfigurationViewProvider; }); diff --git a/src/plugins/telemetryTable/TelemetryTable.js b/src/plugins/telemetryTable/TelemetryTable.js index 4c8a2ec121..f133bc9630 100644 --- a/src/plugins/telemetryTable/TelemetryTable.js +++ b/src/plugins/telemetryTable/TelemetryTable.js @@ -21,395 +21,415 @@ *****************************************************************************/ define([ - 'EventEmitter', - 'lodash', - './collections/TableRowCollection', - './TelemetryTableRow', - './TelemetryTableNameColumn', - './TelemetryTableColumn', - './TelemetryTableUnitColumn', - './TelemetryTableConfiguration', - '../../utils/staleness' + 'EventEmitter', + 'lodash', + './collections/TableRowCollection', + './TelemetryTableRow', + './TelemetryTableNameColumn', + './TelemetryTableColumn', + './TelemetryTableUnitColumn', + './TelemetryTableConfiguration', + '../../utils/staleness' ], function ( - EventEmitter, - _, - TableRowCollection, - TelemetryTableRow, - TelemetryTableNameColumn, - TelemetryTableColumn, - TelemetryTableUnitColumn, - TelemetryTableConfiguration, - StalenessUtils + EventEmitter, + _, + TableRowCollection, + TelemetryTableRow, + TelemetryTableNameColumn, + TelemetryTableColumn, + TelemetryTableUnitColumn, + TelemetryTableConfiguration, + StalenessUtils ) { - class TelemetryTable extends EventEmitter { - constructor(domainObject, openmct) { - super(); - - this.domainObject = domainObject; - this.openmct = openmct; - this.rowCount = 100; - this.tableComposition = undefined; - this.datumCache = []; - this.configuration = new TelemetryTableConfiguration(domainObject, openmct); - this.paused = false; - this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); - - this.telemetryObjects = {}; - this.telemetryCollections = {}; - this.delayedActions = []; - this.outstandingRequests = 0; - this.stalenessSubscription = {}; - - this.addTelemetryObject = this.addTelemetryObject.bind(this); - this.removeTelemetryObject = this.removeTelemetryObject.bind(this); - this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this); - this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this); - this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this); - this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this); - this.isTelemetryObject = this.isTelemetryObject.bind(this); - this.updateFilters = this.updateFilters.bind(this); - this.clearData = this.clearData.bind(this); - this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this); - - this.filterObserver = undefined; - - this.createTableRowCollections(); - } - - /** - * @private - */ - addNameColumn(telemetryObject, metadataValues) { - let metadatum = metadataValues.find(m => m.key === 'name'); - if (!metadatum) { - metadatum = { - format: 'string', - key: 'name', - name: 'Name' - }; - } - - const column = new TelemetryTableNameColumn(this.openmct, telemetryObject, metadatum); - - this.configuration.addSingleColumnForObject(telemetryObject, column); - } - - initialize() { - if (this.domainObject.type === 'table') { - this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters); - this.filters = this.domainObject.configuration.filters; - this.loadComposition(); - } else { - this.addTelemetryObject(this.domainObject); - } - } - - createTableRowCollections() { - this.tableRows = new TableRowCollection(); - - //Fetch any persisted default sort - let sortOptions = this.configuration.getConfiguration().sortOptions; - - //If no persisted sort order, default to sorting by time system, ascending. - sortOptions = sortOptions || { - key: this.openmct.time.timeSystem().key, - direction: 'asc' - }; - - this.tableRows.sortBy(sortOptions); - this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData); - } - - loadComposition() { - this.tableComposition = this.openmct.composition.get(this.domainObject); - - if (this.tableComposition !== undefined) { - this.tableComposition.load().then((composition) => { - - composition = composition.filter(this.isTelemetryObject); - composition.forEach(this.addTelemetryObject); - - this.tableComposition.on('add', this.addTelemetryObject); - this.tableComposition.on('remove', this.removeTelemetryObject); - }); - } - } - - addTelemetryObject(telemetryObject) { - this.addColumnsForObject(telemetryObject, true); - - const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); - let requestOptions = this.buildOptionsFromConfiguration(telemetryObject); - let columnMap = this.getColumnMapForObject(keyString); - let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); - - const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator); - const telemetryRemover = this.getTelemetryRemover(); - - this.removeTelemetryCollection(keyString); - - this.telemetryCollections[keyString] = this.openmct.telemetry - .requestCollection(telemetryObject, requestOptions); - - this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests); - this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests); - this.telemetryCollections[keyString].on('remove', telemetryRemover); - this.telemetryCollections[keyString].on('add', telemetryProcessor); - this.telemetryCollections[keyString].on('clear', this.clearData); - this.telemetryCollections[keyString].load(); - - this.stalenessSubscription[keyString] = {}; - this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils.default(this.openmct, telemetryObject); - this.openmct.telemetry.isStale(telemetryObject).then(stalenessResponse => { - if (stalenessResponse !== undefined) { - this.handleStaleness(keyString, stalenessResponse); - } - }); - const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(telemetryObject, (stalenessResponse) => { - this.handleStaleness(keyString, stalenessResponse); - }); - - this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription; - - this.telemetryObjects[keyString] = { - telemetryObject, - keyString, - requestOptions, - columnMap, - limitEvaluator - }; - - this.emit('object-added', telemetryObject); - } - - handleStaleness(keyString, stalenessResponse, skipCheck = false) { - if (skipCheck || this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(stalenessResponse, keyString)) { - this.emit('telemetry-staleness', { - keyString, - isStale: stalenessResponse.isStale - }); - } - } - - getTelemetryProcessor(keyString, columnMap, limitEvaluator) { - return (telemetry) => { - //Check that telemetry object has not been removed since telemetry was requested. - if (!this.telemetryObjects[keyString]) { - return; - } - - let telemetryRows = telemetry.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); - - if (this.paused) { - this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add')); - } else { - this.tableRows.addRows(telemetryRows); - } - }; - } - - getTelemetryRemover() { - return (telemetry) => { - if (this.paused) { - this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry)); - } else { - this.tableRows.removeRowsByData(telemetry); - } - }; - } - - /** - * @private - */ - incrementOutstandingRequests() { - if (this.outstandingRequests === 0) { - this.emit('outstanding-requests', true); - } - - this.outstandingRequests++; - } - - /** - * @private - */ - decrementOutstandingRequests() { - this.outstandingRequests--; - - if (this.outstandingRequests === 0) { - this.emit('outstanding-requests', false); - } - } - - // will pull all necessary information for all existing bounded telemetry - // and pass to table row collection to reset without making any new requests - // triggered by filtering - resetRowsFromAllData() { - let allRows = []; - - Object.keys(this.telemetryCollections).forEach(keyString => { - let { columnMap, limitEvaluator } = this.telemetryObjects[keyString]; - - this.telemetryCollections[keyString].getAll().forEach(datum => { - allRows.push(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); - }); - }); - - this.tableRows.clearRowsFromTableAndFilter(allRows); - } - - updateFilters(updatedFilters) { - let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); - - if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { - this.filters = deepCopiedFilters; - this.tableRows.clear(); - this.clearAndResubscribe(); - } else { - this.filters = deepCopiedFilters; - } - } - - clearAndResubscribe() { - let objectKeys = Object.keys(this.telemetryObjects); - - this.tableRows.clear(); - objectKeys.forEach((keyString) => { - this.addTelemetryObject(this.telemetryObjects[keyString].telemetryObject); - }); - } - - removeTelemetryObject(objectIdentifier) { - const keyString = this.openmct.objects.makeKeyString(objectIdentifier); - const SKIP_CHECK = true; - - this.configuration.removeColumnsForObject(objectIdentifier, true); - this.tableRows.removeRowsByObject(keyString); - - this.removeTelemetryCollection(keyString); - delete this.telemetryObjects[keyString]; - - this.emit('object-removed', objectIdentifier); - - this.stalenessSubscription[keyString].unsubscribe(); - this.stalenessSubscription[keyString].stalenessUtils.destroy(); - this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK); - delete this.stalenessSubscription[keyString]; - } - - clearData() { - this.tableRows.clear(); - this.emit('refresh'); - } - - addColumnsForObject(telemetryObject) { - let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); - - this.addNameColumn(telemetryObject, metadataValues); - metadataValues.forEach(metadatum => { - if (metadatum.key === 'name') { - return; - } - - let column = this.createColumn(metadatum); - this.configuration.addSingleColumnForObject(telemetryObject, column); - // add units column if available - if (metadatum.unit !== undefined) { - let unitColumn = this.createUnitColumn(metadatum); - this.configuration.addSingleColumnForObject(telemetryObject, unitColumn); - } - }); - } - - getColumnMapForObject(objectKeyString) { - let columns = this.configuration.getColumns(); - - if (columns[objectKeyString]) { - return columns[objectKeyString].reduce((map, column) => { - map[column.getKey()] = column; - - return map; - }, {}); - } - - return {}; - } - - buildOptionsFromConfiguration(telemetryObject) { - let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); - let filters = this.domainObject.configuration - && this.domainObject.configuration.filters - && this.domainObject.configuration.filters[keyString]; - - return {filters} || {}; - } - - createColumn(metadatum) { - return new TelemetryTableColumn(this.openmct, metadatum); - } - - createUnitColumn(metadatum) { - return new TelemetryTableUnitColumn(this.openmct, metadatum); - } - - isTelemetryObject(domainObject) { - return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry'); - } - - sortBy(sortOptions) { - this.tableRows.sortBy(sortOptions); - - if (this.openmct.editor.isEditing()) { - let configuration = this.configuration.getConfiguration(); - configuration.sortOptions = sortOptions; - this.configuration.updateConfiguration(configuration); - } - } - - runDelayedActions() { - this.delayedActions.forEach(action => action()); - this.delayedActions = []; - } - - removeTelemetryCollection(keyString) { - if (this.telemetryCollections[keyString]) { - this.telemetryCollections[keyString].destroy(); - this.telemetryCollections[keyString] = undefined; - delete this.telemetryCollections[keyString]; - } - } - - pause() { - this.paused = true; - } - - unpause() { - this.paused = false; - this.runDelayedActions(); - } - - destroy() { - this.tableRows.destroy(); - - this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData); - - let keystrings = Object.keys(this.telemetryCollections); - keystrings.forEach(this.removeTelemetryCollection); - - if (this.filterObserver) { - this.filterObserver(); - } - - Object.values(this.stalenessSubscription).forEach(stalenessSubscription => { - stalenessSubscription.unsubscribe(); - stalenessSubscription.stalenessUtils.destroy(); - }); - - if (this.tableComposition !== undefined) { - this.tableComposition.off('add', this.addTelemetryObject); - this.tableComposition.off('remove', this.removeTelemetryObject); - } - } + class TelemetryTable extends EventEmitter { + constructor(domainObject, openmct) { + super(); + + this.domainObject = domainObject; + this.openmct = openmct; + this.rowCount = 100; + this.tableComposition = undefined; + this.datumCache = []; + this.configuration = new TelemetryTableConfiguration(domainObject, openmct); + this.paused = false; + this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); + + this.telemetryObjects = {}; + this.telemetryCollections = {}; + this.delayedActions = []; + this.outstandingRequests = 0; + this.stalenessSubscription = {}; + + this.addTelemetryObject = this.addTelemetryObject.bind(this); + this.removeTelemetryObject = this.removeTelemetryObject.bind(this); + this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this); + this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this); + this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this); + this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this); + this.isTelemetryObject = this.isTelemetryObject.bind(this); + this.updateFilters = this.updateFilters.bind(this); + this.clearData = this.clearData.bind(this); + this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this); + + this.filterObserver = undefined; + + this.createTableRowCollections(); } - return TelemetryTable; + /** + * @private + */ + addNameColumn(telemetryObject, metadataValues) { + let metadatum = metadataValues.find((m) => m.key === 'name'); + if (!metadatum) { + metadatum = { + format: 'string', + key: 'name', + name: 'Name' + }; + } + + const column = new TelemetryTableNameColumn(this.openmct, telemetryObject, metadatum); + + this.configuration.addSingleColumnForObject(telemetryObject, column); + } + + initialize() { + if (this.domainObject.type === 'table') { + this.filterObserver = this.openmct.objects.observe( + this.domainObject, + 'configuration.filters', + this.updateFilters + ); + this.filters = this.domainObject.configuration.filters; + this.loadComposition(); + } else { + this.addTelemetryObject(this.domainObject); + } + } + + createTableRowCollections() { + this.tableRows = new TableRowCollection(); + + //Fetch any persisted default sort + let sortOptions = this.configuration.getConfiguration().sortOptions; + + //If no persisted sort order, default to sorting by time system, ascending. + sortOptions = sortOptions || { + key: this.openmct.time.timeSystem().key, + direction: 'asc' + }; + + this.tableRows.sortBy(sortOptions); + this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData); + } + + loadComposition() { + this.tableComposition = this.openmct.composition.get(this.domainObject); + + if (this.tableComposition !== undefined) { + this.tableComposition.load().then((composition) => { + composition = composition.filter(this.isTelemetryObject); + composition.forEach(this.addTelemetryObject); + + this.tableComposition.on('add', this.addTelemetryObject); + this.tableComposition.on('remove', this.removeTelemetryObject); + }); + } + } + + addTelemetryObject(telemetryObject) { + this.addColumnsForObject(telemetryObject, true); + + const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + let requestOptions = this.buildOptionsFromConfiguration(telemetryObject); + let columnMap = this.getColumnMapForObject(keyString); + let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); + + const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator); + const telemetryRemover = this.getTelemetryRemover(); + + this.removeTelemetryCollection(keyString); + + this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection( + telemetryObject, + requestOptions + ); + + this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests); + this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests); + this.telemetryCollections[keyString].on('remove', telemetryRemover); + this.telemetryCollections[keyString].on('add', telemetryProcessor); + this.telemetryCollections[keyString].on('clear', this.clearData); + this.telemetryCollections[keyString].load(); + + this.stalenessSubscription[keyString] = {}; + this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils.default( + this.openmct, + telemetryObject + ); + this.openmct.telemetry.isStale(telemetryObject).then((stalenessResponse) => { + if (stalenessResponse !== undefined) { + this.handleStaleness(keyString, stalenessResponse); + } + }); + const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness( + telemetryObject, + (stalenessResponse) => { + this.handleStaleness(keyString, stalenessResponse); + } + ); + + this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription; + + this.telemetryObjects[keyString] = { + telemetryObject, + keyString, + requestOptions, + columnMap, + limitEvaluator + }; + + this.emit('object-added', telemetryObject); + } + + handleStaleness(keyString, stalenessResponse, skipCheck = false) { + if ( + skipCheck || + this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness( + stalenessResponse, + keyString + ) + ) { + this.emit('telemetry-staleness', { + keyString, + isStale: stalenessResponse.isStale + }); + } + } + + getTelemetryProcessor(keyString, columnMap, limitEvaluator) { + return (telemetry) => { + //Check that telemetry object has not been removed since telemetry was requested. + if (!this.telemetryObjects[keyString]) { + return; + } + + let telemetryRows = telemetry.map( + (datum) => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator) + ); + + if (this.paused) { + this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add')); + } else { + this.tableRows.addRows(telemetryRows); + } + }; + } + + getTelemetryRemover() { + return (telemetry) => { + if (this.paused) { + this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry)); + } else { + this.tableRows.removeRowsByData(telemetry); + } + }; + } + + /** + * @private + */ + incrementOutstandingRequests() { + if (this.outstandingRequests === 0) { + this.emit('outstanding-requests', true); + } + + this.outstandingRequests++; + } + + /** + * @private + */ + decrementOutstandingRequests() { + this.outstandingRequests--; + + if (this.outstandingRequests === 0) { + this.emit('outstanding-requests', false); + } + } + + // will pull all necessary information for all existing bounded telemetry + // and pass to table row collection to reset without making any new requests + // triggered by filtering + resetRowsFromAllData() { + let allRows = []; + + Object.keys(this.telemetryCollections).forEach((keyString) => { + let { columnMap, limitEvaluator } = this.telemetryObjects[keyString]; + + this.telemetryCollections[keyString].getAll().forEach((datum) => { + allRows.push(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); + }); + }); + + this.tableRows.clearRowsFromTableAndFilter(allRows); + } + + updateFilters(updatedFilters) { + let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); + + if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { + this.filters = deepCopiedFilters; + this.tableRows.clear(); + this.clearAndResubscribe(); + } else { + this.filters = deepCopiedFilters; + } + } + + clearAndResubscribe() { + let objectKeys = Object.keys(this.telemetryObjects); + + this.tableRows.clear(); + objectKeys.forEach((keyString) => { + this.addTelemetryObject(this.telemetryObjects[keyString].telemetryObject); + }); + } + + removeTelemetryObject(objectIdentifier) { + const keyString = this.openmct.objects.makeKeyString(objectIdentifier); + const SKIP_CHECK = true; + + this.configuration.removeColumnsForObject(objectIdentifier, true); + this.tableRows.removeRowsByObject(keyString); + + this.removeTelemetryCollection(keyString); + delete this.telemetryObjects[keyString]; + + this.emit('object-removed', objectIdentifier); + + this.stalenessSubscription[keyString].unsubscribe(); + this.stalenessSubscription[keyString].stalenessUtils.destroy(); + this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK); + delete this.stalenessSubscription[keyString]; + } + + clearData() { + this.tableRows.clear(); + this.emit('refresh'); + } + + addColumnsForObject(telemetryObject) { + let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); + + this.addNameColumn(telemetryObject, metadataValues); + metadataValues.forEach((metadatum) => { + if (metadatum.key === 'name') { + return; + } + + let column = this.createColumn(metadatum); + this.configuration.addSingleColumnForObject(telemetryObject, column); + // add units column if available + if (metadatum.unit !== undefined) { + let unitColumn = this.createUnitColumn(metadatum); + this.configuration.addSingleColumnForObject(telemetryObject, unitColumn); + } + }); + } + + getColumnMapForObject(objectKeyString) { + let columns = this.configuration.getColumns(); + + if (columns[objectKeyString]) { + return columns[objectKeyString].reduce((map, column) => { + map[column.getKey()] = column; + + return map; + }, {}); + } + + return {}; + } + + buildOptionsFromConfiguration(telemetryObject) { + let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + let filters = + this.domainObject.configuration && + this.domainObject.configuration.filters && + this.domainObject.configuration.filters[keyString]; + + return { filters } || {}; + } + + createColumn(metadatum) { + return new TelemetryTableColumn(this.openmct, metadatum); + } + + createUnitColumn(metadatum) { + return new TelemetryTableUnitColumn(this.openmct, metadatum); + } + + isTelemetryObject(domainObject) { + return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry'); + } + + sortBy(sortOptions) { + this.tableRows.sortBy(sortOptions); + + if (this.openmct.editor.isEditing()) { + let configuration = this.configuration.getConfiguration(); + configuration.sortOptions = sortOptions; + this.configuration.updateConfiguration(configuration); + } + } + + runDelayedActions() { + this.delayedActions.forEach((action) => action()); + this.delayedActions = []; + } + + removeTelemetryCollection(keyString) { + if (this.telemetryCollections[keyString]) { + this.telemetryCollections[keyString].destroy(); + this.telemetryCollections[keyString] = undefined; + delete this.telemetryCollections[keyString]; + } + } + + pause() { + this.paused = true; + } + + unpause() { + this.paused = false; + this.runDelayedActions(); + } + + destroy() { + this.tableRows.destroy(); + + this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData); + + let keystrings = Object.keys(this.telemetryCollections); + keystrings.forEach(this.removeTelemetryCollection); + + if (this.filterObserver) { + this.filterObserver(); + } + + Object.values(this.stalenessSubscription).forEach((stalenessSubscription) => { + stalenessSubscription.unsubscribe(); + stalenessSubscription.stalenessUtils.destroy(); + }); + + if (this.tableComposition !== undefined) { + this.tableComposition.off('add', this.addTelemetryObject); + this.tableComposition.off('remove', this.removeTelemetryObject); + } + } + } + + return TelemetryTable; }); diff --git a/src/plugins/telemetryTable/TelemetryTableColumn.js b/src/plugins/telemetryTable/TelemetryTableColumn.js index fce935954d..dc72225e9a 100644 --- a/src/plugins/telemetryTable/TelemetryTableColumn.js +++ b/src/plugins/telemetryTable/TelemetryTableColumn.js @@ -20,47 +20,47 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ define(function () { - class TelemetryTableColumn { - constructor(openmct, metadatum, options = {selectable: false}) { - this.metadatum = metadatum; - this.formatter = openmct.telemetry.getValueFormatter(metadatum); - this.titleValue = this.metadatum.name; - this.selectable = options.selectable; - } - - getKey() { - return this.metadatum.key; - } - - getTitle() { - return this.metadatum.name; - } - - getMetadatum() { - return this.metadatum; - } - - hasValueForDatum(telemetryDatum) { - return Object.prototype.hasOwnProperty.call(telemetryDatum, this.metadatum.source); - } - - getRawValue(telemetryDatum) { - return telemetryDatum[this.metadatum.source]; - } - - getFormattedValue(telemetryDatum) { - let formattedValue = this.formatter.format(telemetryDatum); - if (formattedValue !== undefined && typeof formattedValue !== 'string') { - return formattedValue.toString(); - } else { - return formattedValue; - } - } - - getParsedValue(telemetryDatum) { - return this.formatter.parse(telemetryDatum); - } + class TelemetryTableColumn { + constructor(openmct, metadatum, options = { selectable: false }) { + this.metadatum = metadatum; + this.formatter = openmct.telemetry.getValueFormatter(metadatum); + this.titleValue = this.metadatum.name; + this.selectable = options.selectable; } - return TelemetryTableColumn; + getKey() { + return this.metadatum.key; + } + + getTitle() { + return this.metadatum.name; + } + + getMetadatum() { + return this.metadatum; + } + + hasValueForDatum(telemetryDatum) { + return Object.prototype.hasOwnProperty.call(telemetryDatum, this.metadatum.source); + } + + getRawValue(telemetryDatum) { + return telemetryDatum[this.metadatum.source]; + } + + getFormattedValue(telemetryDatum) { + let formattedValue = this.formatter.format(telemetryDatum); + if (formattedValue !== undefined && typeof formattedValue !== 'string') { + return formattedValue.toString(); + } else { + return formattedValue; + } + } + + getParsedValue(telemetryDatum) { + return this.formatter.parse(telemetryDatum); + } + } + + return TelemetryTableColumn; }); diff --git a/src/plugins/telemetryTable/TelemetryTableConfiguration.js b/src/plugins/telemetryTable/TelemetryTableConfiguration.js index 22e16bfb68..d79af9fec5 100644 --- a/src/plugins/telemetryTable/TelemetryTableConfiguration.js +++ b/src/plugins/telemetryTable/TelemetryTableConfiguration.js @@ -20,147 +20,149 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - 'lodash', - 'EventEmitter' -], function (_, EventEmitter) { +define(['lodash', 'EventEmitter'], function (_, EventEmitter) { + class TelemetryTableConfiguration extends EventEmitter { + constructor(domainObject, openmct) { + super(); - class TelemetryTableConfiguration extends EventEmitter { - constructor(domainObject, openmct) { - super(); + this.domainObject = domainObject; + this.openmct = openmct; + this.columns = {}; - this.domainObject = domainObject; - this.openmct = openmct; - this.columns = {}; + this.removeColumnsForObject = this.removeColumnsForObject.bind(this); + this.objectMutated = this.objectMutated.bind(this); - this.removeColumnsForObject = this.removeColumnsForObject.bind(this); - this.objectMutated = this.objectMutated.bind(this); - - this.unlistenFromMutation = openmct.objects.observe(domainObject, 'configuration', this.objectMutated); - } - - getConfiguration() { - let configuration = this.domainObject.configuration || {}; - configuration.hiddenColumns = configuration.hiddenColumns || {}; - configuration.columnWidths = configuration.columnWidths || {}; - configuration.columnOrder = configuration.columnOrder || []; - configuration.cellFormat = configuration.cellFormat || {}; - configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize; - - return configuration; - } - - updateConfiguration(configuration) { - this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); - } - - /** - * @private - * @param {*} object - */ - objectMutated(configuration) { - if (configuration !== undefined) { - this.emit('change', configuration); - } - } - - addSingleColumnForObject(telemetryObject, column, position) { - let objectKeyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); - this.columns[objectKeyString] = this.columns[objectKeyString] || []; - position = position || this.columns[objectKeyString].length; - this.columns[objectKeyString].splice(position, 0, column); - } - - removeColumnsForObject(objectIdentifier) { - let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier); - let columnsToRemove = this.columns[objectKeyString]; - - delete this.columns[objectKeyString]; - - let configuration = this.domainObject.configuration; - let configurationChanged = false; - columnsToRemove.forEach((column) => { - //There may be more than one column with the same key (eg. time system columns) - if (!this.hasColumnWithKey(column.getKey())) { - delete configuration.hiddenColumns[column.getKey()]; - configurationChanged = true; - } - }); - if (configurationChanged) { - this.updateConfiguration(configuration); - } - } - - hasColumnWithKey(columnKey) { - return _.flatten(Object.values(this.columns)) - .some(column => column.getKey() === columnKey); - } - - getColumns() { - return this.columns; - } - - getAllHeaders() { - let flattenedColumns = _.flatten(Object.values(this.columns)); - /* eslint-disable you-dont-need-lodash-underscore/uniq */ - let headers = _.uniq(flattenedColumns, false, column => column.getKey()) - .reduce(fromColumnsToHeadersMap, {}); - /* eslint-enable you-dont-need-lodash-underscore/uniq */ - function fromColumnsToHeadersMap(headersMap, column) { - headersMap[column.getKey()] = column.getTitle(); - - return headersMap; - } - - return headers; - } - - getVisibleHeaders() { - let allHeaders = this.getAllHeaders(); - let configuration = this.getConfiguration(); - - let orderedColumns = this.getColumnOrder(); - let unorderedColumns = _.difference(Object.keys(allHeaders), orderedColumns); - - return orderedColumns.concat(unorderedColumns) - .filter((headerKey) => { - return configuration.hiddenColumns[headerKey] !== true; - }) - .reduce((headers, headerKey) => { - headers[headerKey] = allHeaders[headerKey]; - - return headers; - }, {}); - } - - getColumnWidths() { - let configuration = this.getConfiguration(); - - return configuration.columnWidths; - } - - setColumnWidths(columnWidths) { - let configuration = this.getConfiguration(); - configuration.columnWidths = columnWidths; - this.updateConfiguration(configuration); - } - - getColumnOrder() { - let configuration = this.getConfiguration(); - - return configuration.columnOrder; - } - - setColumnOrder(columnOrder) { - let configuration = this.getConfiguration(); - configuration.columnOrder = columnOrder; - this.updateConfiguration(configuration); - } - - destroy() { - this.unlistenFromMutation(); - } + this.unlistenFromMutation = openmct.objects.observe( + domainObject, + 'configuration', + this.objectMutated + ); } - return TelemetryTableConfiguration; + getConfiguration() { + let configuration = this.domainObject.configuration || {}; + configuration.hiddenColumns = configuration.hiddenColumns || {}; + configuration.columnWidths = configuration.columnWidths || {}; + configuration.columnOrder = configuration.columnOrder || []; + configuration.cellFormat = configuration.cellFormat || {}; + configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize; + + return configuration; + } + + updateConfiguration(configuration) { + this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); + } + + /** + * @private + * @param {*} object + */ + objectMutated(configuration) { + if (configuration !== undefined) { + this.emit('change', configuration); + } + } + + addSingleColumnForObject(telemetryObject, column, position) { + let objectKeyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + this.columns[objectKeyString] = this.columns[objectKeyString] || []; + position = position || this.columns[objectKeyString].length; + this.columns[objectKeyString].splice(position, 0, column); + } + + removeColumnsForObject(objectIdentifier) { + let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier); + let columnsToRemove = this.columns[objectKeyString]; + + delete this.columns[objectKeyString]; + + let configuration = this.domainObject.configuration; + let configurationChanged = false; + columnsToRemove.forEach((column) => { + //There may be more than one column with the same key (eg. time system columns) + if (!this.hasColumnWithKey(column.getKey())) { + delete configuration.hiddenColumns[column.getKey()]; + configurationChanged = true; + } + }); + if (configurationChanged) { + this.updateConfiguration(configuration); + } + } + + hasColumnWithKey(columnKey) { + return _.flatten(Object.values(this.columns)).some((column) => column.getKey() === columnKey); + } + + getColumns() { + return this.columns; + } + + getAllHeaders() { + let flattenedColumns = _.flatten(Object.values(this.columns)); + /* eslint-disable you-dont-need-lodash-underscore/uniq */ + let headers = _.uniq(flattenedColumns, false, (column) => column.getKey()).reduce( + fromColumnsToHeadersMap, + {} + ); + /* eslint-enable you-dont-need-lodash-underscore/uniq */ + function fromColumnsToHeadersMap(headersMap, column) { + headersMap[column.getKey()] = column.getTitle(); + + return headersMap; + } + + return headers; + } + + getVisibleHeaders() { + let allHeaders = this.getAllHeaders(); + let configuration = this.getConfiguration(); + + let orderedColumns = this.getColumnOrder(); + let unorderedColumns = _.difference(Object.keys(allHeaders), orderedColumns); + + return orderedColumns + .concat(unorderedColumns) + .filter((headerKey) => { + return configuration.hiddenColumns[headerKey] !== true; + }) + .reduce((headers, headerKey) => { + headers[headerKey] = allHeaders[headerKey]; + + return headers; + }, {}); + } + + getColumnWidths() { + let configuration = this.getConfiguration(); + + return configuration.columnWidths; + } + + setColumnWidths(columnWidths) { + let configuration = this.getConfiguration(); + configuration.columnWidths = columnWidths; + this.updateConfiguration(configuration); + } + + getColumnOrder() { + let configuration = this.getConfiguration(); + + return configuration.columnOrder; + } + + setColumnOrder(columnOrder) { + let configuration = this.getConfiguration(); + configuration.columnOrder = columnOrder; + this.updateConfiguration(configuration); + } + + destroy() { + this.unlistenFromMutation(); + } + } + + return TelemetryTableConfiguration; }); diff --git a/src/plugins/telemetryTable/TelemetryTableNameColumn.js b/src/plugins/telemetryTable/TelemetryTableNameColumn.js index 7b400c6da7..0ae1ba352d 100644 --- a/src/plugins/telemetryTable/TelemetryTableNameColumn.js +++ b/src/plugins/telemetryTable/TelemetryTableNameColumn.js @@ -19,26 +19,22 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './TelemetryTableColumn.js' -], function ( - TelemetryTableColumn -) { - class TelemetryTableNameColumn extends TelemetryTableColumn { - constructor(openmct, telemetryObject, metadatum) { - super(openmct, metadatum); +define(['./TelemetryTableColumn.js'], function (TelemetryTableColumn) { + class TelemetryTableNameColumn extends TelemetryTableColumn { + constructor(openmct, telemetryObject, metadatum) { + super(openmct, metadatum); - this.telemetryObject = telemetryObject; - } - - getRawValue() { - return this.telemetryObject.name; - } - - getFormattedValue() { - return this.telemetryObject.name; - } + this.telemetryObject = telemetryObject; } - return TelemetryTableNameColumn; + getRawValue() { + return this.telemetryObject.name; + } + + getFormattedValue() { + return this.telemetryObject.name; + } + } + + return TelemetryTableNameColumn; }); diff --git a/src/plugins/telemetryTable/TelemetryTableRow.js b/src/plugins/telemetryTable/TelemetryTableRow.js index a49edcbbd9..762a72642b 100644 --- a/src/plugins/telemetryTable/TelemetryTableRow.js +++ b/src/plugins/telemetryTable/TelemetryTableRow.js @@ -21,93 +21,91 @@ *****************************************************************************/ define([], function () { - class TelemetryTableRow { - constructor(datum, columns, objectKeyString, limitEvaluator) { - this.columns = columns; + class TelemetryTableRow { + constructor(datum, columns, objectKeyString, limitEvaluator) { + this.columns = columns; - this.datum = createNormalizedDatum(datum, columns); - this.fullDatum = datum; - this.limitEvaluator = limitEvaluator; - this.objectKeyString = objectKeyString; - } - - getFormattedDatum(headers) { - return Object.keys(headers).reduce((formattedDatum, columnKey) => { - formattedDatum[columnKey] = this.getFormattedValue(columnKey); - - return formattedDatum; - }, {}); - } - - getFormattedValue(key) { - let column = this.columns[key]; - - return column && column.getFormattedValue(this.datum[key]); - } - - getParsedValue(key) { - let column = this.columns[key]; - - return column && column.getParsedValue(this.datum[key]); - } - - getCellComponentName(key) { - let column = this.columns[key]; - - return column - && column.getCellComponentName - && column.getCellComponentName(); - } - - getRowClass() { - if (!this.rowClass) { - let limitEvaluation = this.limitEvaluator.evaluate(this.datum); - this.rowClass = limitEvaluation && limitEvaluation.cssClass; - } - - return this.rowClass; - } - - getCellLimitClasses() { - if (!this.cellLimitClasses) { - this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => { - if (!column.isUnit) { - let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum()); - alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass; - } - - return alarmStateMap; - }, {}); - } - - return this.cellLimitClasses; - } - - getContextualDomainObject(openmct, objectKeyString) { - return openmct.objects.get(objectKeyString); - } - - getContextMenuActions() { - return ['viewDatumAction', 'viewHistoricalData']; - } + this.datum = createNormalizedDatum(datum, columns); + this.fullDatum = datum; + this.limitEvaluator = limitEvaluator; + this.objectKeyString = objectKeyString; } - /** - * Normalize the structure of datums to assist sorting and merging of columns. - * Maps all sources to keys. - * @private - * @param {*} telemetryDatum - * @param {*} metadataValues - */ - function createNormalizedDatum(datum, columns) { - const normalizedDatum = JSON.parse(JSON.stringify(datum)); + getFormattedDatum(headers) { + return Object.keys(headers).reduce((formattedDatum, columnKey) => { + formattedDatum[columnKey] = this.getFormattedValue(columnKey); - Object.values(columns).forEach(column => { - normalizedDatum[column.getKey()] = column.getRawValue(datum); - }); - - return normalizedDatum; + return formattedDatum; + }, {}); } - return TelemetryTableRow; + getFormattedValue(key) { + let column = this.columns[key]; + + return column && column.getFormattedValue(this.datum[key]); + } + + getParsedValue(key) { + let column = this.columns[key]; + + return column && column.getParsedValue(this.datum[key]); + } + + getCellComponentName(key) { + let column = this.columns[key]; + + return column && column.getCellComponentName && column.getCellComponentName(); + } + + getRowClass() { + if (!this.rowClass) { + let limitEvaluation = this.limitEvaluator.evaluate(this.datum); + this.rowClass = limitEvaluation && limitEvaluation.cssClass; + } + + return this.rowClass; + } + + getCellLimitClasses() { + if (!this.cellLimitClasses) { + this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => { + if (!column.isUnit) { + let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum()); + alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass; + } + + return alarmStateMap; + }, {}); + } + + return this.cellLimitClasses; + } + + getContextualDomainObject(openmct, objectKeyString) { + return openmct.objects.get(objectKeyString); + } + + getContextMenuActions() { + return ['viewDatumAction', 'viewHistoricalData']; + } + } + + /** + * Normalize the structure of datums to assist sorting and merging of columns. + * Maps all sources to keys. + * @private + * @param {*} telemetryDatum + * @param {*} metadataValues + */ + function createNormalizedDatum(datum, columns) { + const normalizedDatum = JSON.parse(JSON.stringify(datum)); + + Object.values(columns).forEach((column) => { + normalizedDatum[column.getKey()] = column.getRawValue(datum); + }); + + return normalizedDatum; + } + + return TelemetryTableRow; }); diff --git a/src/plugins/telemetryTable/TelemetryTableType.js b/src/plugins/telemetryTable/TelemetryTableType.js index 978746fbfc..0087db2605 100644 --- a/src/plugins/telemetryTable/TelemetryTableType.js +++ b/src/plugins/telemetryTable/TelemetryTableType.js @@ -21,17 +21,18 @@ *****************************************************************************/ define(function () { - return { - name: 'Telemetry Table', - description: 'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.', - creatable: true, - cssClass: 'icon-tabular-scrolling', - initialize(domainObject) { - domainObject.composition = []; - domainObject.configuration = { - columnWidths: {}, - hiddenColumns: {} - }; - } - }; + return { + name: 'Telemetry Table', + description: + 'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.', + creatable: true, + cssClass: 'icon-tabular-scrolling', + initialize(domainObject) { + domainObject.composition = []; + domainObject.configuration = { + columnWidths: {}, + hiddenColumns: {} + }; + } + }; }); diff --git a/src/plugins/telemetryTable/TelemetryTableUnitColumn.js b/src/plugins/telemetryTable/TelemetryTableUnitColumn.js index 4ff49a04f9..2f89b6b9d8 100644 --- a/src/plugins/telemetryTable/TelemetryTableUnitColumn.js +++ b/src/plugins/telemetryTable/TelemetryTableUnitColumn.js @@ -19,42 +19,38 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './TelemetryTableColumn.js' -], function ( - TelemetryTableColumn -) { - class TelemetryTableUnitColumn extends TelemetryTableColumn { - constructor(openmct, metadatum) { - super(openmct, metadatum); - this.isUnit = true; - this.titleValue += ' Unit'; - this.formatter = { - format: (datum) => { - return this.metadatum.unit; - }, - parse: (datum) => { - return this.metadatum.unit; - } - }; - } - - getKey() { - return this.metadatum.key + '-unit'; - } - - getTitle() { - return this.metadatum.name + ' Unit'; - } - - getRawValue(telemetryDatum) { - return this.metadatum.unit; - } - - getFormattedValue(telemetryDatum) { - return this.formatter.format(telemetryDatum); +define(['./TelemetryTableColumn.js'], function (TelemetryTableColumn) { + class TelemetryTableUnitColumn extends TelemetryTableColumn { + constructor(openmct, metadatum) { + super(openmct, metadatum); + this.isUnit = true; + this.titleValue += ' Unit'; + this.formatter = { + format: (datum) => { + return this.metadatum.unit; + }, + parse: (datum) => { + return this.metadatum.unit; } + }; } - return TelemetryTableUnitColumn; + getKey() { + return this.metadatum.key + '-unit'; + } + + getTitle() { + return this.metadatum.name + ' Unit'; + } + + getRawValue(telemetryDatum) { + return this.metadatum.unit; + } + + getFormattedValue(telemetryDatum) { + return this.formatter.format(telemetryDatum); + } + } + + return TelemetryTableUnitColumn; }); diff --git a/src/plugins/telemetryTable/TelemetryTableView.js b/src/plugins/telemetryTable/TelemetryTableView.js index 9f7dccbb1e..74ed12681e 100644 --- a/src/plugins/telemetryTable/TelemetryTableView.js +++ b/src/plugins/telemetryTable/TelemetryTableView.js @@ -3,69 +3,70 @@ import TelemetryTable from './TelemetryTable'; import Vue from 'vue'; export default class TelemetryTableView { - constructor(openmct, domainObject, objectPath) { - this.openmct = openmct; - this.domainObject = domainObject; - this.objectPath = objectPath; - this.component = undefined; + constructor(openmct, domainObject, objectPath) { + this.openmct = openmct; + this.domainObject = domainObject; + this.objectPath = objectPath; + this.component = undefined; - Object.defineProperty(this, 'table', { - value: new TelemetryTable(domainObject, openmct), - enumerable: false, - configurable: false - }); + Object.defineProperty(this, 'table', { + value: new TelemetryTable(domainObject, openmct), + enumerable: false, + configurable: false + }); + } + + getViewContext() { + if (!this.component) { + return {}; } - getViewContext() { - if (!this.component) { - return {}; - } + return this.component.$refs.tableComponent.getViewContext(); + } - return this.component.$refs.tableComponent.getViewContext(); - } + onEditModeChange(editMode) { + this.component.isEditing = editMode; + } - onEditModeChange(editMode) { - this.component.isEditing = editMode; - } + onClearData() { + this.table.clearData(); + } - onClearData() { - this.table.clearData(); - } + getTable() { + return this.table; + } - getTable() { - return this.table; - } + destroy(element) { + this.component.$destroy(); + this.component = undefined; + } - destroy(element) { - this.component.$destroy(); - this.component = undefined; - } - - show(element, editMode) { - this.component = new Vue({ - el: element, - components: { - TableComponent - }, - provide: { - openmct: this.openmct, - objectPath: this.objectPath, - table: this.table, - currentView: this - }, - data() { - return { - isEditing: editMode, - marking: { - disableMultiSelect: false, - enable: true, - rowName: '', - rowNamePlural: '', - useAlternateControlBar: false - } - }; - }, - template: '' - }); - } + show(element, editMode) { + this.component = new Vue({ + el: element, + components: { + TableComponent + }, + provide: { + openmct: this.openmct, + objectPath: this.objectPath, + table: this.table, + currentView: this + }, + data() { + return { + isEditing: editMode, + marking: { + disableMultiSelect: false, + enable: true, + rowName: '', + rowNamePlural: '', + useAlternateControlBar: false + } + }; + }, + template: + '' + }); + } } diff --git a/src/plugins/telemetryTable/TelemetryTableViewProvider.js b/src/plugins/telemetryTable/TelemetryTableViewProvider.js index 58deeb81f0..97fe611d0f 100644 --- a/src/plugins/telemetryTable/TelemetryTableViewProvider.js +++ b/src/plugins/telemetryTable/TelemetryTableViewProvider.js @@ -23,32 +23,31 @@ import TelemetryTableView from './TelemetryTableView'; export default function TelemetryTableViewProvider(openmct) { - function hasTelemetry(domainObject) { - if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { - return false; - } - - let metadata = openmct.telemetry.getMetadata(domainObject); - - return metadata.values().length > 0; + function hasTelemetry(domainObject) { + if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { + return false; } - return { - key: 'table', - name: 'Telemetry Table', - cssClass: 'icon-tabular-scrolling', - canView(domainObject) { - return domainObject.type === 'table' - || hasTelemetry(domainObject); - }, - canEdit(domainObject) { - return domainObject.type === 'table'; - }, - view(domainObject, objectPath) { - return new TelemetryTableView(openmct, domainObject, objectPath); - }, - priority() { - return 1; - } - }; + let metadata = openmct.telemetry.getMetadata(domainObject); + + return metadata.values().length > 0; + } + + return { + key: 'table', + name: 'Telemetry Table', + cssClass: 'icon-tabular-scrolling', + canView(domainObject) { + return domainObject.type === 'table' || hasTelemetry(domainObject); + }, + canEdit(domainObject) { + return domainObject.type === 'table'; + }, + view(domainObject, objectPath) { + return new TelemetryTableView(openmct, domainObject, objectPath); + }, + priority() { + return 1; + } + }; } diff --git a/src/plugins/telemetryTable/ViewActions.js b/src/plugins/telemetryTable/ViewActions.js index ac380513cc..83b75f3767 100644 --- a/src/plugins/telemetryTable/ViewActions.js +++ b/src/plugins/telemetryTable/ViewActions.js @@ -21,106 +21,106 @@ *****************************************************************************/ const exportCSV = { - name: 'Export Table Data', - key: 'export-csv-all', - description: "Export this view's data", - cssClass: 'icon-download labeled', - invoke: (objectPath, view) => { - view.getViewContext().exportAllDataAsCSV(); - }, - group: 'view' + name: 'Export Table Data', + key: 'export-csv-all', + description: "Export this view's data", + cssClass: 'icon-download labeled', + invoke: (objectPath, view) => { + view.getViewContext().exportAllDataAsCSV(); + }, + group: 'view' }; const exportMarkedDataAsCSV = { - name: 'Export Marked Rows', - key: 'export-csv-marked', - description: "Export marked rows as CSV", - cssClass: 'icon-download labeled', - invoke: (objectPath, view) => { - view.getViewContext().exportMarkedDataAsCSV(); - }, - group: 'view' + name: 'Export Marked Rows', + key: 'export-csv-marked', + description: 'Export marked rows as CSV', + cssClass: 'icon-download labeled', + invoke: (objectPath, view) => { + view.getViewContext().exportMarkedDataAsCSV(); + }, + group: 'view' }; const unmarkAllRows = { - name: 'Unmark All Rows', - key: 'unmark-all-rows', - description: 'Unmark all rows', - cssClass: 'icon-x labeled', - invoke: (objectPath, view) => { - view.getViewContext().unmarkAllRows(); - }, - showInStatusBar: true, - group: 'view' + name: 'Unmark All Rows', + key: 'unmark-all-rows', + description: 'Unmark all rows', + cssClass: 'icon-x labeled', + invoke: (objectPath, view) => { + view.getViewContext().unmarkAllRows(); + }, + showInStatusBar: true, + group: 'view' }; const pause = { - name: 'Pause', - key: 'pause-data', - description: 'Pause real-time data flow', - cssClass: 'icon-pause', - invoke: (objectPath, view) => { - view.getViewContext().togglePauseByButton(); - }, - showInStatusBar: true, - group: 'view' + name: 'Pause', + key: 'pause-data', + description: 'Pause real-time data flow', + cssClass: 'icon-pause', + invoke: (objectPath, view) => { + view.getViewContext().togglePauseByButton(); + }, + showInStatusBar: true, + group: 'view' }; const play = { - name: 'Play', - key: 'play-data', - description: 'Continue real-time data flow', - cssClass: 'c-button pause-play is-paused', - invoke: (objectPath, view) => { - view.getViewContext().togglePauseByButton(); - }, - showInStatusBar: true, - group: 'view' + name: 'Play', + key: 'play-data', + description: 'Continue real-time data flow', + cssClass: 'c-button pause-play is-paused', + invoke: (objectPath, view) => { + view.getViewContext().togglePauseByButton(); + }, + showInStatusBar: true, + group: 'view' }; const expandColumns = { - name: 'Expand Columns', - key: 'expand-columns', - description: "Increase column widths to fit currently available data.", - cssClass: 'icon-arrows-right-left labeled', - invoke: (objectPath, view) => { - view.getViewContext().expandColumns(); - }, - showInStatusBar: true, - group: 'view' + name: 'Expand Columns', + key: 'expand-columns', + description: 'Increase column widths to fit currently available data.', + cssClass: 'icon-arrows-right-left labeled', + invoke: (objectPath, view) => { + view.getViewContext().expandColumns(); + }, + showInStatusBar: true, + group: 'view' }; const autosizeColumns = { - name: 'Autosize Columns', - key: 'autosize-columns', - description: "Automatically size columns to fit the table into the available space.", - cssClass: 'icon-expand labeled', - invoke: (objectPath, view) => { - view.getViewContext().autosizeColumns(); - }, - showInStatusBar: true, - group: 'view' + name: 'Autosize Columns', + key: 'autosize-columns', + description: 'Automatically size columns to fit the table into the available space.', + cssClass: 'icon-expand labeled', + invoke: (objectPath, view) => { + view.getViewContext().autosizeColumns(); + }, + showInStatusBar: true, + group: 'view' }; const viewActions = [ - exportCSV, - exportMarkedDataAsCSV, - unmarkAllRows, - pause, - play, - expandColumns, - autosizeColumns + exportCSV, + exportMarkedDataAsCSV, + unmarkAllRows, + pause, + play, + expandColumns, + autosizeColumns ]; -viewActions.forEach(action => { - action.appliesTo = (objectPath, view = {}) => { - const viewContext = view.getViewContext && view.getViewContext(); - if (!viewContext) { - return false; - } +viewActions.forEach((action) => { + action.appliesTo = (objectPath, view = {}) => { + const viewContext = view.getViewContext && view.getViewContext(); + if (!viewContext) { + return false; + } - return viewContext.type === 'telemetry-table'; - }; + return viewContext.type === 'telemetry-table'; + }; }); export default viewActions; diff --git a/src/plugins/telemetryTable/collections/TableRowCollection.js b/src/plugins/telemetryTable/collections/TableRowCollection.js index d893c18e5c..0d7d039289 100644 --- a/src/plugins/telemetryTable/collections/TableRowCollection.js +++ b/src/plugins/telemetryTable/collections/TableRowCollection.js @@ -20,336 +20,328 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define( - [ - 'lodash', - 'EventEmitter' - ], - function ( - _, - EventEmitter - ) { - /** - * @constructor - */ - class TableRowCollection extends EventEmitter { - constructor() { - super(); +define(['lodash', 'EventEmitter'], function (_, EventEmitter) { + /** + * @constructor + */ + class TableRowCollection extends EventEmitter { + constructor() { + super(); - this.rows = []; - this.columnFilters = {}; - this.addRows = this.addRows.bind(this); - this.removeRowsByObject = this.removeRowsByObject.bind(this); - this.removeRowsByData = this.removeRowsByData.bind(this); + this.rows = []; + this.columnFilters = {}; + this.addRows = this.addRows.bind(this); + this.removeRowsByObject = this.removeRowsByObject.bind(this); + this.removeRowsByData = this.removeRowsByData.bind(this); - this.clear = this.clear.bind(this); - } + this.clear = this.clear.bind(this); + } - removeRowsByObject(keyString) { - let removed = []; + removeRowsByObject(keyString) { + let removed = []; - this.rows = this.rows.filter((row) => { - if (row.objectKeyString === keyString) { - removed.push(row); + this.rows = this.rows.filter((row) => { + if (row.objectKeyString === keyString) { + removed.push(row); - return false; - } else { - return true; - } - }); + return false; + } else { + return true; + } + }); - this.emit('remove', removed); - } + this.emit('remove', removed); + } - addRows(rows) { - let rowsToAdd = this.filterRows(rows); + addRows(rows) { + let rowsToAdd = this.filterRows(rows); - this.sortAndMergeRows(rowsToAdd); + this.sortAndMergeRows(rowsToAdd); - // we emit filter no matter what to trigger - // an update of visible rows - if (rowsToAdd.length > 0) { - this.emit('add', rowsToAdd); - } - } + // we emit filter no matter what to trigger + // an update of visible rows + if (rowsToAdd.length > 0) { + this.emit('add', rowsToAdd); + } + } - clearRowsFromTableAndFilter(rows) { + clearRowsFromTableAndFilter(rows) { + let rowsToAdd = this.filterRows(rows); + // Reset of all rows, need to wipe current rows + this.rows = []; - let rowsToAdd = this.filterRows(rows); - // Reset of all rows, need to wipe current rows - this.rows = []; + this.sortAndMergeRows(rowsToAdd); - this.sortAndMergeRows(rowsToAdd); + // We emit filter and update of visible rows + this.emit('filter', rowsToAdd); + } - // We emit filter and update of visible rows - this.emit('filter', rowsToAdd); - } + filterRows(rows) { + if (Object.keys(this.columnFilters).length > 0) { + return rows.filter(this.matchesFilters, this); + } - filterRows(rows) { + return rows; + } - if (Object.keys(this.columnFilters).length > 0) { - return rows.filter(this.matchesFilters, this); - } + sortAndMergeRows(rows) { + const sortedRowsToAdd = this.sortCollection(rows); - return rows; - } + if (this.rows.length === 0) { + this.rows = sortedRowsToAdd; - sortAndMergeRows(rows) { - const sortedRowsToAdd = this.sortCollection(rows); + return; + } - if (this.rows.length === 0) { - this.rows = sortedRowsToAdd; + const firstIncomingRow = sortedRowsToAdd[0]; + const lastIncomingRow = sortedRowsToAdd[sortedRowsToAdd.length - 1]; + const firstExistingRow = this.rows[0]; + const lastExistingRow = this.rows[this.rows.length - 1]; - return; - } + if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow) === lastIncomingRow) { + this.rows = [...sortedRowsToAdd, ...this.rows]; + } else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow) === lastExistingRow) { + this.rows = [...this.rows, ...sortedRowsToAdd]; + } else { + this.mergeSortedRows(sortedRowsToAdd); + } + } - const firstIncomingRow = sortedRowsToAdd[0]; - const lastIncomingRow = sortedRowsToAdd[sortedRowsToAdd.length - 1]; - const firstExistingRow = this.rows[0]; - const lastExistingRow = this.rows[this.rows.length - 1]; + sortCollection(rows) { + const sortedRows = _.orderBy( + rows, + (row) => row.getParsedValue(this.sortOptions.key), + this.sortOptions.direction + ); - if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow) - === lastIncomingRow - ) { - this.rows = [...sortedRowsToAdd, ...this.rows]; - } else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow) - === lastExistingRow - ) { - this.rows = [...this.rows, ...sortedRowsToAdd]; - } else { - this.mergeSortedRows(sortedRowsToAdd); - } - } + return sortedRows; + } - sortCollection(rows) { - const sortedRows = _.orderBy( - rows, - row => row.getParsedValue(this.sortOptions.key), this.sortOptions.direction - ); + mergeSortedRows(rows) { + const mergedRows = []; + let i = 0; + let j = 0; - return sortedRows; - } + while (i < this.rows.length && j < rows.length) { + const existingRow = this.rows[i]; + const incomingRow = rows[j]; - mergeSortedRows(rows) { - const mergedRows = []; - let i = 0; - let j = 0; + if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) { + mergedRows.push(existingRow); + i++; + } else { + mergedRows.push(incomingRow); + j++; + } + } - while (i < this.rows.length && j < rows.length) { - const existingRow = this.rows[i]; - const incomingRow = rows[j]; + // tail of existing rows is all that is left to merge + if (i < this.rows.length) { + for (i; i < this.rows.length; i++) { + mergedRows.push(this.rows[i]); + } + } - if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) { - mergedRows.push(existingRow); - i++; - } else { - mergedRows.push(incomingRow); - j++; - } - } + // tail of incoming rows is all that is left to merge + if (j < rows.length) { + for (j; j < rows.length; j++) { + mergedRows.push(rows[j]); + } + } - // tail of existing rows is all that is left to merge - if (i < this.rows.length) { - for (i; i < this.rows.length; i++) { - mergedRows.push(this.rows[i]); - } - } + this.rows = mergedRows; + } - // tail of incoming rows is all that is left to merge - if (j < rows.length) { - for (j; j < rows.length; j++) { - mergedRows.push(rows[j]); - } - } + firstRowInSortOrder(row1, row2) { + const val1 = this.getValueForSortColumn(row1); + const val2 = this.getValueForSortColumn(row2); - this.rows = mergedRows; - } + if (this.sortOptions.direction === 'asc') { + return val1 <= val2 ? row1 : row2; + } else { + return val1 >= val2 ? row1 : row2; + } + } - firstRowInSortOrder(row1, row2) { - const val1 = this.getValueForSortColumn(row1); - const val2 = this.getValueForSortColumn(row2); + removeRowsByData(data) { + let removed = []; - if (this.sortOptions.direction === 'asc') { - return val1 <= val2 ? row1 : row2; - } else { - return val1 >= val2 ? row1 : row2; - } - } + this.rows = this.rows.filter((row) => { + if (data.includes(row.fullDatum)) { + removed.push(row); - removeRowsByData(data) { - let removed = []; + return false; + } else { + return true; + } + }); - this.rows = this.rows.filter((row) => { - if (data.includes(row.fullDatum)) { - removed.push(row); + this.emit('remove', removed); + } - return false; - } else { - return true; - } - }); + /** + * Sorts the telemetry collection based on the provided sort field + * specifier. Subsequent inserts are sorted to maintain specified sport + * order. + * + * @example + * // First build some mock telemetry for the purpose of an example + * let now = Date.now(); + * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) { + * return { + * // define an object property to demonstrate nested paths + * timestamp: { + * ms: now - value * 1000, + * text: + * }, + * value: value + * } + * }); + * let collection = new TelemetryCollection(); + * + * collection.add(telemetry); + * + * // Sort by telemetry value + * collection.sortBy({ + * key: 'value', direction: 'asc' + * }); + * + * // Sort by ms since epoch + * collection.sort({ + * key: 'timestamp.ms', + * direction: 'asc' + * }); + * + * // Sort by 'text' attribute, descending + * collection.sort("timestamp.text"); + * + * + * @param {object} sortOptions An object specifying a sort key, and direction. + */ + sortBy(sortOptions) { + if (arguments.length > 0) { + this.sortOptions = sortOptions; + this.rows = _.orderBy( + this.rows, + (row) => row.getParsedValue(sortOptions.key), + sortOptions.direction + ); + this.emit('sort'); + } - this.emit('remove', removed); - } + // Return duplicate to avoid direct modification of underlying object + return Object.assign({}, this.sortOptions); + } - /** - * Sorts the telemetry collection based on the provided sort field - * specifier. Subsequent inserts are sorted to maintain specified sport - * order. - * - * @example - * // First build some mock telemetry for the purpose of an example - * let now = Date.now(); - * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) { - * return { - * // define an object property to demonstrate nested paths - * timestamp: { - * ms: now - value * 1000, - * text: - * }, - * value: value - * } - * }); - * let collection = new TelemetryCollection(); - * - * collection.add(telemetry); - * - * // Sort by telemetry value - * collection.sortBy({ - * key: 'value', direction: 'asc' - * }); - * - * // Sort by ms since epoch - * collection.sort({ - * key: 'timestamp.ms', - * direction: 'asc' - * }); - * - * // Sort by 'text' attribute, descending - * collection.sort("timestamp.text"); - * - * - * @param {object} sortOptions An object specifying a sort key, and direction. - */ - sortBy(sortOptions) { - if (arguments.length > 0) { - this.sortOptions = sortOptions; - this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction); - this.emit('sort'); - } + setColumnFilter(columnKey, filter) { + filter = filter.trim().toLowerCase(); + let wasBlank = this.columnFilters[columnKey] === undefined; + let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter); - // Return duplicate to avoid direct modification of underlying object - return Object.assign({}, this.sortOptions); - } + if (filter.length === 0) { + delete this.columnFilters[columnKey]; + } else { + this.columnFilters[columnKey] = filter; + } - setColumnFilter(columnKey, filter) { - filter = filter.trim().toLowerCase(); - let wasBlank = this.columnFilters[columnKey] === undefined; - let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter); + if (isSubset || wasBlank) { + this.rows = this.rows.filter(this.matchesFilters, this); + this.emit('filter'); + } else { + this.emit('resetRowsFromAllData'); + } + } - if (filter.length === 0) { - delete this.columnFilters[columnKey]; - } else { - this.columnFilters[columnKey] = filter; - } + setColumnRegexFilter(columnKey, filter) { + filter = filter.trim(); + this.columnFilters[columnKey] = new RegExp(filter); - if (isSubset || wasBlank) { - this.rows = this.rows.filter(this.matchesFilters, this); - this.emit('filter'); - } else { - this.emit('resetRowsFromAllData'); - } + this.emit('resetRowsFromAllData'); + } - } + getColumnMapForObject(objectKeyString) { + let columns = this.configuration.getColumns(); - setColumnRegexFilter(columnKey, filter) { - filter = filter.trim(); - this.columnFilters[columnKey] = new RegExp(filter); + if (columns[objectKeyString]) { + return columns[objectKeyString].reduce((map, column) => { + map[column.getKey()] = column; - this.emit('resetRowsFromAllData'); - } + return map; + }, {}); + } - getColumnMapForObject(objectKeyString) { - let columns = this.configuration.getColumns(); + return {}; + } - if (columns[objectKeyString]) { - return columns[objectKeyString].reduce((map, column) => { - map[column.getKey()] = column; + // /** + // * @private + // */ + isSubsetOfCurrentFilter(columnKey, filter) { + if (this.columnFilters[columnKey] instanceof RegExp) { + return false; + } - return map; - }, {}); - } + return ( + this.columnFilters[columnKey] && + filter.startsWith(this.columnFilters[columnKey]) && + // startsWith check will otherwise fail when filter cleared + // because anyString.startsWith('') === true + filter !== '' + ); + } - return {}; - } - - // /** - // * @private - // */ - isSubsetOfCurrentFilter(columnKey, filter) { - if (this.columnFilters[columnKey] instanceof RegExp) { - return false; - } - - return this.columnFilters[columnKey] - && filter.startsWith(this.columnFilters[columnKey]) - // startsWith check will otherwise fail when filter cleared - // because anyString.startsWith('') === true - && filter !== ''; - } - - /** - * @private - */ - matchesFilters(row) { - let doesMatchFilters = true; - Object.keys(this.columnFilters).forEach((key) => { - if (!doesMatchFilters || !this.rowHasColumn(row, key)) { - return false; - } - - let formattedValue = row.getFormattedValue(key); - if (formattedValue === undefined) { - return false; - } - - if (this.columnFilters[key] instanceof RegExp) { - doesMatchFilters = this.columnFilters[key].test(formattedValue); - } else { - doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1; - } - }); - - return doesMatchFilters; - } - - rowHasColumn(row, key) { - return Object.prototype.hasOwnProperty.call(row.columns, key); - } - - getRows() { - return this.rows; - } - - getRowsLength() { - return this.rows.length; - } - - getValueForSortColumn(row) { - return row.getParsedValue(this.sortOptions.key); - } - - clear() { - let removedRows = this.rows; - this.rows = []; - - this.emit('remove', removedRows); - } - - destroy() { - this.removeAllListeners(); - } + /** + * @private + */ + matchesFilters(row) { + let doesMatchFilters = true; + Object.keys(this.columnFilters).forEach((key) => { + if (!doesMatchFilters || !this.rowHasColumn(row, key)) { + return false; } - return TableRowCollection; - }); + let formattedValue = row.getFormattedValue(key); + if (formattedValue === undefined) { + return false; + } + + if (this.columnFilters[key] instanceof RegExp) { + doesMatchFilters = this.columnFilters[key].test(formattedValue); + } else { + doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1; + } + }); + + return doesMatchFilters; + } + + rowHasColumn(row, key) { + return Object.prototype.hasOwnProperty.call(row.columns, key); + } + + getRows() { + return this.rows; + } + + getRowsLength() { + return this.rows.length; + } + + getValueForSortColumn(row) { + return row.getParsedValue(this.sortOptions.key); + } + + clear() { + let removedRows = this.rows; + this.rows = []; + + this.emit('remove', removedRows); + } + + destroy() { + this.removeAllListeners(); + } + } + + return TableRowCollection; +}); diff --git a/src/plugins/telemetryTable/components/sizing-row.vue b/src/plugins/telemetryTable/components/sizing-row.vue index 9a725730d5..cf62f904ee 100644 --- a/src/plugins/telemetryTable/components/sizing-row.vue +++ b/src/plugins/telemetryTable/components/sizing-row.vue @@ -20,56 +20,58 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table-cell.vue b/src/plugins/telemetryTable/components/table-cell.vue index 6545aff792..bf2797d9a4 100644 --- a/src/plugins/telemetryTable/components/table-cell.vue +++ b/src/plugins/telemetryTable/components/table-cell.vue @@ -20,60 +20,63 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table-column-header.vue b/src/plugins/telemetryTable/components/table-column-header.vue index af593631ed..085ad06fb3 100644 --- a/src/plugins/telemetryTable/components/table-column-header.vue +++ b/src/plugins/telemetryTable/components/table-column-header.vue @@ -20,148 +20,152 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table-configuration.vue b/src/plugins/telemetryTable/components/table-configuration.vue index becb0c5f00..f8ba969336 100644 --- a/src/plugins/telemetryTable/components/table-configuration.vue +++ b/src/plugins/telemetryTable/components/table-configuration.vue @@ -20,72 +20,55 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table-footer-indicator.scss b/src/plugins/telemetryTable/components/table-footer-indicator.scss index 3419cec3f9..02955af97d 100644 --- a/src/plugins/telemetryTable/components/table-footer-indicator.scss +++ b/src/plugins/telemetryTable/components/table-footer-indicator.scss @@ -1,29 +1,29 @@ .c-table-indicator { + display: flex; + align-items: center; + font-size: 0.9em; + overflow: hidden; + + &__elem { + @include ellipsize(); + flex: 0 1 auto; + padding: 2px; + text-transform: uppercase; + + > * { + //display: contents; + } + } + + &__counts { + //background: rgba(deeppink, 0.1); display: flex; - align-items: center; - font-size: 0.9em; + flex: 1 1 auto; + justify-content: flex-end; overflow: hidden; - &__elem { - @include ellipsize(); - flex: 0 1 auto; - padding: 2px; - text-transform: uppercase; - - > * { - //display: contents; - } - } - - &__counts { - //background: rgba(deeppink, 0.1); - display: flex; - flex: 1 1 auto; - justify-content: flex-end; - overflow: hidden; - - > * { - margin-left: $interiorMargin; - } + > * { + margin-left: $interiorMargin; } + } } diff --git a/src/plugins/telemetryTable/components/table-footer-indicator.vue b/src/plugins/telemetryTable/components/table-footer-indicator.vue index 2517387ea1..59171ab252 100644 --- a/src/plugins/telemetryTable/components/table-footer-indicator.vue +++ b/src/plugins/telemetryTable/components/table-footer-indicator.vue @@ -20,44 +20,36 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table-row.scss b/src/plugins/telemetryTable/components/table-row.scss index f21b297f23..a21397c81f 100644 --- a/src/plugins/telemetryTable/components/table-row.scss +++ b/src/plugins/telemetryTable/components/table-row.scss @@ -1,9 +1,9 @@ .noselect { --webkit-touch-callout: none; /* iOS Safari */ - -webkit-user-select: none; /* Safari */ - -khtml-user-select: none; /* Konqueror HTML */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* Internet Explorer/Edge */ - user-select: none; /* Non-prefixed version, currently + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ } diff --git a/src/plugins/telemetryTable/components/table-row.vue b/src/plugins/telemetryTable/components/table-row.vue index 6c8eca257d..874548f5c9 100644 --- a/src/plugins/telemetryTable/components/table-row.vue +++ b/src/plugins/telemetryTable/components/table-row.vue @@ -20,188 +20,204 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table.scss b/src/plugins/telemetryTable/components/table.scss index 03d54c0f72..1dda7f06b8 100644 --- a/src/plugins/telemetryTable/components/table.scss +++ b/src/plugins/telemetryTable/components/table.scss @@ -1,244 +1,249 @@ .c-telemetry-table__drop-target { - position: absolute; - width: 2px; - background-color: $editUIColor; - box-shadow: rgba($editUIColor, 0.5) 0 0 10px; - z-index: 1; - pointer-events: none; + position: absolute; + width: 2px; + background-color: $editUIColor; + box-shadow: rgba($editUIColor, 0.5) 0 0 10px; + z-index: 1; + pointer-events: none; } .c-telemetry-table { - // Table that displays telemetry in a scrolling body area + // Table that displays telemetry in a scrolling body area - @include fontAndSize(); + @include fontAndSize(); - display: flex; - flex-flow: column nowrap; - justify-content: flex-start; + display: flex; + flex-flow: column nowrap; + justify-content: flex-start; + overflow: hidden; + + th, + td { + display: block; + flex: 1 0 auto; + width: 100px; + vertical-align: middle; // This is crucial to hiding 4px height injected by browser by default + } + + /******************************* WRAPPERS */ + &__headers-w { + // Wraps __headers table + flex: 0 0 auto; overflow: hidden; + background: $colorTabHeaderBg; + } - th, td { - display: block; - flex: 1 0 auto; - width: 100px; - vertical-align: middle; // This is crucial to hiding 4px height injected by browser by default + /******************************* TABLES */ + &__headers, + &__body { + tr { + display: flex; + align-items: stretch; + } + } + + &__headers { + // A table + thead { + display: block; } - /******************************* WRAPPERS */ - &__headers-w { - // Wraps __headers table - flex: 0 0 auto; - overflow: hidden; - background: $colorTabHeaderBg; + &__labels { + // Top row, has labels + .c-telemetry-table__headers__content { + // Holds __label, sort indicator and resize-hitarea + display: flex; + align-items: center; + justify-content: center; + width: 100%; + } } - /******************************* TABLES */ - &__headers, - &__body { - tr { - display: flex; - align-items: stretch; - } + &__filter { + .c-table__search { + padding-top: 0; + padding-bottom: 0; + } + + .--width-less-than-600 & { + display: none !important; + } } + } - &__headers { - // A table - thead { - display: block; - } + &__headers__label { + overflow: hidden; + flex: 0 1 auto; + } - &__labels { - // Top row, has labels - .c-telemetry-table__headers__content { - // Holds __label, sort indicator and resize-hitarea - display: flex; - align-items: center; - justify-content: center; - width: 100%; - } - } + &__resize-hitarea { + // In table-column-header.vue + @include abs(); + display: none; // Set to display: block in .is-editing section below + left: auto; + right: -1 * $tabularTdPadLR; + width: $tableResizeColHitareaD; + cursor: col-resize; + transform: translateX(50%); // Move so this element sits over border between columns + } - &__filter { - .c-table__search { - padding-top: 0; - padding-bottom: 0; - } + /******************************* ELEMENTS */ + &__scroll-forcer { + // Force horz scroll when needed; width set via JS + font-size: 0; + height: 1px; // Height 0 won't force scroll properly + position: relative; + } - .--width-less-than-600 & { - display: none !important; - } - } - } + &__progress-bar { + margin-bottom: 3px; + } - &__headers__label { - overflow: hidden; - flex: 0 1 auto; - } + /******************************* WRAPPERS */ + &__body-w { + // Wraps __body table provides scrolling + flex: 1 1 100%; + height: 0; // Fixes Chrome 73 overflow bug + overflow-x: auto; + overflow-y: scroll; + } - &__resize-hitarea { - // In table-column-header.vue - @include abs(); - display: none; // Set to display: block in .is-editing section below - left: auto; right: -1 * $tabularTdPadLR; - width: $tableResizeColHitareaD; - cursor: col-resize; - transform: translateX(50%); // Move so this element sits over border between columns - } + /******************************* TABLES */ + &__body { + // A table + flex: 1 1 100%; + overflow-x: auto; - /******************************* ELEMENTS */ - &__scroll-forcer { - // Force horz scroll when needed; width set via JS - font-size: 0; - height: 1px; // Height 0 won't force scroll properly - position: relative; - } + tr { + display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define + align-items: stretch; + position: absolute; + min-height: 18px; // Needed when a row has empty values in its cells - &__progress-bar { - margin-bottom: 3px; - } - - /******************************* WRAPPERS */ - &__body-w { - // Wraps __body table provides scrolling - flex: 1 1 100%; - height: 0; // Fixes Chrome 73 overflow bug - overflow-x: auto; - overflow-y: scroll; - } - - /******************************* TABLES */ - &__body { - // A table - flex: 1 1 100%; - overflow-x: auto; - - tr { - display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define - align-items: stretch; - position: absolute; - min-height: 18px; // Needed when a row has empty values in its cells - - .is-editing .l-layout__frame & { - pointer-events: none; - } - - &.is-selected { - background-color: $colorSelectedBg !important; - color: $colorSelectedFg !important; - td { - background: none !important; - color: inherit !important; - } - } - } + .is-editing .l-layout__frame & { + pointer-events: none; + } + &.is-selected { + background-color: $colorSelectedBg !important; + color: $colorSelectedFg !important; td { - overflow: hidden; - text-overflow: ellipsis; + background: none !important; + color: inherit !important; } + } } - &__sizing { - // A table - display: table; - z-index: -1; + td { + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__sizing { + // A table + display: table; + z-index: -1; + visibility: hidden; + pointer-events: none; + position: absolute; + + //Add some padding to allow for decorations such as limits indicator + tr { + display: table-row; + } + + th, + td { + display: table-cell; + padding-right: 10px; + padding-left: 10px; + white-space: nowrap; + } + } + + &__sizing-tr { + // A row element used to determine sizing of rows based on font size + visibility: hidden; + pointer-events: none; + } + + &__footer { + $pt: 2px; + border-top: 1px solid $colorInteriorBorder; + margin-top: $interiorMargin; + padding: $pt 0; + overflow: hidden; + transition: all 250ms; + + &:not(.is-filtering) { + .c-frame & { + height: 0; + padding: 0; visibility: hidden; - pointer-events: none; - position: absolute; - - //Add some padding to allow for decorations such as limits indicator - tr { - display: table-row; - } - - th, td { - display: table-cell; - padding-right: 10px; - padding-left: 10px; - white-space: nowrap; - } + } } + } - &__sizing-tr { - // A row element used to determine sizing of rows based on font size - visibility: hidden; - pointer-events: none; - } - - &__footer { - $pt: 2px; - border-top: 1px solid $colorInteriorBorder; - margin-top: $interiorMargin; - padding: $pt 0; - overflow: hidden; - transition: all 250ms; - - &:not(.is-filtering) { - .c-frame & { - height: 0; - padding: 0; - visibility: hidden; - } - } - } - - .c-frame & { - // target .c-frame .c-telemetry-table {} - $pt: 2px; - &:hover { - .c-telemetry-table__footer:not(.is-filtering) { - height: $pt + 16px; - padding: initial; - visibility: visible; - } - } + .c-frame & { + // target .c-frame .c-telemetry-table {} + $pt: 2px; + &:hover { + .c-telemetry-table__footer:not(.is-filtering) { + height: $pt + 16px; + padding: initial; + visibility: visible; + } } + } } // All tables td { - @include isLimit(); + @include isLimit(); } /******************************* SPECIFIC CASE WRAPPERS */ .is-editing { - .c-telemetry-table__headers__labels { - th[draggable], - th[draggable] > * { - cursor: move; - } - - th[draggable]:hover { - $b: $editFrameHovMovebarColorBg; - background: $b; - > * { background: $b; } - } + .c-telemetry-table__headers__labels { + th[draggable], + th[draggable] > * { + cursor: move; } - .c-telemetry-table__resize-hitarea { - display: block; + th[draggable]:hover { + $b: $editFrameHovMovebarColorBg; + background: $b; + > * { + background: $b; + } } + } + + .c-telemetry-table__resize-hitarea { + display: block; + } } .is-paused { - .c-table__body-w { - border: 1px solid rgba($colorPausedBg, 0.8); - } + .c-table__body-w { + border: 1px solid rgba($colorPausedBg, 0.8); + } } /******************************* LEGACY */ .s-status-taking-snapshot, .overlay.snapshot { - .c-table { - &__body-w { - overflow: auto; // Handle overflow-y issues with tables and html2canvas - } - - &-control-bar { - display: none; - + * { - margin-top: 0 !important; - } - } + .c-table { + &__body-w { + overflow: auto; // Handle overflow-y issues with tables and html2canvas } + + &-control-bar { + display: none; + + * { + margin-top: 0 !important; + } + } + } } diff --git a/src/plugins/telemetryTable/components/table.vue b/src/plugins/telemetryTable/components/table.vue index e726e5ad78..f6eebcbbac 100644 --- a/src/plugins/telemetryTable/components/table.vue +++ b/src/plugins/telemetryTable/components/table.vue @@ -20,269 +20,251 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/plugin.js b/src/plugins/telemetryTable/plugin.js index 60fe41d859..8f9be40503 100644 --- a/src/plugins/telemetryTable/plugin.js +++ b/src/plugins/telemetryTable/plugin.js @@ -25,20 +25,20 @@ import TelemetryTableType from './TelemetryTableType'; import TelemetryTableViewActions from './ViewActions'; export default function plugin() { - return function install(openmct) { - openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct)); - openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct)); - openmct.types.addType('table', TelemetryTableType); - openmct.composition.addPolicy((parent, child) => { - if (parent.type === 'table') { - return Object.prototype.hasOwnProperty.call(child, 'telemetry'); - } else { - return true; - } - }); + return function install(openmct) { + openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct)); + openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct)); + openmct.types.addType('table', TelemetryTableType); + openmct.composition.addPolicy((parent, child) => { + if (parent.type === 'table') { + return Object.prototype.hasOwnProperty.call(child, 'telemetry'); + } else { + return true; + } + }); - TelemetryTableViewActions.forEach(action => { - openmct.actions.register(action); - }); - }; + TelemetryTableViewActions.forEach((action) => { + openmct.actions.register(action); + }); + }; } diff --git a/src/plugins/telemetryTable/pluginSpec.js b/src/plugins/telemetryTable/pluginSpec.js index adda5f10a9..9f1eeaf7dd 100644 --- a/src/plugins/telemetryTable/pluginSpec.js +++ b/src/plugins/telemetryTable/pluginSpec.js @@ -22,432 +22,460 @@ import TablePlugin from './plugin.js'; import Vue from 'vue'; import { - createOpenMct, - createMouseEvent, - spyOnBuiltins, - resetApplicationState + createOpenMct, + createMouseEvent, + spyOnBuiltins, + resetApplicationState } from 'utils/testing'; class MockDataTransfer { - constructor() { - this.data = {}; - } - get types() { - return Object.keys(this.data); - } - setData(format, data) { - this.data[format] = data; - } - getData(format) { - return this.data[format]; - } + constructor() { + this.data = {}; + } + get types() { + return Object.keys(this.data); + } + setData(format, data) { + this.data[format] = data; + } + getData(format) { + return this.data[format]; + } } -describe("the plugin", () => { - let openmct; - let tablePlugin; - let element; - let child; - let historicalProvider; - let originalRouterPath; - let unlistenConfigMutation; +describe('the plugin', () => { + let openmct; + let tablePlugin; + let element; + let child; + let historicalProvider; + let originalRouterPath; + let unlistenConfigMutation; - beforeEach((done) => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - // Table Plugin is actually installed by default, but because installing it - // again is harmless it is left here as an examplar for non-default plugins. - tablePlugin = new TablePlugin(); - openmct.install(tablePlugin); + // Table Plugin is actually installed by default, but because installing it + // again is harmless it is left here as an examplar for non-default plugins. + tablePlugin = new TablePlugin(); + openmct.install(tablePlugin); - historicalProvider = { - request: () => { - return Promise.resolve([]); + historicalProvider = { + request: () => { + return Promise.resolve([]); + } + }; + spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider); + + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); + + openmct.time.timeSystem('utc', { + start: 0, + end: 4 + }); + + openmct.types.addType('test-object', { + creatable: true + }); + + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((callBack) => { + callBack(); + }); + + originalRouterPath = openmct.router.path; + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + openmct.time.timeSystem('utc', { + start: 0, + end: 1 + }); + + if (unlistenConfigMutation) { + unlistenConfigMutation(); + } + + return resetApplicationState(openmct); + }); + + describe('defines a table object', function () { + it('that is creatable', () => { + let tableType = openmct.types.get('table'); + expect(tableType.definition.creatable).toBe(true); + }); + }); + + it('provides a table view for objects with telemetry', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'test-object', + telemetry: { + values: [ + { + key: 'some-key' + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, []); + let tableView = applicableViews.find((viewProvider) => viewProvider.key === 'table'); + expect(tableView).toBeDefined(); + }); + + describe('The table view', () => { + let testTelemetryObject; + let applicableViews; + let tableViewProvider; + let tableView; + let tableInstance; + let mockClock; + + beforeEach(async () => { + openmct.time.timeSystem('utc', { + start: 0, + end: 4 + }); + + mockClock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']); + mockClock.key = 'mockClock'; + mockClock.currentValue.and.returnValue(1); + + openmct.time.addClock(mockClock); + openmct.time.clock('mockClock', { + start: 0, + end: 4 + }); + + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key', + name: 'Some attribute', + hints: { + range: 1 + } + }, + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 2 + } } - }; - spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider); + ] + }, + configuration: { + hiddenColumns: { + name: false, + utc: false, + 'some-key': false, + 'some-other-key': false + } + } + }; + const testTelemetry = [ + { + utc: 1, + 'some-key': 'some-value 1', + 'some-other-key': 'some-other-value 1' + }, + { + utc: 2, + 'some-key': 'some-value 2', + 'some-other-key': 'some-other-value 2' + }, + { + utc: 3, + 'some-key': 'some-value 3', + 'some-other-key': 'some-other-value 3' + } + ]; - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + historicalProvider.request = () => Promise.resolve(testTelemetry); - openmct.time.timeSystem('utc', { - start: 0, - end: 4 - }); + openmct.router.path = [testTelemetryObject]; - openmct.types.addType('test-object', { - creatable: true - }); + applicableViews = openmct.objectViews.get(testTelemetryObject, []); + tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table'); + tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); + tableView.show(child, true); - spyOnBuiltins(['requestAnimationFrame']); - window.requestAnimationFrame.and.callFake((callBack) => { - callBack(); - }); + tableInstance = tableView.getTable(); - originalRouterPath = openmct.router.path; - - openmct.on('start', done); - openmct.startHeadless(); + await Vue.nextTick(); }); afterEach(() => { - openmct.time.timeSystem('utc', { - start: 0, - end: 1 - }); - - if (unlistenConfigMutation) { - unlistenConfigMutation(); - } - - return resetApplicationState(openmct); + openmct.router.path = originalRouterPath; }); - describe("defines a table object", function () { - it("that is creatable", () => { - let tableType = openmct.types.get('table'); - expect(tableType.definition.creatable).toBe(true); - }); + it('Shows no progress bar initially', () => { + let progressBar = element.querySelector('.c-progress-bar'); + + expect(tableInstance.outstandingRequests).toBe(0); + expect(progressBar).toBeNull(); }); - it("provides a table view for objects with telemetry", () => { - const testTelemetryObject = { - id: "test-object", - type: "test-object", - telemetry: { - values: [{ - key: "some-key" - }] - } - }; + it('Shows a progress bar while making requests', async () => { + tableInstance.incrementOutstandingRequests(); + await Vue.nextTick(); - const applicableViews = openmct.objectViews.get(testTelemetryObject, []); - let tableView = applicableViews.find((viewProvider) => viewProvider.key === 'table'); - expect(tableView).toBeDefined(); + let progressBar = element.querySelector('.c-progress-bar'); + + expect(tableInstance.outstandingRequests).toBe(1); + expect(progressBar).not.toBeNull(); }); - describe("The table view", () => { - let testTelemetryObject; - let applicableViews; - let tableViewProvider; - let tableView; - let tableInstance; - let mockClock; - - beforeEach(async () => { - openmct.time.timeSystem('utc', { - start: 0, - end: 4 - }); - - mockClock = jasmine.createSpyObj("clock", [ - "on", - "off", - "currentValue" - ]); - mockClock.key = 'mockClock'; - mockClock.currentValue.and.returnValue(1); - - openmct.time.addClock(mockClock); - openmct.time.clock('mockClock', { - start: 0, - end: 4 - }); - - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key", - name: "Some attribute", - hints: { - range: 1 - } - }, { - key: "some-other-key", - name: "Another attribute", - hints: { - range: 2 - } - }] - }, - configuration: { - hiddenColumns: { - name: false, - utc: false, - 'some-key': false, - 'some-other-key': false - } - } - }; - const testTelemetry = [ - { - 'utc': 1, - 'some-key': 'some-value 1', - 'some-other-key': 'some-other-value 1' - }, - { - 'utc': 2, - 'some-key': 'some-value 2', - 'some-other-key': 'some-other-value 2' - }, - { - 'utc': 3, - 'some-key': 'some-value 3', - 'some-other-key': 'some-other-value 3' - } - ]; - - historicalProvider.request = () => Promise.resolve(testTelemetry); - - openmct.router.path = [testTelemetryObject]; - - applicableViews = openmct.objectViews.get(testTelemetryObject, []); - tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table'); - tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); - tableView.show(child, true); - - tableInstance = tableView.getTable(); - - await Vue.nextTick(); - }); - - afterEach(() => { - openmct.router.path = originalRouterPath; - }); - - it("Shows no progress bar initially", () => { - let progressBar = element.querySelector('.c-progress-bar'); - - expect(tableInstance.outstandingRequests).toBe(0); - expect(progressBar).toBeNull(); - }); - - it("Shows a progress bar while making requests", async () => { - tableInstance.incrementOutstandingRequests(); - await Vue.nextTick(); - - let progressBar = element.querySelector('.c-progress-bar'); - - expect(tableInstance.outstandingRequests).toBe(1); - expect(progressBar).not.toBeNull(); - - }); - - it("Renders a row for every telemetry datum returned", async () => { - let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); - await Vue.nextTick(); - expect(rows.length).toBe(3); - }); - - it("Renders a column for every item in telemetry metadata", () => { - let headers = element.querySelectorAll('span.c-telemetry-table__headers__label'); - expect(headers.length).toBe(4); - expect(headers[0].innerText).toBe('Name'); - expect(headers[1].innerText).toBe('Time'); - expect(headers[2].innerText).toBe('Some attribute'); - expect(headers[3].innerText).toBe('Another attribute'); - }); - - it("Supports column reordering via drag and drop", async () => { - let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th'); - let fromColumn = columns[0]; - let toColumn = columns[1]; - let fromColumnText = fromColumn.querySelector('span.c-telemetry-table__headers__label').innerText; - let toColumnText = toColumn.querySelector('span.c-telemetry-table__headers__label').innerText; - - let dragStartEvent = createMouseEvent('dragstart'); - let dragOverEvent = createMouseEvent('dragover'); - let dropEvent = createMouseEvent('drop'); - - dragStartEvent.dataTransfer = - dragOverEvent.dataTransfer = - dropEvent.dataTransfer = new MockDataTransfer(); - - fromColumn.dispatchEvent(dragStartEvent); - toColumn.dispatchEvent(dragOverEvent); - toColumn.dispatchEvent(dropEvent); - - await Vue.nextTick(); - columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th'); - let firstColumn = columns[0]; - let secondColumn = columns[1]; - let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText; - let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText; - expect(fromColumnText).not.toEqual(firstColumnText); - expect(fromColumnText).toEqual(secondColumnText); - expect(toColumnText).not.toEqual(secondColumnText); - expect(toColumnText).toEqual(firstColumnText); - }); - - it("Supports filtering telemetry by regular text search", async () => { - tableInstance.tableRows.setColumnFilter("some-key", "1"); - await Vue.nextTick(); - let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); - - expect(filteredRowElements.length).toEqual(1); - tableInstance.tableRows.setColumnFilter("some-key", ""); - await Vue.nextTick(); - - let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); - expect(allRowElements.length).toEqual(3); - }); - - it("Supports filtering using Regex", async () => { - tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$"); - await Vue.nextTick(); - let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); - - expect(filteredRowElements.length).toEqual(0); - - tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value"); - await Vue.nextTick(); - let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); - - expect(allRowElements.length).toEqual(3); - }); - - it("displays the correct number of column headers when the configuration is mutated", async () => { - const tableInstanceConfiguration = tableInstance.domainObject.configuration; - tableInstanceConfiguration.hiddenColumns['some-key'] = true; - unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); - - await Vue.nextTick(); - let tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label'); - expect(tableHeaderElements.length).toEqual(3); - - tableInstanceConfiguration.hiddenColumns['some-key'] = false; - unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); - - await Vue.nextTick(); - tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label'); - expect(tableHeaderElements.length).toEqual(4); - }); - - it("displays the correct number of table cells in a row when the configuration is mutated", async () => { - const tableInstanceConfiguration = tableInstance.domainObject.configuration; - tableInstanceConfiguration.hiddenColumns['some-key'] = true; - unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); - - await Vue.nextTick(); - let tableRowCells = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr:first-child td'); - expect(tableRowCells.length).toEqual(3); - - tableInstanceConfiguration.hiddenColumns['some-key'] = false; - unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); - - await Vue.nextTick(); - tableRowCells = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr:first-child td'); - expect(tableRowCells.length).toEqual(4); - }); - - it("Pauses the table when a row is marked", async () => { - let firstRow = element.querySelector('table.c-telemetry-table__body > tbody > tr'); - let clickEvent = createMouseEvent('click'); - - // Mark a row - firstRow.dispatchEvent(clickEvent); - - await Vue.nextTick(); - - // Verify table is paused - expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); - }); - - it("Unpauses the table on user bounds change", async () => { - let firstRow = element.querySelector('table.c-telemetry-table__body > tbody > tr'); - let clickEvent = createMouseEvent('click'); - - // Mark a row - firstRow.dispatchEvent(clickEvent); - - await Vue.nextTick(); - - // Verify table is paused - expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); - - const currentBounds = openmct.time.bounds(); - await Vue.nextTick(); - const newBounds = { - start: currentBounds.start, - end: currentBounds.end - 3 - }; - - // Manually change the time bounds - openmct.time.bounds(newBounds); - await Vue.nextTick(); - - // Verify table is no longer paused - expect(element.querySelector('div.c-table.is-paused')).toBeNull(); - }); - - it("Unpauses the table on user bounds change if paused by button", async () => { - const viewContext = tableView.getViewContext(); - - // Pause by button - viewContext.togglePauseByButton(); - await Vue.nextTick(); - - // Verify table is paused - expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); - - const currentBounds = openmct.time.bounds(); - await Vue.nextTick(); - - const newBounds = { - start: currentBounds.start, - end: currentBounds.end - 1 - }; - // Manually change the time bounds - openmct.time.bounds(newBounds); - - await Vue.nextTick(); - - // Verify table is no longer paused - expect(element.querySelector('div.c-table.is-paused')).toBeNull(); - }); - - it("Does not unpause the table on tick", async () => { - const viewContext = tableView.getViewContext(); - - // Pause by button - viewContext.togglePauseByButton(); - - await Vue.nextTick(); - - // Verify table displays the correct number of rows - let tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr'); - expect(tableRows.length).toEqual(3); - - // Verify table is paused - expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); - - // Tick the clock - openmct.time.tick(1); - - await Vue.nextTick(); - - // Verify table is still paused - expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); - - await Vue.nextTick(); - - // Verify table displays the correct number of rows - tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr'); - expect(tableRows.length).toEqual(3); - }); + it('Renders a row for every telemetry datum returned', async () => { + let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); + await Vue.nextTick(); + expect(rows.length).toBe(3); }); + + it('Renders a column for every item in telemetry metadata', () => { + let headers = element.querySelectorAll('span.c-telemetry-table__headers__label'); + expect(headers.length).toBe(4); + expect(headers[0].innerText).toBe('Name'); + expect(headers[1].innerText).toBe('Time'); + expect(headers[2].innerText).toBe('Some attribute'); + expect(headers[3].innerText).toBe('Another attribute'); + }); + + it('Supports column reordering via drag and drop', async () => { + let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th'); + let fromColumn = columns[0]; + let toColumn = columns[1]; + let fromColumnText = fromColumn.querySelector( + 'span.c-telemetry-table__headers__label' + ).innerText; + let toColumnText = toColumn.querySelector('span.c-telemetry-table__headers__label').innerText; + + let dragStartEvent = createMouseEvent('dragstart'); + let dragOverEvent = createMouseEvent('dragover'); + let dropEvent = createMouseEvent('drop'); + + dragStartEvent.dataTransfer = + dragOverEvent.dataTransfer = + dropEvent.dataTransfer = + new MockDataTransfer(); + + fromColumn.dispatchEvent(dragStartEvent); + toColumn.dispatchEvent(dragOverEvent); + toColumn.dispatchEvent(dropEvent); + + await Vue.nextTick(); + columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th'); + let firstColumn = columns[0]; + let secondColumn = columns[1]; + let firstColumnText = firstColumn.querySelector( + 'span.c-telemetry-table__headers__label' + ).innerText; + let secondColumnText = secondColumn.querySelector( + 'span.c-telemetry-table__headers__label' + ).innerText; + expect(fromColumnText).not.toEqual(firstColumnText); + expect(fromColumnText).toEqual(secondColumnText); + expect(toColumnText).not.toEqual(secondColumnText); + expect(toColumnText).toEqual(firstColumnText); + }); + + it('Supports filtering telemetry by regular text search', async () => { + tableInstance.tableRows.setColumnFilter('some-key', '1'); + await Vue.nextTick(); + let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + + expect(filteredRowElements.length).toEqual(1); + tableInstance.tableRows.setColumnFilter('some-key', ''); + await Vue.nextTick(); + + let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + expect(allRowElements.length).toEqual(3); + }); + + it('Supports filtering using Regex', async () => { + tableInstance.tableRows.setColumnRegexFilter('some-key', '^some-value$'); + await Vue.nextTick(); + let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + + expect(filteredRowElements.length).toEqual(0); + + tableInstance.tableRows.setColumnRegexFilter('some-key', '^some-value'); + await Vue.nextTick(); + let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + + expect(allRowElements.length).toEqual(3); + }); + + it('displays the correct number of column headers when the configuration is mutated', async () => { + const tableInstanceConfiguration = tableInstance.domainObject.configuration; + tableInstanceConfiguration.hiddenColumns['some-key'] = true; + unlistenConfigMutation = tableInstance.openmct.objects.mutate( + tableInstance.domainObject, + 'configuration', + tableInstanceConfiguration + ); + + await Vue.nextTick(); + let tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label'); + expect(tableHeaderElements.length).toEqual(3); + + tableInstanceConfiguration.hiddenColumns['some-key'] = false; + unlistenConfigMutation = tableInstance.openmct.objects.mutate( + tableInstance.domainObject, + 'configuration', + tableInstanceConfiguration + ); + + await Vue.nextTick(); + tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label'); + expect(tableHeaderElements.length).toEqual(4); + }); + + it('displays the correct number of table cells in a row when the configuration is mutated', async () => { + const tableInstanceConfiguration = tableInstance.domainObject.configuration; + tableInstanceConfiguration.hiddenColumns['some-key'] = true; + unlistenConfigMutation = tableInstance.openmct.objects.mutate( + tableInstance.domainObject, + 'configuration', + tableInstanceConfiguration + ); + + await Vue.nextTick(); + let tableRowCells = element.querySelectorAll( + 'table.c-telemetry-table__body > tbody > tr:first-child td' + ); + expect(tableRowCells.length).toEqual(3); + + tableInstanceConfiguration.hiddenColumns['some-key'] = false; + unlistenConfigMutation = tableInstance.openmct.objects.mutate( + tableInstance.domainObject, + 'configuration', + tableInstanceConfiguration + ); + + await Vue.nextTick(); + tableRowCells = element.querySelectorAll( + 'table.c-telemetry-table__body > tbody > tr:first-child td' + ); + expect(tableRowCells.length).toEqual(4); + }); + + it('Pauses the table when a row is marked', async () => { + let firstRow = element.querySelector('table.c-telemetry-table__body > tbody > tr'); + let clickEvent = createMouseEvent('click'); + + // Mark a row + firstRow.dispatchEvent(clickEvent); + + await Vue.nextTick(); + + // Verify table is paused + expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); + }); + + it('Unpauses the table on user bounds change', async () => { + let firstRow = element.querySelector('table.c-telemetry-table__body > tbody > tr'); + let clickEvent = createMouseEvent('click'); + + // Mark a row + firstRow.dispatchEvent(clickEvent); + + await Vue.nextTick(); + + // Verify table is paused + expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); + + const currentBounds = openmct.time.bounds(); + await Vue.nextTick(); + const newBounds = { + start: currentBounds.start, + end: currentBounds.end - 3 + }; + + // Manually change the time bounds + openmct.time.bounds(newBounds); + await Vue.nextTick(); + + // Verify table is no longer paused + expect(element.querySelector('div.c-table.is-paused')).toBeNull(); + }); + + it('Unpauses the table on user bounds change if paused by button', async () => { + const viewContext = tableView.getViewContext(); + + // Pause by button + viewContext.togglePauseByButton(); + await Vue.nextTick(); + + // Verify table is paused + expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); + + const currentBounds = openmct.time.bounds(); + await Vue.nextTick(); + + const newBounds = { + start: currentBounds.start, + end: currentBounds.end - 1 + }; + // Manually change the time bounds + openmct.time.bounds(newBounds); + + await Vue.nextTick(); + + // Verify table is no longer paused + expect(element.querySelector('div.c-table.is-paused')).toBeNull(); + }); + + it('Does not unpause the table on tick', async () => { + const viewContext = tableView.getViewContext(); + + // Pause by button + viewContext.togglePauseByButton(); + + await Vue.nextTick(); + + // Verify table displays the correct number of rows + let tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr'); + expect(tableRows.length).toEqual(3); + + // Verify table is paused + expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); + + // Tick the clock + openmct.time.tick(1); + + await Vue.nextTick(); + + // Verify table is still paused + expect(element.querySelector('div.c-table.is-paused')).not.toBeNull(); + + await Vue.nextTick(); + + // Verify table displays the correct number of rows + tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr'); + expect(tableRows.length).toEqual(3); + }); + }); }); diff --git a/src/plugins/themes/espresso-theme.scss b/src/plugins/themes/espresso-theme.scss index 58d039a8ec..05b5a0de1a 100644 --- a/src/plugins/themes/espresso-theme.scss +++ b/src/plugins/themes/espresso-theme.scss @@ -1,22 +1,22 @@ -@import "../../styles/vendor/normalize-min"; -@import "../../styles/constants"; -@import "../../styles/constants-mobile.scss"; +@import '../../styles/vendor/normalize-min'; +@import '../../styles/constants'; +@import '../../styles/constants-mobile.scss'; -@import "../../styles/constants-espresso"; +@import '../../styles/constants-espresso'; -@import "../../styles/mixins"; -@import "../../styles/animations"; -@import "../../styles/about"; -@import "../../styles/glyphs"; -@import "../../styles/global"; -@import "../../styles/status"; -@import "../../styles/limits"; -@import "../../styles/controls"; -@import "../../styles/forms"; -@import "../../styles/table"; -@import "../../styles/legacy"; -@import "../../styles/legacy-plots"; -@import "../../styles/plotly"; -@import "../../styles/legacy-messages"; +@import '../../styles/mixins'; +@import '../../styles/animations'; +@import '../../styles/about'; +@import '../../styles/glyphs'; +@import '../../styles/global'; +@import '../../styles/status'; +@import '../../styles/limits'; +@import '../../styles/controls'; +@import '../../styles/forms'; +@import '../../styles/table'; +@import '../../styles/legacy'; +@import '../../styles/legacy-plots'; +@import '../../styles/plotly'; +@import '../../styles/legacy-messages'; -@import "../../styles/vue-styles.scss"; +@import '../../styles/vue-styles.scss'; diff --git a/src/plugins/themes/espresso.js b/src/plugins/themes/espresso.js index d403c67696..99127db482 100644 --- a/src/plugins/themes/espresso.js +++ b/src/plugins/themes/espresso.js @@ -1,7 +1,7 @@ import { installTheme } from './installTheme'; export default function plugin() { - return function install(openmct) { - installTheme(openmct, 'espresso'); - }; + return function install(openmct) { + installTheme(openmct, 'espresso'); + }; } diff --git a/src/plugins/themes/installTheme.js b/src/plugins/themes/installTheme.js index ffdf0924ba..9987b09b3d 100644 --- a/src/plugins/themes/installTheme.js +++ b/src/plugins/themes/installTheme.js @@ -1,18 +1,18 @@ const dataAttribute = 'theme'; export function installTheme(openmct, themeName) { - const currentTheme = document.querySelector(`link[data-${dataAttribute}]`); - if (currentTheme) { - currentTheme.remove(); - } + const currentTheme = document.querySelector(`link[data-${dataAttribute}]`); + if (currentTheme) { + currentTheme.remove(); + } - const newTheme = document.createElement('link'); - newTheme.setAttribute('rel', 'stylesheet'); + const newTheme = document.createElement('link'); + newTheme.setAttribute('rel', 'stylesheet'); - // eslint-disable-next-line no-undef - const href = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}${themeName}Theme.css`; - newTheme.setAttribute('href', href); - newTheme.dataset[dataAttribute] = themeName; + // eslint-disable-next-line no-undef + const href = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}${themeName}Theme.css`; + newTheme.setAttribute('href', href); + newTheme.dataset[dataAttribute] = themeName; - document.head.appendChild(newTheme); + document.head.appendChild(newTheme); } diff --git a/src/plugins/themes/snow-theme.scss b/src/plugins/themes/snow-theme.scss index 294be82237..24342f120f 100644 --- a/src/plugins/themes/snow-theme.scss +++ b/src/plugins/themes/snow-theme.scss @@ -1,22 +1,22 @@ -@import "../../styles/vendor/normalize-min"; -@import "../../styles/constants"; -@import "../../styles/constants-mobile.scss"; +@import '../../styles/vendor/normalize-min'; +@import '../../styles/constants'; +@import '../../styles/constants-mobile.scss'; -@import "../../styles/constants-snow"; +@import '../../styles/constants-snow'; -@import "../../styles/mixins"; -@import "../../styles/animations"; -@import "../../styles/about"; -@import "../../styles/glyphs"; -@import "../../styles/global"; -@import "../../styles/status"; -@import "../../styles/limits"; -@import "../../styles/controls"; -@import "../../styles/forms"; -@import "../../styles/table"; -@import "../../styles/legacy"; -@import "../../styles/legacy-plots"; -@import "../../styles/plotly"; -@import "../../styles/legacy-messages"; +@import '../../styles/mixins'; +@import '../../styles/animations'; +@import '../../styles/about'; +@import '../../styles/glyphs'; +@import '../../styles/global'; +@import '../../styles/status'; +@import '../../styles/limits'; +@import '../../styles/controls'; +@import '../../styles/forms'; +@import '../../styles/table'; +@import '../../styles/legacy'; +@import '../../styles/legacy-plots'; +@import '../../styles/plotly'; +@import '../../styles/legacy-messages'; -@import "../../styles/vue-styles.scss"; +@import '../../styles/vue-styles.scss'; diff --git a/src/plugins/themes/snow.js b/src/plugins/themes/snow.js index 3befb82252..af6095282f 100644 --- a/src/plugins/themes/snow.js +++ b/src/plugins/themes/snow.js @@ -1,7 +1,7 @@ import { installTheme } from './installTheme'; export default function plugin() { - return function install(openmct) { - installTheme(openmct, 'snow'); - }; + return function install(openmct) { + installTheme(openmct, 'snow'); + }; } diff --git a/src/plugins/timeConductor/Conductor.vue b/src/plugins/timeConductor/Conductor.vue index 171175afb6..9079ed0fb1 100644 --- a/src/plugins/timeConductor/Conductor.vue +++ b/src/plugins/timeConductor/Conductor.vue @@ -20,50 +20,46 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorAxis.vue b/src/plugins/timeConductor/ConductorAxis.vue index 3794ad9ab1..05838d05c7 100644 --- a/src/plugins/timeConductor/ConductorAxis.vue +++ b/src/plugins/timeConductor/ConductorAxis.vue @@ -20,20 +20,12 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorHistory.vue b/src/plugins/timeConductor/ConductorHistory.vue index 7418e2ff09..3aed6944a9 100644 --- a/src/plugins/timeConductor/ConductorHistory.vue +++ b/src/plugins/timeConductor/ConductorHistory.vue @@ -20,20 +20,17 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorInputsFixed.vue b/src/plugins/timeConductor/ConductorInputsFixed.vue index dcbf552b81..ed404b9007 100644 --- a/src/plugins/timeConductor/ConductorInputsFixed.vue +++ b/src/plugins/timeConductor/ConductorInputsFixed.vue @@ -20,293 +20,288 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorInputsRealtime.vue b/src/plugins/timeConductor/ConductorInputsRealtime.vue index 2b7e22bd8c..76fb824326 100644 --- a/src/plugins/timeConductor/ConductorInputsRealtime.vue +++ b/src/plugins/timeConductor/ConductorInputsRealtime.vue @@ -20,310 +20,302 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorMode.vue b/src/plugins/timeConductor/ConductorMode.vue index 4d7c99f846..7852820844 100644 --- a/src/plugins/timeConductor/ConductorMode.vue +++ b/src/plugins/timeConductor/ConductorMode.vue @@ -20,163 +20,157 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorModeIcon.vue b/src/plugins/timeConductor/ConductorModeIcon.vue index 2a3cc59133..1486dcfb4d 100644 --- a/src/plugins/timeConductor/ConductorModeIcon.vue +++ b/src/plugins/timeConductor/ConductorModeIcon.vue @@ -20,8 +20,8 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorTimeSystem.vue b/src/plugins/timeConductor/ConductorTimeSystem.vue index 7812137738..80e7743e7f 100644 --- a/src/plugins/timeConductor/ConductorTimeSystem.vue +++ b/src/plugins/timeConductor/ConductorTimeSystem.vue @@ -20,114 +20,115 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/DatePicker.vue b/src/plugins/timeConductor/DatePicker.vue index 8be915d67c..25a7843792 100644 --- a/src/plugins/timeConductor/DatePicker.vue +++ b/src/plugins/timeConductor/DatePicker.vue @@ -20,69 +20,54 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/conductor-axis.scss b/src/plugins/timeConductor/conductor-axis.scss index 8fec88e73b..1f58cd8b12 100644 --- a/src/plugins/timeConductor/conductor-axis.scss +++ b/src/plugins/timeConductor/conductor-axis.scss @@ -1,67 +1,67 @@ @use 'sass:math'; .c-conductor-axis { - $h: 18px; - $tickYPos: math.div($h, 2) + 12px; + $h: 18px; + $tickYPos: math.div($h, 2) + 12px; - @include userSelectNone(); - @include bgTicks($c: rgba($colorBodyFg, 0.4)); - background-position: 0 50%; - background-size: 5px 2px; - border-radius: $controlCr; - height: $h; + @include userSelectNone(); + @include bgTicks($c: rgba($colorBodyFg, 0.4)); + background-position: 0 50%; + background-size: 5px 2px; + border-radius: $controlCr; + height: $h; - svg { - text-rendering: geometricPrecision; - width: 100%; - height: 100%; - > g.axis { - // Overall Tick holder - transform: translateY($tickYPos); - path { - // Domain line - display: none; - } + svg { + text-rendering: geometricPrecision; + width: 100%; + height: 100%; + > g.axis { + // Overall Tick holder + transform: translateY($tickYPos); + path { + // Domain line + display: none; + } - g { - // Each tick. These move on drag. - line { - // Line beneath ticks - display: none; - } - } - } - - text { - // Tick labels - fill: $colorBodyFg; - font-size: 1em; - paint-order: stroke; - font-weight: bold; - stroke: $colorBodyBg; - stroke-linecap: butt; - stroke-linejoin: bevel; - stroke-width: 6px; + g { + // Each tick. These move on drag. + line { + // Line beneath ticks + display: none; } + } } - body.desktop .is-fixed-mode & { - background-size: 3px 30%; - background-color: $colorBodyBgSubtle; - box-shadow: inset rgba(black, 0.4) 0 1px 1px; - - svg text { - fill: $colorBodyFg; - stroke: $colorBodyBgSubtle; - } + text { + // Tick labels + fill: $colorBodyFg; + font-size: 1em; + paint-order: stroke; + font-weight: bold; + stroke: $colorBodyBg; + stroke-linecap: butt; + stroke-linejoin: bevel; + stroke-width: 6px; } + } - .is-realtime-mode & { - $c: 1px solid rgba($colorTime, 0.7); - border-left: $c; - border-right: $c; - svg text { - fill: $colorTime; - } + body.desktop .is-fixed-mode & { + background-size: 3px 30%; + background-color: $colorBodyBgSubtle; + box-shadow: inset rgba(black, 0.4) 0 1px 1px; + + svg text { + fill: $colorBodyFg; + stroke: $colorBodyBgSubtle; } + } + + .is-realtime-mode & { + $c: 1px solid rgba($colorTime, 0.7); + border-left: $c; + border-right: $c; + svg text { + fill: $colorTime; + } + } } diff --git a/src/plugins/timeConductor/conductor-mode-icon.scss b/src/plugins/timeConductor/conductor-mode-icon.scss index cc7748ed93..a7091a0a35 100644 --- a/src/plugins/timeConductor/conductor-mode-icon.scss +++ b/src/plugins/timeConductor/conductor-mode-icon.scss @@ -1,107 +1,160 @@ @keyframes clock-hands { - 0% { transform: translate(-50%, -50%) rotate(0deg); } - 100% { transform: translate(-50%, -50%) rotate(360deg); } + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } } @keyframes clock-hands-sticky { - 0% { transform: translate(-50%, -50%) rotate(0deg); } - 7% { transform: translate(-50%, -50%) rotate(0deg); } - 8% { transform: translate(-50%, -50%) rotate(30deg); } - 15% { transform: translate(-50%, -50%) rotate(30deg); } - 16% { transform: translate(-50%, -50%) rotate(60deg); } - 24% { transform: translate(-50%, -50%) rotate(60deg); } - 25% { transform: translate(-50%, -50%) rotate(90deg); } - 32% { transform: translate(-50%, -50%) rotate(90deg); } - 33% { transform: translate(-50%, -50%) rotate(120deg); } - 40% { transform: translate(-50%, -50%) rotate(120deg); } - 41% { transform: translate(-50%, -50%) rotate(150deg); } - 49% { transform: translate(-50%, -50%) rotate(150deg); } - 50% { transform: translate(-50%, -50%) rotate(180deg); } - 57% { transform: translate(-50%, -50%) rotate(180deg); } - 58% { transform: translate(-50%, -50%) rotate(210deg); } - 65% { transform: translate(-50%, -50%) rotate(210deg); } - 66% { transform: translate(-50%, -50%) rotate(240deg); } - 74% { transform: translate(-50%, -50%) rotate(240deg); } - 75% { transform: translate(-50%, -50%) rotate(270deg); } - 82% { transform: translate(-50%, -50%) rotate(270deg); } - 83% { transform: translate(-50%, -50%) rotate(300deg); } - 90% { transform: translate(-50%, -50%) rotate(300deg); } - 91% { transform: translate(-50%, -50%) rotate(330deg); } - 99% { transform: translate(-50%, -50%) rotate(330deg); } - 100% { transform: translate(-50%, -50%) rotate(360deg); } + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 7% { + transform: translate(-50%, -50%) rotate(0deg); + } + 8% { + transform: translate(-50%, -50%) rotate(30deg); + } + 15% { + transform: translate(-50%, -50%) rotate(30deg); + } + 16% { + transform: translate(-50%, -50%) rotate(60deg); + } + 24% { + transform: translate(-50%, -50%) rotate(60deg); + } + 25% { + transform: translate(-50%, -50%) rotate(90deg); + } + 32% { + transform: translate(-50%, -50%) rotate(90deg); + } + 33% { + transform: translate(-50%, -50%) rotate(120deg); + } + 40% { + transform: translate(-50%, -50%) rotate(120deg); + } + 41% { + transform: translate(-50%, -50%) rotate(150deg); + } + 49% { + transform: translate(-50%, -50%) rotate(150deg); + } + 50% { + transform: translate(-50%, -50%) rotate(180deg); + } + 57% { + transform: translate(-50%, -50%) rotate(180deg); + } + 58% { + transform: translate(-50%, -50%) rotate(210deg); + } + 65% { + transform: translate(-50%, -50%) rotate(210deg); + } + 66% { + transform: translate(-50%, -50%) rotate(240deg); + } + 74% { + transform: translate(-50%, -50%) rotate(240deg); + } + 75% { + transform: translate(-50%, -50%) rotate(270deg); + } + 82% { + transform: translate(-50%, -50%) rotate(270deg); + } + 83% { + transform: translate(-50%, -50%) rotate(300deg); + } + 90% { + transform: translate(-50%, -50%) rotate(300deg); + } + 91% { + transform: translate(-50%, -50%) rotate(330deg); + } + 99% { + transform: translate(-50%, -50%) rotate(330deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } } - .c-clock-symbol { - $c: $colorBtnBg; //$colorObjHdrIc; - $d: 18px; - height: $d; - width: $d; - position: relative; + $c: $colorBtnBg; //$colorObjHdrIc; + $d: 18px; + height: $d; + width: $d; + position: relative; + &:before { + font-family: symbolsfont; + color: $c; + content: $glyph-icon-brackets; + font-size: $d; + line-height: normal; + display: block; + width: 100%; + height: 100%; + z-index: 1; + } + + // Clock hands + div[class*='hand'] { + $handW: 2px; + $handH: $d * 0.4; + animation-iteration-count: infinite; + animation-timing-function: steps(12); + transform-origin: bottom; + position: absolute; + height: $handW; + width: $handW; + left: 50%; + top: 50%; + z-index: 2; &:before { - font-family: symbolsfont; - color: $c; - content: $glyph-icon-brackets; - font-size: $d; - line-height: normal; - display: block; - width: 100%; - height: 100%; - z-index: 1; + background: $c; + content: ''; + display: block; + position: absolute; + width: 100%; + bottom: -1px; } + &.hand-little { + z-index: 2; + animation-duration: 12s; + transform: translate(-50%, -50%) rotate(120deg); + &:before { + height: ceil($handH * 0.6); + } + } + &.hand-big { + z-index: 1; + animation-duration: 1s; + transform: translate(-50%, -50%); + &:before { + height: $handH; + } + } + } - // Clock hands - div[class*="hand"] { - $handW: 2px; - $handH: $d * 0.4; - animation-iteration-count: infinite; - animation-timing-function: steps(12); - transform-origin: bottom; - position: absolute; - height: $handW; - width: $handW; - left: 50%; - top: 50%; - z-index: 2; - &:before { - background: $c; - content: ''; - display: block; - position: absolute; - width: 100%; - bottom: -1px; - } - &.hand-little { - z-index: 2; - animation-duration: 12s; - transform: translate(-50%, -50%) rotate(120deg); - &:before { - height: ceil($handH * 0.6); - } - } - &.hand-big { - z-index: 1; - animation-duration: 1s; - transform: translate(-50%, -50%); - &:before { - height: $handH; - } - } + // Modes + .is-realtime-mode &, + .is-lad-mode & { + &:before { + // Brackets icon + color: $colorTime; } - - // Modes - .is-realtime-mode &, - .is-lad-mode & { - &:before { - // Brackets icon - color: $colorTime; - } - div[class*="hand"] { - animation-name: clock-hands; - &:before { - background: $colorTime; - } - } + div[class*='hand'] { + animation-name: clock-hands; + &:before { + background: $colorTime; + } } + } } diff --git a/src/plugins/timeConductor/conductor-mode.scss b/src/plugins/timeConductor/conductor-mode.scss index 6939cb00cf..6835faeb8e 100644 --- a/src/plugins/timeConductor/conductor-mode.scss +++ b/src/plugins/timeConductor/conductor-mode.scss @@ -1,14 +1,14 @@ .c-conductor__mode-menu { - max-height: 80vh; - max-width: 500px; - min-height: 250px; - z-index: 70; + max-height: 80vh; + max-width: 500px; + min-height: 250px; + z-index: 70; - [class*="__icon"] { - filter: $colorKeyFilter; - } + [class*='__icon'] { + filter: $colorKeyFilter; + } - [class*="__item-description"] { - min-width: 200px; - } + [class*='__item-description'] { + min-width: 200px; + } } diff --git a/src/plugins/timeConductor/conductor.scss b/src/plugins/timeConductor/conductor.scss index 45acbf7a56..28dee1ac6a 100644 --- a/src/plugins/timeConductor/conductor.scss +++ b/src/plugins/timeConductor/conductor.scss @@ -1,310 +1,310 @@ .c-input--submit { - // Can't use display: none because some browsers will pretend the input doesn't exist, and enter won't work - visibility: none; - height: 0; - width: 0; - padding: 0; + // Can't use display: none because some browsers will pretend the input doesn't exist, and enter won't work + visibility: none; + height: 0; + width: 0; + padding: 0; } /*********************************************** CONDUCTOR LAYOUT */ .c-conductor { - &__inputs { - display: contents; - } + &__inputs { + display: contents; + } - &__time-bounds { - display: grid; - grid-column-gap: $interiorMargin; - grid-row-gap: $interiorMargin; - align-items: center; + &__time-bounds { + display: grid; + grid-column-gap: $interiorMargin; + grid-row-gap: $interiorMargin; + align-items: center; - // Default: fixed mode, desktop - grid-template-rows: 1fr; - grid-template-columns: 20px auto 1fr auto; - grid-template-areas: "tc-mode-icon tc-start tc-ticks tc-end"; - } + // Default: fixed mode, desktop + grid-template-rows: 1fr; + grid-template-columns: 20px auto 1fr auto; + grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-end'; + } - &__mode-icon { - grid-area: tc-mode-icon; - } + &__mode-icon { + grid-area: tc-mode-icon; + } - &__start-fixed, - &__start-delta { - grid-area: tc-start; - display: flex; - } + &__start-fixed, + &__start-delta { + grid-area: tc-start; + display: flex; + } - &__end-fixed, - &__end-delta { - grid-area: tc-end; - display: flex; - justify-content: flex-end; - } + &__end-fixed, + &__end-delta { + grid-area: tc-end; + display: flex; + justify-content: flex-end; + } - &__ticks { - grid-area: tc-ticks; - } + &__ticks { + grid-area: tc-ticks; + } - &__controls { - grid-area: tc-controls; - display: flex; - align-items: center; - > * + * { - margin-left: $interiorMargin; - } - } - - &.is-fixed-mode { - .c-conductor-axis { - &__zoom-indicator { - border: 1px solid transparent; - display: none; // Hidden by default - } - } - - &:not(.is-panning), - &:not(.is-zooming) { - .c-conductor-axis { - &:hover, - &:active { - cursor: col-resize; - } - } - } - - &.is-panning, - &.is-zooming { - .c-conductor-input input { - // Styles for inputs while zooming or panning - background: rgba($timeConductorActiveBg, 0.4); - } - } - - &.alt-pressed { - .c-conductor-axis:hover { - // When alt is being pressed and user is hovering over the axis, set the cursor - @include cursorGrab(); - } - } - - &.is-panning { - .c-conductor-axis { - @include cursorGrab(); - background-color: $timeConductorActivePanBg; - transition: $transIn; - - svg text { - stroke: $timeConductorActivePanBg; - transition: $transIn; - } - } - } - - &.is-zooming { - .c-conductor-axis__zoom-indicator { - display: block; - position: absolute; - background: rgba($timeConductorActiveBg, 0.4); - border-left-color: $timeConductorActiveBg; - border-right-color: $timeConductorActiveBg; - top: 0; bottom: 0; - } - } - } - - &.is-realtime-mode { - .c-conductor__time-bounds { - grid-template-columns: 20px auto 1fr auto auto; - grid-template-areas: "tc-mode-icon tc-start tc-ticks tc-updated tc-end"; - } - - .c-conductor__end-fixed { - grid-area: tc-updated; - } - } - - body.phone.portrait & { - .c-conductor__time-bounds { - grid-row-gap: $interiorMargin; - grid-template-rows: auto auto; - grid-template-columns: 20px auto auto; - } - - .c-conductor__controls { - padding-left: 25px; // Line up visually with other controls - } - - &__mode-icon { - grid-row: 1; - } - - &__ticks, - &__zoom { - display: none; - } - - &.is-fixed-mode { - [class*='__start-fixed'], - [class*='__end-fixed'] { - [class*='__label'] { - // Start and end are in separate columns; make the labels line up - width: 30px; - } - } - - [class*='__end-input'] { - justify-content: flex-start; - } - - .c-conductor__time-bounds { - grid-template-areas: - "tc-mode-icon tc-start tc-start" - "tc-mode-icon tc-end tc-end" - } - } - - &.is-realtime-mode { - .c-conductor__time-bounds { - grid-template-areas: - "tc-mode-icon tc-start tc-updated" - "tc-mode-icon tc-end tc-end"; - } - - .c-conductor__end-fixed { - justify-content: flex-end; - } - } - } -} - -.c-conductor-holder--compact { - min-height: 22px; - - .c-conductor { - &__inputs, - &__time-bounds { - display: flex; - - .c-toggle-switch { - // Used in independent Time Conductor - flex: 0 0 auto; - } - } - - &__inputs { - > * + * { - margin-left: $interiorMarginSm; - } - } - } - - .is-realtime-mode .c-conductor__end-fixed { - display: none !important; - } -} - -.c-conductor-input { - color: $colorInputFg; + &__controls { + grid-area: tc-controls; display: flex; align-items: center; - justify-content: flex-start; - > * + * { - margin-left: $interiorMarginSm; + margin-left: $interiorMargin; + } + } + + &.is-fixed-mode { + .c-conductor-axis { + &__zoom-indicator { + border: 1px solid transparent; + display: none; // Hidden by default + } } - &:before { - // Realtime-mode clock icon symbol - margin-right: $interiorMarginSm; - } - - .c-direction-indicator { - // Holds realtime-mode + and - symbols - font-size: 0.7em; - } - - input:invalid { - background: rgba($colorFormInvalid, 0.5); - } -} - -.is-realtime-mode { - .c-conductor__controls button, - .c-conductor__delta-button { - @include themedButton($colorTimeBg); - color: $colorTimeFg; - } - - .c-conductor-input { - &:before { - color: $colorTime; + &:not(.is-panning), + &:not(.is-zooming) { + .c-conductor-axis { + &:hover, + &:active { + cursor: col-resize; } + } + } + + &.is-panning, + &.is-zooming { + .c-conductor-input input { + // Styles for inputs while zooming or panning + background: rgba($timeConductorActiveBg, 0.4); + } + } + + &.alt-pressed { + .c-conductor-axis:hover { + // When alt is being pressed and user is hovering over the axis, set the cursor + @include cursorGrab(); + } + } + + &.is-panning { + .c-conductor-axis { + @include cursorGrab(); + background-color: $timeConductorActivePanBg; + transition: $transIn; + + svg text { + stroke: $timeConductorActivePanBg; + transition: $transIn; + } + } + } + + &.is-zooming { + .c-conductor-axis__zoom-indicator { + display: block; + position: absolute; + background: rgba($timeConductorActiveBg, 0.4); + border-left-color: $timeConductorActiveBg; + border-right-color: $timeConductorActiveBg; + top: 0; + bottom: 0; + } + } + } + + &.is-realtime-mode { + .c-conductor__time-bounds { + grid-template-columns: 20px auto 1fr auto auto; + grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-updated tc-end'; } .c-conductor__end-fixed { - // Displays last RT udpate - color: $colorTime; - - input { - // Remove input look - background: none; - box-shadow: none; - color: $colorTime; - pointer-events: none; - - &[disabled] { - opacity: 1 !important; - } - } + grid-area: tc-updated; } + } + + body.phone.portrait & { + .c-conductor__time-bounds { + grid-row-gap: $interiorMargin; + grid-template-rows: auto auto; + grid-template-columns: 20px auto auto; + } + + .c-conductor__controls { + padding-left: 25px; // Line up visually with other controls + } + + &__mode-icon { + grid-row: 1; + } + + &__ticks, + &__zoom { + display: none; + } + + &.is-fixed-mode { + [class*='__start-fixed'], + [class*='__end-fixed'] { + [class*='__label'] { + // Start and end are in separate columns; make the labels line up + width: 30px; + } + } + + [class*='__end-input'] { + justify-content: flex-start; + } + + .c-conductor__time-bounds { + grid-template-areas: + 'tc-mode-icon tc-start tc-start' + 'tc-mode-icon tc-end tc-end'; + } + } + + &.is-realtime-mode { + .c-conductor__time-bounds { + grid-template-areas: + 'tc-mode-icon tc-start tc-updated' + 'tc-mode-icon tc-end tc-end'; + } + + .c-conductor__end-fixed { + justify-content: flex-end; + } + } + } +} + +.c-conductor-holder--compact { + min-height: 22px; + + .c-conductor { + &__inputs, + &__time-bounds { + display: flex; + + .c-toggle-switch { + // Used in independent Time Conductor + flex: 0 0 auto; + } + } + + &__inputs { + > * + * { + margin-left: $interiorMarginSm; + } + } + } + + .is-realtime-mode .c-conductor__end-fixed { + display: none !important; + } +} + +.c-conductor-input { + color: $colorInputFg; + display: flex; + align-items: center; + justify-content: flex-start; + + > * + * { + margin-left: $interiorMarginSm; + } + + &:before { + // Realtime-mode clock icon symbol + margin-right: $interiorMarginSm; + } + + .c-direction-indicator { + // Holds realtime-mode + and - symbols + font-size: 0.7em; + } + + input:invalid { + background: rgba($colorFormInvalid, 0.5); + } +} + +.is-realtime-mode { + .c-conductor__controls button, + .c-conductor__delta-button { + @include themedButton($colorTimeBg); + color: $colorTimeFg; + } + + .c-conductor-input { + &:before { + color: $colorTime; + } + } + + .c-conductor__end-fixed { + // Displays last RT udpate + color: $colorTime; + + input { + // Remove input look + background: none; + box-shadow: none; + color: $colorTime; + pointer-events: none; + + &[disabled] { + opacity: 1 !important; + } + } + } } [class^='pr-tc-input-menu'] { - // Uses ^= here to target both start and end menus - background: $colorBodyBg; - border-radius: $controlCr; - display: grid; - grid-template-columns: 1fr 1fr 2fr; - grid-column-gap: 3px; - grid-row-gap: 4px; - align-items: start; - box-shadow: $shdwMenu; - padding: $interiorMargin; - position: absolute; - left: 8px; - bottom: 24px; - z-index: 99; + // Uses ^= here to target both start and end menus + background: $colorBodyBg; + border-radius: $controlCr; + display: grid; + grid-template-columns: 1fr 1fr 2fr; + grid-column-gap: 3px; + grid-row-gap: 4px; + align-items: start; + box-shadow: $shdwMenu; + padding: $interiorMargin; + position: absolute; + left: 8px; + bottom: 24px; + z-index: 99; - &[class*='--bottom'] { - bottom: auto; - top: 24px; - } + &[class*='--bottom'] { + bottom: auto; + top: 24px; + } } .l-shell__time-conductor .pr-tc-input-menu--end { - left: auto; - right: 0; + left: auto; + right: 0; } - [class^='pr-time'] { - &[class*='label'] { - font-size: 0.8em; - opacity: 0.6; - text-transform: uppercase; - } + &[class*='label'] { + font-size: 0.8em; + opacity: 0.6; + text-transform: uppercase; + } - &[class*='controls'] { - display: flex; - align-items: center; - white-space: nowrap; + &[class*='controls'] { + display: flex; + align-items: center; + white-space: nowrap; - input { - height: 22px; - line-height: 22px; - margin-right: $interiorMarginSm; - font-size: 1.25em; - width: 42px; - } + input { + height: 22px; + line-height: 22px; + margin-right: $interiorMarginSm; + font-size: 1.25em; + width: 42px; } + } } diff --git a/src/plugins/timeConductor/date-picker.scss b/src/plugins/timeConductor/date-picker.scss index 3c447e9745..cd36818e63 100644 --- a/src/plugins/timeConductor/date-picker.scss +++ b/src/plugins/timeConductor/date-picker.scss @@ -1,101 +1,101 @@ /******************************************************** PICKER */ .c-datetime-picker { - @include userSelectNone(); - padding: $interiorMarginLg !important; - display: flex !important; // Override .c-menu display: block; - flex-direction: column; - > * + * { - margin-top: $interiorMargin; - } + @include userSelectNone(); + padding: $interiorMarginLg !important; + display: flex !important; // Override .c-menu display: block; + flex-direction: column; + > * + * { + margin-top: $interiorMargin; + } - &__close-button { - display: none; // Only show when body.phone, see below. - } + &__close-button { + display: none; // Only show when body.phone, see below. + } - &__pager { - flex: 0 0 auto; - } + &__pager { + flex: 0 0 auto; + } - &__calendar { - border-top: 1px solid $colorInteriorBorder; - flex: 1 1 auto; - } + &__calendar { + border-top: 1px solid $colorInteriorBorder; + flex: 1 1 auto; + } } .c-pager { - display: grid; - grid-column-gap: $interiorMargin; - grid-template-rows: 1fr; - grid-template-columns: auto 1fr auto; - align-items: center; + display: grid; + grid-column-gap: $interiorMargin; + grid-template-rows: 1fr; + grid-template-columns: auto 1fr auto; + align-items: center; - .c-icon-button { - font-size: 0.8em; - } + .c-icon-button { + font-size: 0.8em; + } - &__month-year { - text-align: center; - } + &__month-year { + text-align: center; + } } /******************************************************** CALENDAR */ .c-calendar { - display: grid; - grid-template-columns: repeat(7, min-content); - grid-template-rows: auto; - grid-gap: 1px; - height: 100%; + display: grid; + grid-template-columns: repeat(7, min-content); + grid-template-rows: auto; + grid-gap: 1px; + height: 100%; - $mutedOpacity: 0.5; + $mutedOpacity: 0.5; - ul { - display: contents; - &[class*='--header'] { - pointer-events: none; - li { - opacity: $mutedOpacity; - } - } + ul { + display: contents; + &[class*='--header'] { + pointer-events: none; + li { + opacity: $mutedOpacity; + } + } + } + + li { + display: flex; + flex-direction: column; + justify-content: center !important; + padding: $interiorMargin; + + &.is-in-month { + background: $colorMenuElementHilite; } - li { - display: flex; - flex-direction: column; - justify-content: center !important; - padding: $interiorMargin; - - &.is-in-month { - background: $colorMenuElementHilite; - } - - &.selected { - background: $colorKey; - color: $colorKeyFg; - } + &.selected { + background: $colorKey; + color: $colorKeyFg; } + } - &__day { - &--sub { - opacity: $mutedOpacity; - font-size: 0.8em; - } + &__day { + &--sub { + opacity: $mutedOpacity; + font-size: 0.8em; } + } } /******************************************************** MOBILE */ body.phone { - .c-datetime-picker { - &.c-menu { - @include modalFullScreen(); - } - - &__close-button { - display: flex; - justify-content: flex-end; - } + .c-datetime-picker { + &.c-menu { + @include modalFullScreen(); } - .c-calendar { - grid-template-columns: repeat(7, auto); + &__close-button { + display: flex; + justify-content: flex-end; } + } + + .c-calendar { + grid-template-columns: repeat(7, auto); + } } diff --git a/src/plugins/timeConductor/independent/IndependentTimeConductor.vue b/src/plugins/timeConductor/independent/IndependentTimeConductor.vue index 7b5cde4927..19a363ac7a 100644 --- a/src/plugins/timeConductor/independent/IndependentTimeConductor.vue +++ b/src/plugins/timeConductor/independent/IndependentTimeConductor.vue @@ -20,237 +20,242 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/independent/Mode.vue b/src/plugins/timeConductor/independent/Mode.vue index b3fcc98aa2..94d439a623 100644 --- a/src/plugins/timeConductor/independent/Mode.vue +++ b/src/plugins/timeConductor/independent/Mode.vue @@ -20,206 +20,212 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/plugin.js b/src/plugins/timeConductor/plugin.js index 2eec7210e6..849eb18857 100644 --- a/src/plugins/timeConductor/plugin.js +++ b/src/plugins/timeConductor/plugin.js @@ -23,101 +23,110 @@ import Conductor from './Conductor.vue'; function isTruthy(a) { - return Boolean(a); + return Boolean(a); } function validateMenuOption(menuOption, index) { - if (menuOption.clock && !menuOption.clockOffsets) { - return `Conductor menu option is missing required property 'clockOffsets'. This field is required when configuring a menu option with a clock.\r\n${JSON.stringify(menuOption)}`; - } + if (menuOption.clock && !menuOption.clockOffsets) { + return `Conductor menu option is missing required property 'clockOffsets'. This field is required when configuring a menu option with a clock.\r\n${JSON.stringify( + menuOption + )}`; + } - if (!menuOption.timeSystem) { - return `Conductor menu option is missing required property 'timeSystem'\r\n${JSON.stringify(menuOption)}`; - } + if (!menuOption.timeSystem) { + return `Conductor menu option is missing required property 'timeSystem'\r\n${JSON.stringify( + menuOption + )}`; + } - if (!menuOption.bounds && !menuOption.clock) { - return `Conductor menu option is missing required property 'bounds'. This field is required when configuring a menu option with fixed bounds.\r\n${JSON.stringify(menuOption)}`; - } + if (!menuOption.bounds && !menuOption.clock) { + return `Conductor menu option is missing required property 'bounds'. This field is required when configuring a menu option with fixed bounds.\r\n${JSON.stringify( + menuOption + )}`; + } } function hasRequiredOptions(config) { - if (config === undefined - || config.menuOptions === undefined - || config.menuOptions.length === 0) { - return "You must specify one or more 'menuOptions'."; - } + if (config === undefined || config.menuOptions === undefined || config.menuOptions.length === 0) { + return "You must specify one or more 'menuOptions'."; + } - if (config.menuOptions.some(validateMenuOption)) { - return config.menuOptions.map(validateMenuOption) - .filter(isTruthy) - .join('\n'); - } + if (config.menuOptions.some(validateMenuOption)) { + return config.menuOptions.map(validateMenuOption).filter(isTruthy).join('\n'); + } - return undefined; + return undefined; } function validateConfiguration(config, openmct) { - const systems = openmct.time.getAllTimeSystems() - .reduce(function (m, ts) { - m[ts.key] = ts; + const systems = openmct.time.getAllTimeSystems().reduce(function (m, ts) { + m[ts.key] = ts; - return m; - }, {}); - const clocks = openmct.time.getAllClocks() - .reduce(function (m, c) { - m[c.key] = c; + return m; + }, {}); + const clocks = openmct.time.getAllClocks().reduce(function (m, c) { + m[c.key] = c; - return m; - }, {}); + return m; + }, {}); - return config.menuOptions.map(function (menuOption) { - let message = ''; - if (menuOption.timeSystem && !systems[menuOption.timeSystem]) { - message = `Time system '${menuOption.timeSystem}' has not been registered: \r\n ${JSON.stringify(menuOption)}`; - } + return config.menuOptions + .map(function (menuOption) { + let message = ''; + if (menuOption.timeSystem && !systems[menuOption.timeSystem]) { + message = `Time system '${ + menuOption.timeSystem + }' has not been registered: \r\n ${JSON.stringify(menuOption)}`; + } - if (menuOption.clock && !clocks[menuOption.clock]) { - message = `Clock '${menuOption.clock}' has not been registered: \r\n ${JSON.stringify(menuOption)}`; - } + if (menuOption.clock && !clocks[menuOption.clock]) { + message = `Clock '${menuOption.clock}' has not been registered: \r\n ${JSON.stringify( + menuOption + )}`; + } - return message; - }).filter(isTruthy).join('\n'); + return message; + }) + .filter(isTruthy) + .join('\n'); } function throwIfError(configResult) { - if (configResult) { - throw new Error(`Invalid Time Conductor Configuration. ${configResult} \r\n https://github.com/nasa/openmct/blob/master/API.md#the-time-conductor`); - } + if (configResult) { + throw new Error( + `Invalid Time Conductor Configuration. ${configResult} \r\n https://github.com/nasa/openmct/blob/master/API.md#the-time-conductor` + ); + } } function mountComponent(openmct, configuration) { - openmct.layout.conductorComponent = Object.create({ - components: { - Conductor - }, - template: "", - provide: { - openmct: openmct, - configuration: configuration - } - }); + openmct.layout.conductorComponent = Object.create({ + components: { + Conductor + }, + template: '', + provide: { + openmct: openmct, + configuration: configuration + } + }); } export default function (config) { - return function (openmct) { - let configResult = hasRequiredOptions(config) || validateConfiguration(config, openmct); - throwIfError(configResult); + return function (openmct) { + let configResult = hasRequiredOptions(config) || validateConfiguration(config, openmct); + throwIfError(configResult); - const defaults = config.menuOptions[0]; - if (defaults.clock) { - openmct.time.clock(defaults.clock, defaults.clockOffsets); - openmct.time.timeSystem(defaults.timeSystem, openmct.time.bounds()); - } else { - openmct.time.timeSystem(defaults.timeSystem, defaults.bounds); - } + const defaults = config.menuOptions[0]; + if (defaults.clock) { + openmct.time.clock(defaults.clock, defaults.clockOffsets); + openmct.time.timeSystem(defaults.timeSystem, openmct.time.bounds()); + } else { + openmct.time.timeSystem(defaults.timeSystem, defaults.bounds); + } - openmct.on('start', function () { - mountComponent(openmct, config); - }); - }; + openmct.on('start', function () { + mountComponent(openmct, config); + }); + }; } diff --git a/src/plugins/timeConductor/pluginSpec.js b/src/plugins/timeConductor/pluginSpec.js index 32b3fb0b62..b7753ac0c8 100644 --- a/src/plugins/timeConductor/pluginSpec.js +++ b/src/plugins/timeConductor/pluginSpec.js @@ -20,9 +20,9 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createMouseEvent, createOpenMct, resetApplicationState} from "utils/testing"; -import {millisecondsToDHMS, getPreciseDuration} from "../../utils/duration"; -import ConductorPlugin from "./plugin"; +import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing'; +import { millisecondsToDHMS, getPreciseDuration } from '../../utils/duration'; +import ConductorPlugin from './plugin'; import Vue from 'vue'; const THIRTY_SECONDS = 30 * 1000; @@ -33,132 +33,149 @@ const THIRTY_MINUTES = FIFTEEN_MINUTES * 2; const date = new Date(Date.UTC(78, 0, 20, 0, 0, 0)).getTime(); describe('time conductor', () => { - let element; - let child; - let appHolder; - let openmct; - let config = { - menuOptions: [ - { - name: "FixedTimeRange", - timeSystem: 'utc', - bounds: { - start: date - THIRTY_MINUTES, - end: date - }, - presets: [], - records: 2 - }, - { - name: "LocalClock", - timeSystem: 'utc', - clock: 'local', - clockOffsets: { - start: -THIRTY_MINUTES, - end: THIRTY_SECONDS - }, - presets: [] - } - ] - }; + let element; + let child; + let appHolder; + let openmct; + let config = { + menuOptions: [ + { + name: 'FixedTimeRange', + timeSystem: 'utc', + bounds: { + start: date - THIRTY_MINUTES, + end: date + }, + presets: [], + records: 2 + }, + { + name: 'LocalClock', + timeSystem: 'utc', + clock: 'local', + clockOffsets: { + start: -THIRTY_MINUTES, + end: THIRTY_SECONDS + }, + presets: [] + } + ] + }; + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(new ConductorPlugin(config)); + + element = document.createElement('div'); + element.style.width = '640px'; + element.style.height = '480px'; + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + element.appendChild(child); + + openmct.on('start', () => { + openmct.time.bounds({ + start: config.menuOptions[0].bounds.start, + end: config.menuOptions[0].bounds.end + }); + Vue.nextTick(() => { + done(); + }); + }); + appHolder = document.createElement('div'); + openmct.start(appHolder); + }); + + afterEach(() => { + appHolder = undefined; + openmct = undefined; + + return resetApplicationState(openmct); + }); + + describe('in fixed time mode', () => { + it('shows delta inputs', () => { + const fixedModeEl = appHolder.querySelector('.is-fixed-mode'); + const dateTimeInputs = fixedModeEl.querySelectorAll('.c-input--datetime'); + expect(dateTimeInputs[0].value).toEqual('1978-01-19 23:30:00.000Z'); + expect(dateTimeInputs[1].value).toEqual('1978-01-20 00:00:00.000Z'); + expect(fixedModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual( + 'Fixed Timespan' + ); + }); + }); + + describe('in realtime mode', () => { beforeEach((done) => { - openmct = createOpenMct(); - openmct.install(new ConductorPlugin(config)); + const switcher = appHolder.querySelector('.c-mode-button'); + const clickEvent = createMouseEvent('click'); - element = document.createElement('div'); - element.style.width = '640px'; - element.style.height = '480px'; - child = document.createElement('div'); - child.style.width = '640px'; - child.style.height = '480px'; - element.appendChild(child); - - openmct.on('start', () => { - openmct.time.bounds({ - start: config.menuOptions[0].bounds.start, - end: config.menuOptions[0].bounds.end - }); - Vue.nextTick(() => { - done(); - }); + switcher.dispatchEvent(clickEvent); + Vue.nextTick(() => { + const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1]; + clockItem.dispatchEvent(clickEvent); + Vue.nextTick(() => { + done(); }); - appHolder = document.createElement("div"); - openmct.start(appHolder); + }); }); - afterEach(() => { - appHolder = undefined; - openmct = undefined; + it('shows delta inputs', () => { + const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); + const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button'); - return resetApplicationState(openmct); + expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00'); + expect(dateTimeInputs[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30'); }); - describe('in fixed time mode', () => { - it('shows delta inputs', () => { - const fixedModeEl = appHolder.querySelector('.is-fixed-mode'); - const dateTimeInputs = fixedModeEl.querySelectorAll('.c-input--datetime'); - expect(dateTimeInputs[0].value).toEqual('1978-01-19 23:30:00.000Z'); - expect(dateTimeInputs[1].value).toEqual('1978-01-20 00:00:00.000Z'); - expect(fixedModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual('Fixed Timespan'); - }); + it('shows clock options', () => { + const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); + + expect(realtimeModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual( + 'Local Clock' + ); }); - describe('in realtime mode', () => { - beforeEach((done) => { - const switcher = appHolder.querySelector('.c-mode-button'); - const clickEvent = createMouseEvent("click"); + it('shows the current time', () => { + const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); + const currentTimeEl = realtimeModeEl.querySelector('.c-input--datetime'); + const currentTime = openmct.time.clock().currentValue(); + const { start, end } = openmct.time.bounds(); - switcher.dispatchEvent(clickEvent); - Vue.nextTick(() => { - const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1]; - clockItem.dispatchEvent(clickEvent); - Vue.nextTick(() => { - done(); - }); - }); - }); - - it('shows delta inputs', () => { - const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); - const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button'); - - expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00'); - expect(dateTimeInputs[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30'); - }); - - it('shows clock options', () => { - const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); - - expect(realtimeModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual('Local Clock'); - }); - - it('shows the current time', () => { - const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); - const currentTimeEl = realtimeModeEl.querySelector('.c-input--datetime'); - const currentTime = openmct.time.clock().currentValue(); - const { start, end } = openmct.time.bounds(); - - expect(currentTime).toBeGreaterThan(start); - expect(currentTime).toBeLessThanOrEqual(end); - expect(currentTimeEl.value.length).toBeGreaterThan(0); - }); + expect(currentTime).toBeGreaterThan(start); + expect(currentTime).toBeLessThanOrEqual(end); + expect(currentTimeEl.value.length).toBeGreaterThan(0); }); - + }); }); describe('duration functions', () => { - it('should transform milliseconds to DHMS', () => { - const functionResults = [millisecondsToDHMS(0), millisecondsToDHMS(86400000), - millisecondsToDHMS(129600000), millisecondsToDHMS(661824000), millisecondsToDHMS(213927028)]; - const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms']; - expect(validResults).toEqual(functionResults); - }); + it('should transform milliseconds to DHMS', () => { + const functionResults = [ + millisecondsToDHMS(0), + millisecondsToDHMS(86400000), + millisecondsToDHMS(129600000), + millisecondsToDHMS(661824000), + millisecondsToDHMS(213927028) + ]; + const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms']; + expect(validResults).toEqual(functionResults); + }); - it('should get precise duration', () => { - const functionResults = [getPreciseDuration(0), getPreciseDuration(643680000), - getPreciseDuration(1605312000), getPreciseDuration(213927028)]; - const validResults = ['00:00:00:00:000', '07:10:48:00:000', '18:13:55:12:000', '02:11:25:27:028']; - expect(validResults).toEqual(functionResults); - }); + it('should get precise duration', () => { + const functionResults = [ + getPreciseDuration(0), + getPreciseDuration(643680000), + getPreciseDuration(1605312000), + getPreciseDuration(213927028) + ]; + const validResults = [ + '00:00:00:00:000', + '07:10:48:00:000', + '18:13:55:12:000', + '02:11:25:27:028' + ]; + expect(validResults).toEqual(functionResults); + }); }); diff --git a/src/plugins/timeConductor/timePopup.vue b/src/plugins/timeConductor/timePopup.vue index ddc20334e3..9802d11a71 100644 --- a/src/plugins/timeConductor/timePopup.vue +++ b/src/plugins/timeConductor/timePopup.vue @@ -20,183 +20,179 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/utcMultiTimeFormat.js b/src/plugins/timeConductor/utcMultiTimeFormat.js index 7765e376a1..8880f72241 100644 --- a/src/plugins/timeConductor/utcMultiTimeFormat.js +++ b/src/plugins/timeConductor/utcMultiTimeFormat.js @@ -23,44 +23,67 @@ import moment from 'moment'; export default function multiFormat(date) { - const momentified = moment.utc(date); - /** - * Uses logic from d3 Time-Scales, v3 of the API. See - * https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md - * - * Licensed - */ - const format = [ - [".SSS", function (m) { - return m.milliseconds(); - }], - [":ss", function (m) { - return m.seconds(); - }], - ["HH:mm", function (m) { - return m.minutes(); - }], - ["HH:mm", function (m) { - return m.hours(); - }], - ["ddd DD", function (m) { - return m.days() - && m.date() !== 1; - }], - ["MMM DD", function (m) { - return m.date() !== 1; - }], - ["MMMM", function (m) { - return m.month(); - }], - ["YYYY", function () { - return true; - }] - ].filter(function (row) { - return row[1](momentified); - })[0][0]; + const momentified = moment.utc(date); + /** + * Uses logic from d3 Time-Scales, v3 of the API. See + * https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md + * + * Licensed + */ + const format = [ + [ + '.SSS', + function (m) { + return m.milliseconds(); + } + ], + [ + ':ss', + function (m) { + return m.seconds(); + } + ], + [ + 'HH:mm', + function (m) { + return m.minutes(); + } + ], + [ + 'HH:mm', + function (m) { + return m.hours(); + } + ], + [ + 'ddd DD', + function (m) { + return m.days() && m.date() !== 1; + } + ], + [ + 'MMM DD', + function (m) { + return m.date() !== 1; + } + ], + [ + 'MMMM', + function (m) { + return m.month(); + } + ], + [ + 'YYYY', + function () { + return true; + } + ] + ].filter(function (row) { + return row[1](momentified); + })[0][0]; - if (format !== undefined) { - return moment.utc(date).format(format); - } + if (format !== undefined) { + return moment.utc(date).format(format); + } } diff --git a/src/plugins/timeline/TimelineCompositionPolicy.js b/src/plugins/timeline/TimelineCompositionPolicy.js index 4d2dc675e9..f8e20b9c2d 100644 --- a/src/plugins/timeline/TimelineCompositionPolicy.js +++ b/src/plugins/timeline/TimelineCompositionPolicy.js @@ -20,53 +20,51 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -const ALLOWED_TYPES = [ - 'telemetry.plot.overlay', - 'telemetry.plot.stacked', - 'plan', - 'gantt-chart' -]; -const DISALLOWED_TYPES = [ - 'telemetry.plot.bar-graph', - 'telemetry.plot.scatter-plot' -]; +const ALLOWED_TYPES = ['telemetry.plot.overlay', 'telemetry.plot.stacked', 'plan', 'gantt-chart']; +const DISALLOWED_TYPES = ['telemetry.plot.bar-graph', 'telemetry.plot.scatter-plot']; export default function TimelineCompositionPolicy(openmct) { - function hasNumericTelemetry(domainObject, metadata) { - const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); - if (!hasTelemetry || !metadata) { - return false; - } - - return metadata.values().length > 0 && hasDomainAndRange(metadata); + function hasNumericTelemetry(domainObject, metadata) { + const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); + if (!hasTelemetry || !metadata) { + return false; } - function hasDomainAndRange(metadata) { - return (metadata.valuesForHints(['range']).length > 0 - && metadata.valuesForHints(['domain']).length > 0); + return metadata.values().length > 0 && hasDomainAndRange(metadata); + } + + function hasDomainAndRange(metadata) { + return ( + metadata.valuesForHints(['range']).length > 0 && + metadata.valuesForHints(['domain']).length > 0 + ); + } + + function hasImageTelemetry(domainObject, metadata) { + if (!metadata) { + return false; } - function hasImageTelemetry(domainObject, metadata) { - if (!metadata) { - return false; + return metadata.valuesForHints(['image']).length > 0; + } + + return { + allow: function (parent, child) { + if (parent.type === 'time-strip') { + const metadata = openmct.telemetry.getMetadata(child); + + if ( + !DISALLOWED_TYPES.includes(child.type) && + (hasNumericTelemetry(child, metadata) || + hasImageTelemetry(child, metadata) || + ALLOWED_TYPES.includes(child.type)) + ) { + return true; } - return metadata.valuesForHints(['image']).length > 0; + return false; + } + + return true; } - - return { - allow: function (parent, child) { - if (parent.type === 'time-strip') { - const metadata = openmct.telemetry.getMetadata(child); - - if (!DISALLOWED_TYPES.includes(child.type) - && (hasNumericTelemetry(child, metadata) || hasImageTelemetry(child, metadata) || ALLOWED_TYPES.includes(child.type))) { - return true; - } - - return false; - } - - return true; - } - }; + }; } diff --git a/src/plugins/timeline/TimelineObjectView.vue b/src/plugins/timeline/TimelineObjectView.vue index e291d2a73c..665d207a89 100644 --- a/src/plugins/timeline/TimelineObjectView.vue +++ b/src/plugins/timeline/TimelineObjectView.vue @@ -21,118 +21,124 @@ --> diff --git a/src/plugins/timeline/TimelineViewLayout.vue b/src/plugins/timeline/TimelineViewLayout.vue index ff754baf5b..32793c5da9 100644 --- a/src/plugins/timeline/TimelineViewLayout.vue +++ b/src/plugins/timeline/TimelineViewLayout.vue @@ -21,179 +21,176 @@ --> diff --git a/src/plugins/timeline/TimelineViewProvider.js b/src/plugins/timeline/TimelineViewProvider.js index 96d067930c..357a1c5fa3 100644 --- a/src/plugins/timeline/TimelineViewProvider.js +++ b/src/plugins/timeline/TimelineViewProvider.js @@ -24,43 +24,42 @@ import TimelineViewLayout from './TimelineViewLayout.vue'; import Vue from 'vue'; export default function TimelineViewProvider(openmct) { + return { + key: 'time-strip.view', + name: 'TimeStrip', + cssClass: 'icon-clock', + canView(domainObject) { + return domainObject.type === 'time-strip'; + }, - return { - key: 'time-strip.view', - name: 'TimeStrip', - cssClass: 'icon-clock', - canView(domainObject) { - return domainObject.type === 'time-strip'; + canEdit(domainObject) { + return domainObject.type === 'time-strip'; + }, + + view: function (domainObject, objectPath) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + TimelineViewLayout + }, + provide: { + openmct, + domainObject, + composition: openmct.composition.get(domainObject), + objectPath + }, + template: '' + }); }, - - canEdit(domainObject) { - return domainObject.type === 'time-strip'; - }, - - view: function (domainObject, objectPath) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - TimelineViewLayout - }, - provide: { - openmct, - domainObject, - composition: openmct.composition.get(domainObject), - objectPath - }, - template: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/timeline/plugin.js b/src/plugins/timeline/plugin.js index 7aa7aec47c..3839925bd9 100644 --- a/src/plugins/timeline/plugin.js +++ b/src/plugins/timeline/plugin.js @@ -21,28 +21,28 @@ *****************************************************************************/ import TimelineViewProvider from './TimelineViewProvider'; -import timelineInterceptor from "./timelineInterceptor"; -import TimelineCompositionPolicy from "./TimelineCompositionPolicy"; +import timelineInterceptor from './timelineInterceptor'; +import TimelineCompositionPolicy from './TimelineCompositionPolicy'; export default function () { - return function install(openmct) { - openmct.types.addType('time-strip', { - name: 'Time Strip', - key: 'time-strip', - description: 'Compose and display time-based telemetry and other object types in a timeline-like view.', - creatable: true, - cssClass: 'icon-timeline', - initialize: function (domainObject) { - domainObject.composition = []; - domainObject.configuration = { - useIndependentTime: false - }; - } - }); - timelineInterceptor(openmct); - openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow); + return function install(openmct) { + openmct.types.addType('time-strip', { + name: 'Time Strip', + key: 'time-strip', + description: + 'Compose and display time-based telemetry and other object types in a timeline-like view.', + creatable: true, + cssClass: 'icon-timeline', + initialize: function (domainObject) { + domainObject.composition = []; + domainObject.configuration = { + useIndependentTime: false + }; + } + }); + timelineInterceptor(openmct); + openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow); - openmct.objectViews.addProvider(new TimelineViewProvider(openmct)); - }; + openmct.objectViews.addProvider(new TimelineViewProvider(openmct)); + }; } - diff --git a/src/plugins/timeline/pluginSpec.js b/src/plugins/timeline/pluginSpec.js index 2aa0f8ac18..dc2181fd74 100644 --- a/src/plugins/timeline/pluginSpec.js +++ b/src/plugins/timeline/pluginSpec.js @@ -20,351 +20,369 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createOpenMct, resetApplicationState } from "@/utils/testing"; -import TimelinePlugin from "./plugin"; +import { createOpenMct, resetApplicationState } from '@/utils/testing'; +import TimelinePlugin from './plugin'; import Vue from 'vue'; -import EventEmitter from "EventEmitter"; +import EventEmitter from 'EventEmitter'; describe('the plugin', function () { - let objectDef; - let appHolder; - let element; - let child; - let openmct; - let mockObjectPath; - let mockCompositionForTimelist; - let planObject = { + let objectDef; + let appHolder; + let element; + let child; + let openmct; + let mockObjectPath; + let mockCompositionForTimelist; + let planObject = { + identifier: { + key: 'test-plan-object', + namespace: '' + }, + type: 'plan', + id: 'test-plan-object', + selectFile: { + body: JSON.stringify({ + 'TEST-GROUP': [ + { + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', + start: 1597170002854, + end: 1597171032854, + type: 'TEST-GROUP', + color: 'fuchsia', + textColor: 'black' + }, + { + name: 'Sed ut perspiciatis', + start: 1597171132854, + end: 1597171232854, + type: 'TEST-GROUP', + color: 'fuchsia', + textColor: 'black' + } + ] + }) + } + }; + let timelineObject = { + composition: [], + configuration: { + useIndependentTime: false, + timeOptions: { + mode: { + key: 'fixed' + }, + fixedOffsets: { + start: 10, + end: 11 + }, + clockOffsets: { + start: -(30 * 60 * 1000), + end: 30 * 60 * 1000 + } + } + }, + name: 'Some timestrip', + type: 'time-strip', + location: 'mine', + modified: 1631005183584, + persisted: 1631005183502, + identifier: { + namespace: '', + key: 'b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9' + } + }; + + beforeEach((done) => { + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + + mockObjectPath = [ + { + name: 'mock folder', + type: 'fake-folder', identifier: { - key: 'test-plan-object', - namespace: '' - }, - type: 'plan', - id: "test-plan-object", - selectFile: { - body: JSON.stringify({ - "TEST-GROUP": [ - { - "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", - "start": 1597170002854, - "end": 1597171032854, - "type": "TEST-GROUP", - "color": "fuchsia", - "textColor": "black" - }, - { - "name": "Sed ut perspiciatis", - "start": 1597171132854, - "end": 1597171232854, - "type": "TEST-GROUP", - "color": "fuchsia", - "textColor": "black" - } - ] - }) + key: 'mock-folder', + namespace: '' } + }, + { + name: 'mock parent folder', + type: 'time-strip', + identifier: { + key: 'mock-parent-folder', + namespace: '' + } + } + ]; + + const timeSystem = { + timeSystemKey: 'utc', + bounds: { + start: 1597160002854, + end: 1597181232854 + } }; - let timelineObject = { - "composition": [], - configuration: { - useIndependentTime: false, - timeOptions: { - mode: { - key: 'fixed' - }, - fixedOffsets: { - start: 10, - end: 11 - }, - clockOffsets: { - start: -(30 * 60 * 1000), - end: (30 * 60 * 1000) - } + + openmct = createOpenMct(timeSystem); + openmct.install(new TimelinePlugin()); + + objectDef = openmct.types.get('time-strip').definition; + + element = document.createElement('div'); + element.style.width = '640px'; + element.style.height = '480px'; + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + element.appendChild(child); + + openmct.on('start', done); + openmct.start(appHolder); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + let mockObject = { + name: 'Time Strip', + key: 'time-strip', + creatable: true + }; + + it('defines a time-strip object type with the correct key', () => { + expect(objectDef.key).toEqual(mockObject.key); + }); + + describe('the time-strip object', () => { + it('is creatable', () => { + expect(objectDef.creatable).toEqual(mockObject.creatable); + }); + }); + + describe('the view', () => { + let timelineView; + let testViewObject; + + beforeEach(() => { + testViewObject = { + ...timelineObject + }; + + const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); + timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); + let view = timelineView.view(testViewObject, mockObjectPath); + view.show(child, true); + + return Vue.nextTick(); + }); + + it('provides a view', () => { + expect(timelineView).toBeDefined(); + }); + + it('displays a time axis', () => { + const el = element.querySelector('.c-timesystem-axis'); + expect(el).toBeDefined(); + }); + + it('does not show the independent time conductor based on configuration', () => { + const independentTimeConductorEl = element.querySelector( + '.c-timeline-holder > .c-conductor__controls' + ); + expect(independentTimeConductorEl).toBeNull(); + }); + }); + + describe('the timeline composition', () => { + let timelineDomainObject; + let timelineView; + + beforeEach(() => { + timelineDomainObject = { + ...timelineObject, + composition: [ + { + identifier: { + key: 'test-plan-object', + namespace: '' } - }, - "name": "Some timestrip", - "type": "time-strip", - "location": "mine", - "modified": 1631005183584, - "persisted": 1631005183502, - "identifier": { - "namespace": "", - "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" - } + } + ] + }; + + mockCompositionForTimelist = new EventEmitter(); + mockCompositionForTimelist.load = () => { + mockCompositionForTimelist.emit('add', planObject); + + return [planObject]; + }; + + spyOn(openmct.composition, 'get') + .withArgs(timelineDomainObject) + .and.returnValue(mockCompositionForTimelist); + + openmct.router.path = [timelineDomainObject]; + + const applicableViews = openmct.objectViews.get(timelineDomainObject, [timelineDomainObject]); + timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); + let view = timelineView.view(timelineDomainObject, [timelineDomainObject]); + view.show(child, true); + + return Vue.nextTick(); + }); + + it('loads the plan from composition', () => { + return Vue.nextTick(() => { + const items = element.querySelectorAll('.js-timeline__content'); + expect(items.length).toEqual(1); + }); + }); + }); + + describe('the independent time conductor', () => { + let timelineView; + let testViewObject = { + ...timelineObject, + configuration: { + ...timelineObject.configuration, + useIndependentTime: true + } }; beforeEach((done) => { - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; + const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); + timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); + let view = timelineView.view(testViewObject, mockObjectPath); + view.show(child, true); - mockObjectPath = [ - { - name: 'mock folder', - type: 'fake-folder', - identifier: { - key: 'mock-folder', - namespace: '' - } - }, - { - name: 'mock parent folder', - type: 'time-strip', - identifier: { - key: 'mock-parent-folder', - namespace: '' - } - } - ]; - - const timeSystem = { - timeSystemKey: 'utc', - bounds: { - start: 1597160002854, - end: 1597181232854 - } - }; - - openmct = createOpenMct(timeSystem); - openmct.install(new TimelinePlugin()); - - objectDef = openmct.types.get('time-strip').definition; - - element = document.createElement('div'); - element.style.width = '640px'; - element.style.height = '480px'; - child = document.createElement('div'); - child.style.width = '640px'; - child.style.height = '480px'; - element.appendChild(child); - - openmct.on('start', done); - openmct.start(appHolder); + Vue.nextTick(done); }); - afterEach(() => { - return resetApplicationState(openmct); - }); + it('displays an independent time conductor with saved options - local clock', () => { + return Vue.nextTick(() => { + const independentTimeConductorEl = element.querySelector( + '.c-timeline-holder > .c-conductor__controls' + ); + expect(independentTimeConductorEl).toBeDefined(); - let mockObject = { - name: 'Time Strip', - key: 'time-strip', - creatable: true + const independentTimeContext = openmct.time.getIndependentContext( + testViewObject.identifier.key + ); + expect(independentTimeContext.clockOffsets()).toEqual( + testViewObject.configuration.timeOptions.clockOffsets + ); + }); + }); + }); + + describe('the independent time conductor - fixed', () => { + let timelineView; + let testViewObject2 = { + ...timelineObject, + id: 'test-object2', + identifier: { + key: 'test-object2', + namespace: '' + }, + configuration: { + ...timelineObject.configuration, + useIndependentTime: true + } }; - it('defines a time-strip object type with the correct key', () => { - expect(objectDef.key).toEqual(mockObject.key); + beforeEach((done) => { + const applicableViews = openmct.objectViews.get(testViewObject2, mockObjectPath); + timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); + let view = timelineView.view(testViewObject2, mockObjectPath); + view.show(child, true); + + Vue.nextTick(done); }); - describe('the time-strip object', () => { - it('is creatable', () => { - expect(objectDef.creatable).toEqual(mockObject.creatable); - }); + it('displays an independent time conductor with saved options - fixed timespan', () => { + return Vue.nextTick(() => { + const independentTimeConductorEl = element.querySelector( + '.c-timeline-holder > .c-conductor__controls' + ); + expect(independentTimeConductorEl).toBeDefined(); + + const independentTimeContext = openmct.time.getIndependentContext( + testViewObject2.identifier.key + ); + expect(independentTimeContext.bounds()).toEqual( + testViewObject2.configuration.timeOptions.fixedOffsets + ); + }); + }); + }); + + describe('The timestrip composition policy', () => { + let testObject; + beforeEach(() => { + testObject = { + ...timelineObject, + composition: [] + }; }); - describe('the view', () => { - let timelineView; - let testViewObject; - - beforeEach(() => { - testViewObject = { - ...timelineObject - }; - - const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); - timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); - let view = timelineView.view(testViewObject, mockObjectPath); - view.show(child, true); - - return Vue.nextTick(); - }); - - it('provides a view', () => { - expect(timelineView).toBeDefined(); - }); - - it('displays a time axis', () => { - const el = element.querySelector('.c-timesystem-axis'); - expect(el).toBeDefined(); - }); - - it('does not show the independent time conductor based on configuration', () => { - const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls'); - expect(independentTimeConductorEl).toBeNull(); - }); - }); - - describe('the timeline composition', () => { - let timelineDomainObject; - let timelineView; - - beforeEach(() => { - timelineDomainObject = { - ...timelineObject, - composition: [ - { - identifier: { - key: 'test-plan-object', - namespace: '' - } - } - ] - }; - - mockCompositionForTimelist = new EventEmitter(); - mockCompositionForTimelist.load = () => { - mockCompositionForTimelist.emit('add', planObject); - - return [planObject]; - }; - - spyOn(openmct.composition, 'get').withArgs(timelineDomainObject).and.returnValue(mockCompositionForTimelist); - - openmct.router.path = [timelineDomainObject]; - - const applicableViews = openmct.objectViews.get(timelineDomainObject, [timelineDomainObject]); - timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); - let view = timelineView.view(timelineDomainObject, [timelineDomainObject]); - view.show(child, true); - - return Vue.nextTick(); - }); - - it('loads the plan from composition', () => { - return Vue.nextTick(() => { - const items = element.querySelectorAll('.js-timeline__content'); - expect(items.length).toEqual(1); - }); - }); - }); - - describe('the independent time conductor', () => { - let timelineView; - let testViewObject = { - ...timelineObject, - configuration: { - ...timelineObject.configuration, - useIndependentTime: true - } - }; - - beforeEach(done => { - const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); - timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); - let view = timelineView.view(testViewObject, mockObjectPath); - view.show(child, true); - - Vue.nextTick(done); - }); - - it('displays an independent time conductor with saved options - local clock', () => { - - return Vue.nextTick(() => { - const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls'); - expect(independentTimeConductorEl).toBeDefined(); - - const independentTimeContext = openmct.time.getIndependentContext(testViewObject.identifier.key); - expect(independentTimeContext.clockOffsets()).toEqual(testViewObject.configuration.timeOptions.clockOffsets); - }); - }); - }); - - describe('the independent time conductor - fixed', () => { - let timelineView; - let testViewObject2 = { - ...timelineObject, - id: "test-object2", - identifier: { - key: "test-object2", - namespace: '' + it('allows composition for plots', () => { + const testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'some-key', + name: 'Some attribute', + hints: { + domain: 1 + } }, - configuration: { - ...timelineObject.configuration, - useIndependentTime: true + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 1 + } } - }; - - beforeEach((done) => { - const applicableViews = openmct.objectViews.get(testViewObject2, mockObjectPath); - timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); - let view = timelineView.view(testViewObject2, mockObjectPath); - view.show(child, true); - - Vue.nextTick(done); - }); - - it('displays an independent time conductor with saved options - fixed timespan', () => { - return Vue.nextTick(() => { - const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls'); - expect(independentTimeConductorEl).toBeDefined(); - - const independentTimeContext = openmct.time.getIndependentContext(testViewObject2.identifier.key); - expect(independentTimeContext.bounds()).toEqual(testViewObject2.configuration.timeOptions.fixedOffsets); - }); - }); + ] + } + }; + const composition = openmct.composition.get(testObject); + expect(() => { + composition.add(testTelemetryObject); + }).not.toThrow(); + expect(testObject.composition.length).toBe(1); }); - describe("The timestrip composition policy", () => { - let testObject; - beforeEach(() => { - testObject = { - ...timelineObject, - composition: [] - }; - }); - - it("allows composition for plots", () => { - const testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "some-key", - name: "Some attribute", - hints: { - domain: 1 - } - }, { - key: "some-other-key", - name: "Another attribute", - hints: { - range: 1 - } - }] - } - }; - const composition = openmct.composition.get(testObject); - expect(() => { - composition.add(testTelemetryObject); - }).not.toThrow(); - expect(testObject.composition.length).toBe(1); - }); - - it("allows composition for plans", () => { - const composition = openmct.composition.get(testObject); - expect(() => { - composition.add(planObject); - }).not.toThrow(); - expect(testObject.composition.length).toBe(1); - }); - - it("disallows composition for non time-based plots", () => { - const barGraphObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "telemetry.plot.bar-graph", - name: "Test Object" - }; - const composition = openmct.composition.get(testObject); - expect(() => { - composition.add(barGraphObject); - }).toThrow(); - expect(testObject.composition.length).toBe(0); - }); + it('allows composition for plans', () => { + const composition = openmct.composition.get(testObject); + expect(() => { + composition.add(planObject); + }).not.toThrow(); + expect(testObject.composition.length).toBe(1); }); + + it('disallows composition for non time-based plots', () => { + const barGraphObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'telemetry.plot.bar-graph', + name: 'Test Object' + }; + const composition = openmct.composition.get(testObject); + expect(() => { + composition.add(barGraphObject); + }).toThrow(); + expect(testObject.composition.length).toBe(0); + }); + }); }); diff --git a/src/plugins/timeline/timeline.scss b/src/plugins/timeline/timeline.scss index 537fdc3384..bd5e3f6d5a 100644 --- a/src/plugins/timeline/timeline.scss +++ b/src/plugins/timeline/timeline.scss @@ -1,12 +1,12 @@ .c-timeline-holder { - overflow: hidden; + overflow: hidden; } .c-plan.c-timeline-holder { - overflow-x: hidden; - overflow-y: auto; + overflow-x: hidden; + overflow-y: auto; } .c-timeline__objects { - display: contents; + display: contents; } diff --git a/src/plugins/timeline/timelineInterceptor.js b/src/plugins/timeline/timelineInterceptor.js index 3bb75f2140..631b1ade94 100644 --- a/src/plugins/timeline/timelineInterceptor.js +++ b/src/plugins/timeline/timelineInterceptor.js @@ -21,20 +21,18 @@ *****************************************************************************/ export default function timelineInterceptor(openmct) { + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return domainObject && domainObject.type === 'time-strip'; + }, + invoke: (identifier, object) => { + if (object && object.configuration === undefined) { + object.configuration = { + useIndependentTime: true + }; + } - openmct.objects.addGetInterceptor({ - appliesTo: (identifier, domainObject) => { - return domainObject && domainObject.type === 'time-strip'; - }, - invoke: (identifier, object) => { - - if (object && object.configuration === undefined) { - object.configuration = { - useIndependentTime: true - }; - } - - return object; - } - }); + return object; + } + }); } diff --git a/src/plugins/timelist/Timelist.vue b/src/plugins/timelist/Timelist.vue index 825c04e57a..54c392571c 100644 --- a/src/plugins/timelist/Timelist.vue +++ b/src/plugins/timelist/Timelist.vue @@ -21,475 +21,499 @@ --> diff --git a/src/plugins/timelist/TimelistCompositionPolicy.js b/src/plugins/timelist/TimelistCompositionPolicy.js index e3695c85ce..89dccfbaa4 100644 --- a/src/plugins/timelist/TimelistCompositionPolicy.js +++ b/src/plugins/timelist/TimelistCompositionPolicy.js @@ -19,16 +19,16 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import {TIMELIST_TYPE} from "@/plugins/timelist/constants"; +import { TIMELIST_TYPE } from '@/plugins/timelist/constants'; export default function TimelistCompositionPolicy(openmct) { - return { - allow: function (parent, child) { - if (parent.type === TIMELIST_TYPE && child.type !== 'plan') { - return false; - } + return { + allow: function (parent, child) { + if (parent.type === TIMELIST_TYPE && child.type !== 'plan') { + return false; + } - return true; - } - }; + return true; + } + }; } diff --git a/src/plugins/timelist/TimelistViewProvider.js b/src/plugins/timelist/TimelistViewProvider.js index 03d38d043e..65c1d54725 100644 --- a/src/plugins/timelist/TimelistViewProvider.js +++ b/src/plugins/timelist/TimelistViewProvider.js @@ -25,44 +25,42 @@ import { TIMELIST_TYPE } from './constants'; import Vue from 'vue'; export default function TimelistViewProvider(openmct) { + return { + key: 'timelist.view', + name: 'Time List', + cssClass: 'icon-timelist', + canView(domainObject) { + return domainObject.type === TIMELIST_TYPE; + }, - return { - key: 'timelist.view', - name: 'Time List', - cssClass: 'icon-timelist', - canView(domainObject) { - return domainObject.type === TIMELIST_TYPE; + canEdit(domainObject) { + return domainObject.type === TIMELIST_TYPE; + }, + + view: function (domainObject, objectPath) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + Timelist + }, + provide: { + openmct, + domainObject, + path: objectPath, + composition: openmct.composition.get(domainObject) + }, + template: '' + }); }, - - canEdit(domainObject) { - return domainObject.type === TIMELIST_TYPE; - }, - - view: function (domainObject, objectPath) { - let component; - - return { - show: function (element) { - - component = new Vue({ - el: element, - components: { - Timelist - }, - provide: { - openmct, - domainObject, - path: objectPath, - composition: openmct.composition.get(domainObject) - }, - template: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/timelist/constants.js b/src/plugins/timelist/constants.js index 7d9c978c06..0849e34bca 100644 --- a/src/plugins/timelist/constants.js +++ b/src/plugins/timelist/constants.js @@ -1,24 +1,24 @@ export const SORT_ORDER_OPTIONS = [ - { - label: 'Start ascending', - property: 'start', - direction: 'ASC' - }, - { - label: 'Start descending', - property: 'start', - direction: 'DESC' - }, - { - label: 'End ascending', - property: 'end', - direction: 'ASC' - }, - { - label: 'End descending', - property: 'end', - direction: 'DESC' - } + { + label: 'Start ascending', + property: 'start', + direction: 'ASC' + }, + { + label: 'Start descending', + property: 'start', + direction: 'DESC' + }, + { + label: 'End ascending', + property: 'end', + direction: 'ASC' + }, + { + label: 'End descending', + property: 'end', + direction: 'DESC' + } ]; export const TIMELIST_TYPE = 'timelist'; diff --git a/src/plugins/timelist/inspector/EventProperties.vue b/src/plugins/timelist/inspector/EventProperties.vue index 691fc6bed5..1a79b67052 100644 --- a/src/plugins/timelist/inspector/EventProperties.vue +++ b/src/plugins/timelist/inspector/EventProperties.vue @@ -20,126 +20,106 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timelist/inspector/Filtering.vue b/src/plugins/timelist/inspector/Filtering.vue index ad5a3d77c5..f302fe59b0 100644 --- a/src/plugins/timelist/inspector/Filtering.vue +++ b/src/plugins/timelist/inspector/Filtering.vue @@ -20,93 +20,82 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timelist/inspector/TimeListInspectorViewProvider.js b/src/plugins/timelist/inspector/TimeListInspectorViewProvider.js index e79eb239c0..3b9c0e0517 100644 --- a/src/plugins/timelist/inspector/TimeListInspectorViewProvider.js +++ b/src/plugins/timelist/inspector/TimeListInspectorViewProvider.js @@ -20,51 +20,50 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import TimelistPropertiesView from "./TimelistPropertiesView.vue"; +import TimelistPropertiesView from './TimelistPropertiesView.vue'; import { TIMELIST_TYPE } from '../constants'; import Vue from 'vue'; export default function TimeListInspectorViewProvider(openmct) { - return { - key: 'timelist-inspector', - name: 'Timelist Inspector View', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: 'timelist-inspector', + name: 'Timelist Inspector View', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - let context = selection[0][0].context; + let context = selection[0][0].context; - return context && context.item - && context.item.type === TIMELIST_TYPE; + return context && context.item && context.item.type === TIMELIST_TYPE; + }, + view: function (selection) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + TimelistPropertiesView: TimelistPropertiesView + }, + provide: { + openmct, + domainObject: selection[0][0].context.item + }, + template: '' + }); }, - view: function (selection) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - TimelistPropertiesView: TimelistPropertiesView - }, - provide: { - openmct, - domainObject: selection[0][0].context.item - }, - template: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/timelist/inspector/TimelistPropertiesView.vue b/src/plugins/timelist/inspector/TimelistPropertiesView.vue index 88a2e41ed8..8d7e01860c 100644 --- a/src/plugins/timelist/inspector/TimelistPropertiesView.vue +++ b/src/plugins/timelist/inspector/TimelistPropertiesView.vue @@ -21,126 +21,111 @@ --> diff --git a/src/plugins/timelist/plugin.js b/src/plugins/timelist/plugin.js index 3dc59642c0..9de9becb2b 100644 --- a/src/plugins/timelist/plugin.js +++ b/src/plugins/timelist/plugin.js @@ -22,37 +22,37 @@ import TimelistViewProvider from './TimelistViewProvider'; import { TIMELIST_TYPE } from './constants'; -import TimeListInspectorViewProvider from "./inspector/TimeListInspectorViewProvider"; -import TimelistCompositionPolicy from "@/plugins/timelist/TimelistCompositionPolicy"; +import TimeListInspectorViewProvider from './inspector/TimeListInspectorViewProvider'; +import TimelistCompositionPolicy from '@/plugins/timelist/TimelistCompositionPolicy'; export default function () { - return function install(openmct) { - openmct.types.addType(TIMELIST_TYPE, { - name: 'Time List', - key: TIMELIST_TYPE, - description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.', - creatable: true, - cssClass: 'icon-timelist', - initialize: function (domainObject) { - domainObject.configuration = { - sortOrderIndex: 0, - futureEventsIndex: 1, - futureEventsDurationIndex: 0, - futureEventsDuration: 20, - currentEventsIndex: 1, - currentEventsDurationIndex: 0, - currentEventsDuration: 20, - pastEventsIndex: 1, - pastEventsDurationIndex: 0, - pastEventsDuration: 20, - filter: '' - }; - domainObject.composition = []; - } - }); - openmct.objectViews.addProvider(new TimelistViewProvider(openmct)); - openmct.inspectorViews.addProvider(new TimeListInspectorViewProvider(openmct)); - openmct.composition.addPolicy(new TimelistCompositionPolicy(openmct).allow); - - }; + return function install(openmct) { + openmct.types.addType(TIMELIST_TYPE, { + name: 'Time List', + key: TIMELIST_TYPE, + description: + 'A configurable, time-ordered list view of activities for a compatible mission plan file.', + creatable: true, + cssClass: 'icon-timelist', + initialize: function (domainObject) { + domainObject.configuration = { + sortOrderIndex: 0, + futureEventsIndex: 1, + futureEventsDurationIndex: 0, + futureEventsDuration: 20, + currentEventsIndex: 1, + currentEventsDurationIndex: 0, + currentEventsDuration: 20, + pastEventsIndex: 1, + pastEventsDurationIndex: 0, + pastEventsDuration: 20, + filter: '' + }; + domainObject.composition = []; + } + }); + openmct.objectViews.addProvider(new TimelistViewProvider(openmct)); + openmct.inspectorViews.addProvider(new TimeListInspectorViewProvider(openmct)); + openmct.composition.addPolicy(new TimelistCompositionPolicy(openmct).allow); + }; } diff --git a/src/plugins/timelist/pluginSpec.js b/src/plugins/timelist/pluginSpec.js index 481be82f52..c4633e32f7 100644 --- a/src/plugins/timelist/pluginSpec.js +++ b/src/plugins/timelist/pluginSpec.js @@ -20,364 +20,378 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createOpenMct, resetApplicationState} from "utils/testing"; -import TimelistPlugin from "./plugin"; -import { TIMELIST_TYPE } from "./constants"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import TimelistPlugin from './plugin'; +import { TIMELIST_TYPE } from './constants'; import Vue from 'vue'; -import moment from "moment"; -import EventEmitter from "EventEmitter"; +import moment from 'moment'; +import EventEmitter from 'EventEmitter'; const LIST_ITEM_CLASS = '.js-table__body .js-list-item'; const LIST_ITEM_VALUE_CLASS = '.js-list-item__value'; const LIST_ITEM_BODY_CLASS = '.js-table__body th'; describe('the plugin', function () { - let timelistDefinition; - let element; - let child; - let openmct; - let appHolder; - let originalRouterPath; - let mockComposition; - let now = Date.now(); - let twoHoursPast = now - (1000 * 60 * 60 * 2); - let oneHourPast = now - (1000 * 60 * 60); - let twoHoursFuture = now + (1000 * 60 * 60 * 2); - let planObject = { + let timelistDefinition; + let element; + let child; + let openmct; + let appHolder; + let originalRouterPath; + let mockComposition; + let now = Date.now(); + let twoHoursPast = now - 1000 * 60 * 60 * 2; + let oneHourPast = now - 1000 * 60 * 60; + let twoHoursFuture = now + 1000 * 60 * 60 * 2; + let planObject = { + identifier: { + key: 'test-plan-object', + namespace: '' + }, + type: 'plan', + id: 'test-plan-object', + selectFile: { + body: JSON.stringify({ + 'TEST-GROUP': [ + { + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', + start: twoHoursPast, + end: oneHourPast, + type: 'TEST-GROUP', + color: 'fuchsia', + textColor: 'black' + }, + { + name: 'Sed ut perspiciatis', + start: now, + end: twoHoursFuture, + type: 'TEST-GROUP', + color: 'fuchsia', + textColor: 'black' + } + ] + }) + } + }; + + beforeEach((done) => { + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + + openmct = createOpenMct(); + openmct.install(new TimelistPlugin()); + + timelistDefinition = openmct.types.get(TIMELIST_TYPE).definition; + + element = document.createElement('div'); + element.style.width = '640px'; + element.style.height = '480px'; + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + element.appendChild(child); + + originalRouterPath = openmct.router.path; + + mockComposition = new EventEmitter(); + // eslint-disable-next-line require-await + mockComposition.load = async () => { + return [planObject]; + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + openmct.on('start', done); + openmct.start(appHolder); + }); + + afterEach(() => { + openmct.router.path = originalRouterPath; + + return resetApplicationState(openmct); + }); + + let mockTimelistObject = { + name: 'Timelist', + key: TIMELIST_TYPE, + creatable: true + }; + + it('defines a timelist object type with the correct key', () => { + expect(timelistDefinition.key).toEqual(mockTimelistObject.key); + }); + + it('is creatable', () => { + expect(timelistDefinition.creatable).toEqual(mockTimelistObject.creatable); + }); + + describe('the timelist view', () => { + it('provides a timelist view', () => { + const testViewObject = { + id: 'test-object', + type: TIMELIST_TYPE + }; + openmct.router.path = [testViewObject]; + + const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); + let timelistView = applicableViews.find( + (viewProvider) => viewProvider.key === 'timelist.view' + ); + expect(timelistView).toBeDefined(); + }); + }); + + describe('the timelist view displays activities', () => { + let timelistDomainObject; + let timelistView; + + beforeEach(() => { + timelistDomainObject = { identifier: { - key: 'test-plan-object', - namespace: '' + key: 'test-object', + namespace: '' + }, + type: TIMELIST_TYPE, + id: 'test-object', + configuration: { + sortOrderIndex: 0, + futureEventsIndex: 1, + futureEventsDurationIndex: 0, + futureEventsDuration: 0, + currentEventsIndex: 1, + currentEventsDurationIndex: 0, + currentEventsDuration: 0, + pastEventsIndex: 1, + pastEventsDurationIndex: 0, + pastEventsDuration: 0, + filter: '' }, - type: 'plan', - id: "test-plan-object", selectFile: { - body: JSON.stringify({ - "TEST-GROUP": [ - { - "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", - "start": twoHoursPast, - "end": oneHourPast, - "type": "TEST-GROUP", - "color": "fuchsia", - "textColor": "black" - }, - { - "name": "Sed ut perspiciatis", - "start": now, - "end": twoHoursFuture, - "type": "TEST-GROUP", - "color": "fuchsia", - "textColor": "black" - } - ] - }) + body: JSON.stringify({ + 'TEST-GROUP': [ + { + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', + start: twoHoursPast, + end: oneHourPast, + type: 'TEST-GROUP', + color: 'fuchsia', + textColor: 'black' + }, + { + name: 'Sed ut perspiciatis', + start: now, + end: twoHoursFuture, + type: 'TEST-GROUP', + color: 'fuchsia', + textColor: 'black' + } + ] + }) } - }; + }; - beforeEach((done) => { - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; + openmct.router.path = [timelistDomainObject]; - openmct = createOpenMct(); - openmct.install(new TimelistPlugin()); + const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]); + timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); + let view = timelistView.view(timelistDomainObject, []); + view.show(child, true); - timelistDefinition = openmct.types.get(TIMELIST_TYPE).definition; - - element = document.createElement('div'); - element.style.width = '640px'; - element.style.height = '480px'; - child = document.createElement('div'); - child.style.width = '640px'; - child.style.height = '480px'; - element.appendChild(child); - - originalRouterPath = openmct.router.path; - - mockComposition = new EventEmitter(); - // eslint-disable-next-line require-await - mockComposition.load = async () => { - return [planObject]; - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - openmct.on('start', done); - openmct.start(appHolder); + return Vue.nextTick(); }); - afterEach(() => { - openmct.router.path = originalRouterPath; - - return resetApplicationState(openmct); + it('displays the activities', () => { + const items = element.querySelectorAll(LIST_ITEM_CLASS); + expect(items.length).toEqual(2); }); - let mockTimelistObject = { - name: 'Timelist', - key: TIMELIST_TYPE, - creatable: true - }; - - it('defines a timelist object type with the correct key', () => { - expect(timelistDefinition.key).toEqual(mockTimelistObject.key); + it('displays the activity headers', () => { + const headers = element.querySelectorAll(LIST_ITEM_BODY_CLASS); + expect(headers.length).toEqual(4); }); - it('is creatable', () => { - expect(timelistDefinition.creatable).toEqual(mockTimelistObject.creatable); + it('displays activity details', (done) => { + Vue.nextTick(() => { + const itemEls = element.querySelectorAll(LIST_ITEM_CLASS); + const itemValues = itemEls[0].querySelectorAll(LIST_ITEM_VALUE_CLASS); + expect(itemValues.length).toEqual(4); + expect(itemValues[3].innerHTML.trim()).toEqual( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua' + ); + expect(itemValues[0].innerHTML.trim()).toEqual( + `${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z` + ); + expect(itemValues[1].innerHTML.trim()).toEqual( + `${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z` + ); + + done(); + }); + }); + }); + + describe('the timelist composition', () => { + let timelistDomainObject; + let timelistView; + + beforeEach(() => { + timelistDomainObject = { + identifier: { + key: 'test-object', + namespace: '' + }, + type: TIMELIST_TYPE, + id: 'test-object', + configuration: { + sortOrderIndex: 0, + futureEventsIndex: 1, + futureEventsDurationIndex: 0, + futureEventsDuration: 0, + currentEventsIndex: 1, + currentEventsDurationIndex: 0, + currentEventsDuration: 0, + pastEventsIndex: 1, + pastEventsDurationIndex: 0, + pastEventsDuration: 0, + filter: '' + }, + composition: [ + { + identifier: { + key: 'test-plan-object', + namespace: '' + } + } + ] + }; + + openmct.router.path = [timelistDomainObject]; + + const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]); + timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); + let view = timelistView.view(timelistDomainObject, [timelistDomainObject]); + view.show(child, true); + + return Vue.nextTick(); }); - describe('the timelist view', () => { - it('provides a timelist view', () => { - const testViewObject = { - id: "test-object", - type: TIMELIST_TYPE - }; - openmct.router.path = [testViewObject]; + it('loads the plan from composition', () => { + mockComposition.emit('add', planObject); - const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); - let timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); - expect(timelistView).toBeDefined(); - }); + return Vue.nextTick(() => { + const items = element.querySelectorAll(LIST_ITEM_CLASS); + expect(items.length).toEqual(2); + }); + }); + }); + + describe('filters', () => { + let timelistDomainObject; + let timelistView; + + beforeEach(() => { + timelistDomainObject = { + identifier: { + key: 'test-object', + namespace: '' + }, + type: TIMELIST_TYPE, + id: 'test-object', + configuration: { + sortOrderIndex: 0, + futureEventsIndex: 1, + futureEventsDurationIndex: 0, + futureEventsDuration: 0, + currentEventsIndex: 1, + currentEventsDurationIndex: 0, + currentEventsDuration: 0, + pastEventsIndex: 1, + pastEventsDurationIndex: 0, + pastEventsDuration: 0, + filter: 'perspiciatis' + }, + composition: [ + { + identifier: { + key: 'test-plan-object', + namespace: '' + } + } + ] + }; + + openmct.router.path = [timelistDomainObject]; + + const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]); + timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); + let view = timelistView.view(timelistDomainObject, [timelistDomainObject]); + view.show(child, true); + + return Vue.nextTick(); }); - describe('the timelist view displays activities', () => { - let timelistDomainObject; - let timelistView; + it('activities', () => { + mockComposition.emit('add', planObject); - beforeEach(() => { - timelistDomainObject = { - identifier: { - key: 'test-object', - namespace: '' - }, - type: TIMELIST_TYPE, - id: "test-object", - configuration: { - sortOrderIndex: 0, - futureEventsIndex: 1, - futureEventsDurationIndex: 0, - futureEventsDuration: 0, - currentEventsIndex: 1, - currentEventsDurationIndex: 0, - currentEventsDuration: 0, - pastEventsIndex: 1, - pastEventsDurationIndex: 0, - pastEventsDuration: 0, - filter: '' - }, - selectFile: { - body: JSON.stringify({ - "TEST-GROUP": [ - { - "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", - "start": twoHoursPast, - "end": oneHourPast, - "type": "TEST-GROUP", - "color": "fuchsia", - "textColor": "black" - }, - { - "name": "Sed ut perspiciatis", - "start": now, - "end": twoHoursFuture, - "type": "TEST-GROUP", - "color": "fuchsia", - "textColor": "black" - } - ] - }) - } - }; + return Vue.nextTick(() => { + const items = element.querySelectorAll(LIST_ITEM_CLASS); + expect(items.length).toEqual(1); + }); + }); + }); - openmct.router.path = [timelistDomainObject]; + describe('time filtering - past', () => { + let timelistDomainObject; + let timelistView; - const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]); - timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); - let view = timelistView.view(timelistDomainObject, []); - view.show(child, true); + beforeEach(() => { + timelistDomainObject = { + identifier: { + key: 'test-object', + namespace: '' + }, + type: TIMELIST_TYPE, + id: 'test-object', + configuration: { + sortOrderIndex: 0, + futureEventsIndex: 1, + futureEventsDurationIndex: 0, + futureEventsDuration: 0, + currentEventsIndex: 1, + currentEventsDurationIndex: 0, + currentEventsDuration: 0, + pastEventsIndex: 0, + pastEventsDurationIndex: 0, + pastEventsDuration: 0, + filter: '' + }, + composition: [ + { + identifier: { + key: 'test-plan-object', + namespace: '' + } + } + ] + }; - return Vue.nextTick(); - }); + openmct.router.path = [timelistDomainObject]; - it('displays the activities', () => { - const items = element.querySelectorAll(LIST_ITEM_CLASS); - expect(items.length).toEqual(2); - }); + const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]); + timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); + let view = timelistView.view(timelistDomainObject, [timelistDomainObject]); + view.show(child, true); - it('displays the activity headers', () => { - const headers = element.querySelectorAll(LIST_ITEM_BODY_CLASS); - expect(headers.length).toEqual(4); - }); - - it('displays activity details', (done) => { - Vue.nextTick(() => { - const itemEls = element.querySelectorAll(LIST_ITEM_CLASS); - const itemValues = itemEls[0].querySelectorAll(LIST_ITEM_VALUE_CLASS); - expect(itemValues.length).toEqual(4); - expect(itemValues[3].innerHTML.trim()).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'); - expect(itemValues[0].innerHTML.trim()).toEqual(`${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`); - expect(itemValues[1].innerHTML.trim()).toEqual(`${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`); - - done(); - }); - }); + return Vue.nextTick(); }); - describe('the timelist composition', () => { - let timelistDomainObject; - let timelistView; + it('hides past events', () => { + mockComposition.emit('add', planObject); - beforeEach(() => { - timelistDomainObject = { - identifier: { - key: 'test-object', - namespace: '' - }, - type: TIMELIST_TYPE, - id: "test-object", - configuration: { - sortOrderIndex: 0, - futureEventsIndex: 1, - futureEventsDurationIndex: 0, - futureEventsDuration: 0, - currentEventsIndex: 1, - currentEventsDurationIndex: 0, - currentEventsDuration: 0, - pastEventsIndex: 1, - pastEventsDurationIndex: 0, - pastEventsDuration: 0, - filter: '' - }, - composition: [{ - identifier: { - key: 'test-plan-object', - namespace: '' - } - }] - }; - - openmct.router.path = [timelistDomainObject]; - - const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]); - timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); - let view = timelistView.view(timelistDomainObject, [timelistDomainObject]); - view.show(child, true); - - return Vue.nextTick(); - }); - - it('loads the plan from composition', () => { - mockComposition.emit('add', planObject); - - return Vue.nextTick(() => { - const items = element.querySelectorAll(LIST_ITEM_CLASS); - expect(items.length).toEqual(2); - }); - }); - }); - - describe('filters', () => { - let timelistDomainObject; - let timelistView; - - beforeEach(() => { - timelistDomainObject = { - identifier: { - key: 'test-object', - namespace: '' - }, - type: TIMELIST_TYPE, - id: "test-object", - configuration: { - sortOrderIndex: 0, - futureEventsIndex: 1, - futureEventsDurationIndex: 0, - futureEventsDuration: 0, - currentEventsIndex: 1, - currentEventsDurationIndex: 0, - currentEventsDuration: 0, - pastEventsIndex: 1, - pastEventsDurationIndex: 0, - pastEventsDuration: 0, - filter: 'perspiciatis' - }, - composition: [{ - identifier: { - key: 'test-plan-object', - namespace: '' - } - }] - }; - - openmct.router.path = [timelistDomainObject]; - - const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]); - timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); - let view = timelistView.view(timelistDomainObject, [timelistDomainObject]); - view.show(child, true); - - return Vue.nextTick(); - }); - - it('activities', () => { - mockComposition.emit('add', planObject); - - return Vue.nextTick(() => { - const items = element.querySelectorAll(LIST_ITEM_CLASS); - expect(items.length).toEqual(1); - }); - }); - }); - - describe('time filtering - past', () => { - let timelistDomainObject; - let timelistView; - - beforeEach(() => { - timelistDomainObject = { - identifier: { - key: 'test-object', - namespace: '' - }, - type: TIMELIST_TYPE, - id: "test-object", - configuration: { - sortOrderIndex: 0, - futureEventsIndex: 1, - futureEventsDurationIndex: 0, - futureEventsDuration: 0, - currentEventsIndex: 1, - currentEventsDurationIndex: 0, - currentEventsDuration: 0, - pastEventsIndex: 0, - pastEventsDurationIndex: 0, - pastEventsDuration: 0, - filter: '' - }, - composition: [{ - identifier: { - key: 'test-plan-object', - namespace: '' - } - }] - }; - - openmct.router.path = [timelistDomainObject]; - - const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]); - timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); - let view = timelistView.view(timelistDomainObject, [timelistDomainObject]); - view.show(child, true); - - return Vue.nextTick(); - }); - - it('hides past events', () => { - mockComposition.emit('add', planObject); - - return Vue.nextTick(() => { - const items = element.querySelectorAll(LIST_ITEM_CLASS); - expect(items.length).toEqual(2); - }); - }); + return Vue.nextTick(() => { + const items = element.querySelectorAll(LIST_ITEM_CLASS); + expect(items.length).toEqual(2); + }); }); + }); }); diff --git a/src/plugins/timelist/timelist.scss b/src/plugins/timelist/timelist.scss index 652e105cfe..af66e0509b 100644 --- a/src/plugins/timelist/timelist.scss +++ b/src/plugins/timelist/timelist.scss @@ -57,5 +57,4 @@ } } } - } diff --git a/src/plugins/timer/TimerViewProvider.js b/src/plugins/timer/TimerViewProvider.js index 608441056a..8d54b7baf1 100644 --- a/src/plugins/timer/TimerViewProvider.js +++ b/src/plugins/timer/TimerViewProvider.js @@ -24,42 +24,42 @@ import Timer from './components/Timer.vue'; import Vue from 'vue'; export default function TimerViewProvider(openmct) { - return { - key: 'timer.view', - name: 'Timer', - cssClass: 'icon-timer', - canView(domainObject) { - return domainObject.type === 'timer'; + return { + key: 'timer.view', + name: 'Timer', + cssClass: 'icon-timer', + canView(domainObject) { + return domainObject.type === 'timer'; + }, + + view: function (domainObject, objectPath) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + Timer + }, + provide: { + openmct, + objectPath, + currentView: this + }, + data() { + return { + domainObject + }; + }, + template: '' + }); }, - - view: function (domainObject, objectPath) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - Timer - }, - provide: { - openmct, - objectPath, - currentView: this - }, - data() { - return { - domainObject - }; - }, - template: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/timer/actions/PauseTimerAction.js b/src/plugins/timer/actions/PauseTimerAction.js index 7839b13f66..843fcde21a 100644 --- a/src/plugins/timer/actions/PauseTimerAction.js +++ b/src/plugins/timer/actions/PauseTimerAction.js @@ -21,41 +21,41 @@ *****************************************************************************/ export default class PauseTimerAction { - constructor(openmct) { - this.name = 'Pause'; - this.key = 'timer.pause'; - this.description = 'Pause the currently displayed timer'; - this.group = 'view'; - this.cssClass = 'icon-pause'; - this.priority = 3; + constructor(openmct) { + this.name = 'Pause'; + this.key = 'timer.pause'; + this.description = 'Pause the currently displayed timer'; + this.group = 'view'; + this.cssClass = 'icon-pause'; + this.priority = 3; - this.openmct = openmct; + this.openmct = openmct; + } + invoke(objectPath) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return new Error('Unable to run pause timer action. No domainObject provided.'); } - invoke(objectPath) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return new Error('Unable to run pause timer action. No domainObject provided.'); - } - const newConfiguration = { ...domainObject.configuration }; - newConfiguration.timerState = 'paused'; - newConfiguration.pausedTime = new Date(); + const newConfiguration = { ...domainObject.configuration }; + newConfiguration.timerState = 'paused'; + newConfiguration.pausedTime = new Date(); - this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + } + appliesTo(objectPath, view = {}) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return; } - appliesTo(objectPath, view = {}) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return; - } - // Use object configuration timerState for viewless context menus, - // otherwise manually show/hide based on the view's timerState - const viewKey = view.key; - const { timerState } = domainObject.configuration; + // Use object configuration timerState for viewless context menus, + // otherwise manually show/hide based on the view's timerState + const viewKey = view.key; + const { timerState } = domainObject.configuration; - return viewKey - ? domainObject.type === 'timer' - : domainObject.type === 'timer' && timerState === 'started'; - } + return viewKey + ? domainObject.type === 'timer' + : domainObject.type === 'timer' && timerState === 'started'; + } } diff --git a/src/plugins/timer/actions/RestartTimerAction.js b/src/plugins/timer/actions/RestartTimerAction.js index dcfe95a508..8c05003401 100644 --- a/src/plugins/timer/actions/RestartTimerAction.js +++ b/src/plugins/timer/actions/RestartTimerAction.js @@ -21,42 +21,42 @@ *****************************************************************************/ export default class RestartTimerAction { - constructor(openmct) { - this.name = 'Restart at 0'; - this.key = 'timer.restart'; - this.description = 'Restart the currently displayed timer'; - this.group = 'view'; - this.cssClass = 'icon-refresh'; - this.priority = 2; + constructor(openmct) { + this.name = 'Restart at 0'; + this.key = 'timer.restart'; + this.description = 'Restart the currently displayed timer'; + this.group = 'view'; + this.cssClass = 'icon-refresh'; + this.priority = 2; - this.openmct = openmct; + this.openmct = openmct; + } + invoke(objectPath) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return new Error('Unable to run restart timer action. No domainObject provided.'); } - invoke(objectPath) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return new Error('Unable to run restart timer action. No domainObject provided.'); - } - const newConfiguration = { ...domainObject.configuration }; - newConfiguration.timerState = 'started'; - newConfiguration.timestamp = new Date(); - newConfiguration.pausedTime = undefined; + const newConfiguration = { ...domainObject.configuration }; + newConfiguration.timerState = 'started'; + newConfiguration.timestamp = new Date(); + newConfiguration.pausedTime = undefined; - this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + } + appliesTo(objectPath, view = {}) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return; } - appliesTo(objectPath, view = {}) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return; - } - // Use object configuration timerState for viewless context menus, - // otherwise manually show/hide based on the view's timerState - const viewKey = view.key; - const { timerState } = domainObject.configuration; + // Use object configuration timerState for viewless context menus, + // otherwise manually show/hide based on the view's timerState + const viewKey = view.key; + const { timerState } = domainObject.configuration; - return viewKey - ? domainObject.type === 'timer' - : domainObject.type === 'timer' && timerState !== 'stopped'; - } + return viewKey + ? domainObject.type === 'timer' + : domainObject.type === 'timer' && timerState !== 'stopped'; + } } diff --git a/src/plugins/timer/actions/StartTimerAction.js b/src/plugins/timer/actions/StartTimerAction.js index 6d520d0cc1..f200773952 100644 --- a/src/plugins/timer/actions/StartTimerAction.js +++ b/src/plugins/timer/actions/StartTimerAction.js @@ -23,59 +23,59 @@ import moment from 'moment'; export default class StartTimerAction { - constructor(openmct) { - this.name = 'Start'; - this.key = 'timer.start'; - this.description = 'Start the currently displayed timer'; - this.group = 'view'; - this.cssClass = 'icon-play'; - this.priority = 3; + constructor(openmct) { + this.name = 'Start'; + this.key = 'timer.start'; + this.description = 'Start the currently displayed timer'; + this.group = 'view'; + this.cssClass = 'icon-play'; + this.priority = 3; - this.openmct = openmct; + this.openmct = openmct; + } + invoke(objectPath) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return new Error('Unable to run start timer action. No domainObject provided.'); } - invoke(objectPath) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return new Error('Unable to run start timer action. No domainObject provided.'); - } - let { pausedTime, timestamp } = domainObject.configuration; - const newConfiguration = { ...domainObject.configuration }; + let { pausedTime, timestamp } = domainObject.configuration; + const newConfiguration = { ...domainObject.configuration }; - if (pausedTime) { - pausedTime = moment(pausedTime); - } - - if (timestamp) { - timestamp = moment(timestamp); - } - - const now = moment(new Date()); - if (pausedTime) { - const timeShift = moment.duration(now.diff(pausedTime)); - const shiftedTime = timestamp.add(timeShift); - newConfiguration.timestamp = shiftedTime.toDate(); - } else if (!timestamp) { - newConfiguration.timestamp = now.toDate(); - } - - newConfiguration.timerState = 'started'; - newConfiguration.pausedTime = undefined; - this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + if (pausedTime) { + pausedTime = moment(pausedTime); } - appliesTo(objectPath, view = {}) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return; - } - // Use object configuration timerState for viewless context menus, - // otherwise manually show/hide based on the view's timerState - const viewKey = view.key; - const { timerState } = domainObject.configuration; - - return viewKey - ? domainObject.type === 'timer' - : domainObject.type === 'timer' && timerState !== 'started'; + if (timestamp) { + timestamp = moment(timestamp); } + + const now = moment(new Date()); + if (pausedTime) { + const timeShift = moment.duration(now.diff(pausedTime)); + const shiftedTime = timestamp.add(timeShift); + newConfiguration.timestamp = shiftedTime.toDate(); + } else if (!timestamp) { + newConfiguration.timestamp = now.toDate(); + } + + newConfiguration.timerState = 'started'; + newConfiguration.pausedTime = undefined; + this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + } + appliesTo(objectPath, view = {}) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return; + } + + // Use object configuration timerState for viewless context menus, + // otherwise manually show/hide based on the view's timerState + const viewKey = view.key; + const { timerState } = domainObject.configuration; + + return viewKey + ? domainObject.type === 'timer' + : domainObject.type === 'timer' && timerState !== 'started'; + } } diff --git a/src/plugins/timer/actions/StopTimerAction.js b/src/plugins/timer/actions/StopTimerAction.js index ca29cc6125..7514bbb618 100644 --- a/src/plugins/timer/actions/StopTimerAction.js +++ b/src/plugins/timer/actions/StopTimerAction.js @@ -21,42 +21,42 @@ *****************************************************************************/ export default class StopTimerAction { - constructor(openmct) { - this.name = 'Stop'; - this.key = 'timer.stop'; - this.description = 'Stop the currently displayed timer'; - this.group = 'view'; - this.cssClass = 'icon-box-round-corners'; - this.priority = 1; + constructor(openmct) { + this.name = 'Stop'; + this.key = 'timer.stop'; + this.description = 'Stop the currently displayed timer'; + this.group = 'view'; + this.cssClass = 'icon-box-round-corners'; + this.priority = 1; - this.openmct = openmct; + this.openmct = openmct; + } + invoke(objectPath) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return new Error('Unable to run stop timer action. No domainObject provided.'); } - invoke(objectPath) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return new Error('Unable to run stop timer action. No domainObject provided.'); - } - const newConfiguration = { ...domainObject.configuration }; - newConfiguration.timerState = 'stopped'; - newConfiguration.timestamp = undefined; - newConfiguration.pausedTime = undefined; + const newConfiguration = { ...domainObject.configuration }; + newConfiguration.timerState = 'stopped'; + newConfiguration.timestamp = undefined; + newConfiguration.pausedTime = undefined; - this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + } + appliesTo(objectPath, view = {}) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return; } - appliesTo(objectPath, view = {}) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return; - } - // Use object configuration timerState for viewless context menus, - // otherwise manually show/hide based on the view's timerState - const viewKey = view.key; - const { timerState } = domainObject.configuration; + // Use object configuration timerState for viewless context menus, + // otherwise manually show/hide based on the view's timerState + const viewKey = view.key; + const { timerState } = domainObject.configuration; - return viewKey - ? domainObject.type === 'timer' - : domainObject.type === 'timer' && timerState !== 'stopped'; - } + return viewKey + ? domainObject.type === 'timer' + : domainObject.type === 'timer' && timerState !== 'stopped'; + } } diff --git a/src/plugins/timer/components/Timer.vue b/src/plugins/timer/components/Timer.vue index 09a13f7d84..65636feb3a 100644 --- a/src/plugins/timer/components/Timer.vue +++ b/src/plugins/timer/components/Timer.vue @@ -21,242 +21,239 @@ --> diff --git a/src/plugins/timer/plugin.js b/src/plugins/timer/plugin.js index b0079c07b0..9081d6653e 100644 --- a/src/plugins/timer/plugin.js +++ b/src/plugins/timer/plugin.js @@ -1,4 +1,3 @@ - /***************************************************************************** * Open MCT, Copyright (c) 2014-2023, United States Government * as represented by the Administrator of the National Aeronautics and Space @@ -29,91 +28,85 @@ import StartTimerAction from './actions/StartTimerAction'; import StopTimerAction from './actions/StopTimerAction'; export default function TimerPlugin() { - return function install(openmct) { - openmct.types.addType('timer', { - name: 'Timer', - description: 'A timer that counts up or down to a datetime. Timers can be started, stopped and reset whenever needed, and support a variety of display formats. Each Timer displays the same value to all users. Timers can be added to Display Layouts.', - creatable: true, - cssClass: 'icon-timer', - initialize: function (domainObject) { - domainObject.configuration = { - timerFormat: 'long', - timestamp: undefined, - timezone: 'UTC', - timerState: undefined, - pausedTime: undefined - }; + return function install(openmct) { + openmct.types.addType('timer', { + name: 'Timer', + description: + 'A timer that counts up or down to a datetime. Timers can be started, stopped and reset whenever needed, and support a variety of display formats. Each Timer displays the same value to all users. Timers can be added to Display Layouts.', + creatable: true, + cssClass: 'icon-timer', + initialize: function (domainObject) { + domainObject.configuration = { + timerFormat: 'long', + timestamp: undefined, + timezone: 'UTC', + timerState: undefined, + pausedTime: undefined + }; + }, + form: [ + { + key: 'timestamp', + control: 'datetime', + name: 'Target', + property: ['configuration', 'timestamp'] + }, + { + key: 'timerFormat', + name: 'Display Format', + control: 'select', + options: [ + { + value: 'long', + name: 'DDD hh:mm:ss' }, - "form": [ - { - "key": "timestamp", - "control": "datetime", - "name": "Target", - property: [ - 'configuration', - 'timestamp' - ] - }, - { - "key": "timerFormat", - "name": "Display Format", - "control": "select", - "options": [ - { - "value": "long", - "name": "DDD hh:mm:ss" - }, - { - "value": "short", - "name": "hh:mm:ss" - } - ], - property: [ - 'configuration', - 'timerFormat' - ] - } - ] - }); - openmct.objectViews.addProvider(new TimerViewProvider(openmct)); - - openmct.actions.register(new PauseTimerAction(openmct)); - openmct.actions.register(new RestartTimerAction(openmct)); - openmct.actions.register(new StartTimerAction(openmct)); - openmct.actions.register(new StopTimerAction(openmct)); - - openmct.objects.addGetInterceptor({ - appliesTo: (identifier, domainObject) => { - return domainObject && domainObject.type === 'timer'; - }, - invoke: (identifier, domainObject) => { - if (domainObject.configuration) { - return domainObject; - } - - const configuration = {}; - - if (domainObject.timerFormat) { - configuration.timerFormat = domainObject.timerFormat; - } - - if (domainObject.timestamp) { - configuration.timestamp = domainObject.timestamp; - } - - if (domainObject.timerState) { - configuration.timerState = domainObject.timerState; - } - - if (domainObject.pausedTime) { - configuration.pausedTime = domainObject.pausedTime; - } - - openmct.objects.mutate(domainObject, 'configuration', configuration); - - return domainObject; + { + value: 'short', + name: 'hh:mm:ss' } - }); + ], + property: ['configuration', 'timerFormat'] + } + ] + }); + openmct.objectViews.addProvider(new TimerViewProvider(openmct)); - }; + openmct.actions.register(new PauseTimerAction(openmct)); + openmct.actions.register(new RestartTimerAction(openmct)); + openmct.actions.register(new StartTimerAction(openmct)); + openmct.actions.register(new StopTimerAction(openmct)); + + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return domainObject && domainObject.type === 'timer'; + }, + invoke: (identifier, domainObject) => { + if (domainObject.configuration) { + return domainObject; + } + + const configuration = {}; + + if (domainObject.timerFormat) { + configuration.timerFormat = domainObject.timerFormat; + } + + if (domainObject.timestamp) { + configuration.timestamp = domainObject.timestamp; + } + + if (domainObject.timerState) { + configuration.timerState = domainObject.timerState; + } + + if (domainObject.pausedTime) { + configuration.pausedTime = domainObject.pausedTime; + } + + openmct.objects.mutate(domainObject, 'configuration', configuration); + + return domainObject; + } + }); + }; } diff --git a/src/plugins/timer/pluginSpec.js b/src/plugins/timer/pluginSpec.js index 3bd9dbfde6..077ca44317 100644 --- a/src/plugins/timer/pluginSpec.js +++ b/src/plugins/timer/pluginSpec.js @@ -25,341 +25,343 @@ import timerPlugin from './plugin'; import Vue from 'vue'; -describe("Timer plugin:", () => { - let openmct; - let timerDefinition; - let element; - let child; - let appHolder; +describe('Timer plugin:', () => { + let openmct; + let timerDefinition; + let element; + let child; + let appHolder; - let timerDomainObject; + let timerDomainObject; - function setupTimer() { - return new Promise((resolve, reject) => { - timerDomainObject = { - identifier: { - key: 'timer', - namespace: 'test-namespace' - }, - type: 'timer' - }; + function setupTimer() { + return new Promise((resolve, reject) => { + timerDomainObject = { + identifier: { + key: 'timer', + namespace: 'test-namespace' + }, + type: 'timer' + }; - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; - document.body.appendChild(appHolder); + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + document.body.appendChild(appHolder); - openmct = createOpenMct(); + openmct = createOpenMct(); - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); - openmct.install(timerPlugin()); + openmct.install(timerPlugin()); - timerDefinition = openmct.types.get('timer').definition; - timerDefinition.initialize(timerDomainObject); + timerDefinition = openmct.types.get('timer').definition; + timerDefinition.initialize(timerDomainObject); - spyOn(openmct.objects, 'supportsMutation').and.returnValue(true); + spyOn(openmct.objects, 'supportsMutation').and.returnValue(true); - openmct.on('start', resolve); - openmct.start(appHolder); - }); - } + openmct.on('start', resolve); + openmct.start(appHolder); + }); + } + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe("should still work if it's in the old format", () => { + let timerViewProvider; + let timerView; + let timerViewObject; + let mutableTimerObject; + let timerObjectPath; + const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM + + beforeEach(async () => { + await setupTimer(); + + timerViewObject = { + identifier: { + key: 'timer', + namespace: 'test-namespace' + }, + type: 'timer', + id: 'test-object', + name: 'Timer', + timerFormat: 'short', + timestamp: relativeTimestamp, + timerState: 'paused', + pausedTime: relativeTimestamp + }; + + const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]); + timerViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'timer.view'); + + spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject)); + spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); + + mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier); + + timerObjectPath = [mutableTimerObject]; + timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath); + timerView.show(child); + + await Vue.nextTick(); + }); afterEach(() => { - return resetApplicationState(openmct); + timerView.destroy(); }); - describe("should still work if it's in the old format", () => { - let timerViewProvider; - let timerView; - let timerViewObject; - let mutableTimerObject; - let timerObjectPath; - const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM + it('should migrate old object properties to the configuration section', () => { + openmct.objects.applyGetInterceptors(timerViewObject.identifier, timerViewObject); + expect(timerViewObject.configuration.timerFormat).toBe('short'); + expect(timerViewObject.configuration.timestamp).toBe(relativeTimestamp); + expect(timerViewObject.configuration.timerState).toBe('paused'); + expect(timerViewObject.configuration.pausedTime).toBe(relativeTimestamp); + }); + }); - beforeEach(async () => { - await setupTimer(); + describe('Timer view:', () => { + let timerViewProvider; + let timerView; + let timerViewObject; + let mutableTimerObject; + let timerObjectPath; - timerViewObject = { - identifier: { - key: 'timer', - namespace: 'test-namespace' - }, - type: 'timer', - id: "test-object", - name: 'Timer', - timerFormat: 'short', - timestamp: relativeTimestamp, - timerState: 'paused', - pausedTime: relativeTimestamp - }; + beforeEach(async () => { + await setupTimer(); - const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]); - timerViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'timer.view'); + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((cb) => setTimeout(cb, 500)); + const baseTimestamp = 1634688000000; // Oct 20, 2021, 12:00 AM + const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM - spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject)); - spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); + jasmine.clock().install(); + const baseTime = new Date(baseTimestamp); + jasmine.clock().mockDate(baseTime); - mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier); + timerViewObject = { + ...timerDomainObject, + id: 'test-object', + name: 'Timer', + configuration: { + timerFormat: 'long', + timestamp: relativeTimestamp, + timezone: 'UTC', + timerState: undefined, + pausedTime: undefined + } + }; - timerObjectPath = [mutableTimerObject]; - timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath); - timerView.show(child); + spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject)); + spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); - await Vue.nextTick(); - }); + const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]); + timerViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'timer.view'); - afterEach(() => { - timerView.destroy(); - }); + mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier); - it("should migrate old object properties to the configuration section", () => { - openmct.objects.applyGetInterceptors(timerViewObject.identifier, timerViewObject); - expect(timerViewObject.configuration.timerFormat).toBe('short'); - expect(timerViewObject.configuration.timestamp).toBe(relativeTimestamp); - expect(timerViewObject.configuration.timerState).toBe('paused'); - expect(timerViewObject.configuration.pausedTime).toBe(relativeTimestamp); - }); + timerObjectPath = [mutableTimerObject]; + timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath); + timerView.show(child); + + await Vue.nextTick(); }); - describe("Timer view:", () => { - let timerViewProvider; - let timerView; - let timerViewObject; - let mutableTimerObject; - let timerObjectPath; - - beforeEach(async () => { - await setupTimer(); - - spyOnBuiltins(['requestAnimationFrame']); - window.requestAnimationFrame.and.callFake((cb) => setTimeout(cb, 500)); - const baseTimestamp = 1634688000000; // Oct 20, 2021, 12:00 AM - const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM - - jasmine.clock().install(); - const baseTime = new Date(baseTimestamp); - jasmine.clock().mockDate(baseTime); - - timerViewObject = { - ...timerDomainObject, - id: "test-object", - name: 'Timer', - configuration: { - timerFormat: 'long', - timestamp: relativeTimestamp, - timezone: 'UTC', - timerState: undefined, - pausedTime: undefined - } - }; - - spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject)); - spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); - - const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]); - timerViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'timer.view'); - - mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier); - - timerObjectPath = [mutableTimerObject]; - timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath); - timerView.show(child); - - await Vue.nextTick(); - }); - - afterEach(() => { - jasmine.clock().uninstall(); - timerView.destroy(); - openmct.objects.destroyMutable(mutableTimerObject); - if (appHolder) { - appHolder.remove(); - } - }); - - it("has name as Timer", () => { - expect(timerDefinition.name).toEqual('Timer'); - }); - - it("is creatable", () => { - expect(timerDefinition.creatable).toEqual(true); - }); - - it("provides timer view", () => { - expect(timerViewProvider).toBeDefined(); - }); - - it("renders timer element", () => { - const timerElement = element.querySelectorAll('.c-timer'); - expect(timerElement.length).toBe(1); - }); - - it("renders major elements", () => { - const timerElement = element.querySelector('.c-timer'); - const resetButton = timerElement.querySelector('.c-timer__ctrl-reset'); - const pausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); - const timerValue = timerElement.querySelector('.c-timer__value'); - const hasMajorElements = Boolean(resetButton && pausePlayButton && timerDirectionIcon && timerValue); - - expect(hasMajorElements).toBe(true); - }); - - it("gets errors from actions if configuration is not passed", async () => { - await Vue.nextTick(); - const objectPath = _.cloneDeep(timerObjectPath); - delete objectPath[0].configuration; - - let action = openmct.actions.getAction('timer.start'); - let actionResults = action.invoke(objectPath); - let actionFilterWithoutConfig = action.appliesTo(objectPath); - await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'started' }); - let actionFilterWithConfig = action.appliesTo(timerObjectPath); - - let actionError = new Error('Unable to run start timer action. No domainObject provided.'); - expect(actionResults).toEqual(actionError); - expect(actionFilterWithoutConfig).toBe(undefined); - expect(actionFilterWithConfig).toBe(false); - - action = openmct.actions.getAction('timer.stop'); - actionResults = action.invoke(objectPath); - actionFilterWithoutConfig = action.appliesTo(objectPath); - await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' }); - actionFilterWithConfig = action.appliesTo(timerObjectPath); - - actionError = new Error('Unable to run stop timer action. No domainObject provided.'); - expect(actionResults).toEqual(actionError); - expect(actionFilterWithoutConfig).toBe(undefined); - expect(actionFilterWithConfig).toBe(false); - - action = openmct.actions.getAction('timer.pause'); - actionResults = action.invoke(objectPath); - actionFilterWithoutConfig = action.appliesTo(objectPath); - await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'paused' }); - actionFilterWithConfig = action.appliesTo(timerObjectPath); - - actionError = new Error('Unable to run pause timer action. No domainObject provided.'); - expect(actionResults).toEqual(actionError); - expect(actionFilterWithoutConfig).toBe(undefined); - expect(actionFilterWithConfig).toBe(false); - - action = openmct.actions.getAction('timer.restart'); - actionResults = action.invoke(objectPath); - actionFilterWithoutConfig = action.appliesTo(objectPath); - await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' }); - actionFilterWithConfig = action.appliesTo(timerObjectPath); - - actionError = new Error('Unable to run restart timer action. No domainObject provided.'); - expect(actionResults).toEqual(actionError); - expect(actionFilterWithoutConfig).toBe(undefined); - expect(actionFilterWithConfig).toBe(false); - }); - - it("displays a started timer ticking down to a future date", async () => { - const newBaseTime = 1634774400000; // Oct 21 2021, 12:00 AM - openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime); - - jasmine.clock().tick(5000); - await Vue.nextTick(); - - const timerElement = element.querySelector('.c-timer'); - const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); - const timerValue = timerElement.querySelector('.c-timer__value').innerText; - - expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); - expect(timerDirectionIcon.classList.contains('icon-minus')).toBe(true); - expect(timerValue).toBe('0D 23:59:55'); - }); - - it("displays a started timer ticking up from a past date", async () => { - const newBaseTime = 1634601600000; // Oct 19, 2021, 12:00 AM - openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime); - - jasmine.clock().tick(5000); - await Vue.nextTick(); - - const timerElement = element.querySelector('.c-timer'); - const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); - const timerValue = timerElement.querySelector('.c-timer__value').innerText; - - expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); - expect(timerDirectionIcon.classList.contains('icon-plus')).toBe(true); - expect(timerValue).toBe('1D 00:00:05'); - }); - - it("displays a paused timer correctly in the DOM", async () => { - jasmine.clock().tick(5000); - await Vue.nextTick(); - - let action = openmct.actions.getAction('timer.pause'); - if (action) { - action.invoke(timerObjectPath, timerView); - } - - await Vue.nextTick(); - const timerElement = element.querySelector('.c-timer'); - const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - let timerValue = timerElement.querySelector('.c-timer__value').innerText; - - expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true); - expect(timerValue).toBe('0D 23:59:55'); - - jasmine.clock().tick(5000); - await Vue.nextTick(); - expect(timerValue).toBe('0D 23:59:55'); - - action = openmct.actions.getAction('timer.start'); - if (action) { - action.invoke(timerObjectPath, timerView); - } - - await Vue.nextTick(); - action = openmct.actions.getAction('timer.pause'); - if (action) { - action.invoke(timerObjectPath, timerView); - } - - await Vue.nextTick(); - timerValue = timerElement.querySelector('.c-timer__value').innerText; - expect(timerValue).toBe('1D 00:00:00'); - }); - - it("displays a stopped timer correctly in the DOM", async () => { - const action = openmct.actions.getAction('timer.stop'); - if (action) { - action.invoke(timerObjectPath, timerView); - } - - await Vue.nextTick(); - const timerElement = element.querySelector('.c-timer'); - const timerValue = timerElement.querySelector('.c-timer__value').innerText; - const timerResetButton = timerElement.querySelector('.c-timer__ctrl-reset'); - const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - - expect(timerResetButton.classList.contains('hide')).toBe(true); - expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true); - expect(timerValue).toBe('--:--:--'); - }); - - it("displays a restarted timer correctly in the DOM", async () => { - const action = openmct.actions.getAction('timer.restart'); - if (action) { - action.invoke(timerObjectPath, timerView); - } - - jasmine.clock().tick(5000); - await Vue.nextTick(); - const timerElement = element.querySelector('.c-timer'); - const timerValue = timerElement.querySelector('.c-timer__value').innerText; - const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - - expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); - expect(timerValue).toBe('0D 00:00:05'); - }); + afterEach(() => { + jasmine.clock().uninstall(); + timerView.destroy(); + openmct.objects.destroyMutable(mutableTimerObject); + if (appHolder) { + appHolder.remove(); + } }); + + it('has name as Timer', () => { + expect(timerDefinition.name).toEqual('Timer'); + }); + + it('is creatable', () => { + expect(timerDefinition.creatable).toEqual(true); + }); + + it('provides timer view', () => { + expect(timerViewProvider).toBeDefined(); + }); + + it('renders timer element', () => { + const timerElement = element.querySelectorAll('.c-timer'); + expect(timerElement.length).toBe(1); + }); + + it('renders major elements', () => { + const timerElement = element.querySelector('.c-timer'); + const resetButton = timerElement.querySelector('.c-timer__ctrl-reset'); + const pausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); + const timerValue = timerElement.querySelector('.c-timer__value'); + const hasMajorElements = Boolean( + resetButton && pausePlayButton && timerDirectionIcon && timerValue + ); + + expect(hasMajorElements).toBe(true); + }); + + it('gets errors from actions if configuration is not passed', async () => { + await Vue.nextTick(); + const objectPath = _.cloneDeep(timerObjectPath); + delete objectPath[0].configuration; + + let action = openmct.actions.getAction('timer.start'); + let actionResults = action.invoke(objectPath); + let actionFilterWithoutConfig = action.appliesTo(objectPath); + await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'started' }); + let actionFilterWithConfig = action.appliesTo(timerObjectPath); + + let actionError = new Error('Unable to run start timer action. No domainObject provided.'); + expect(actionResults).toEqual(actionError); + expect(actionFilterWithoutConfig).toBe(undefined); + expect(actionFilterWithConfig).toBe(false); + + action = openmct.actions.getAction('timer.stop'); + actionResults = action.invoke(objectPath); + actionFilterWithoutConfig = action.appliesTo(objectPath); + await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' }); + actionFilterWithConfig = action.appliesTo(timerObjectPath); + + actionError = new Error('Unable to run stop timer action. No domainObject provided.'); + expect(actionResults).toEqual(actionError); + expect(actionFilterWithoutConfig).toBe(undefined); + expect(actionFilterWithConfig).toBe(false); + + action = openmct.actions.getAction('timer.pause'); + actionResults = action.invoke(objectPath); + actionFilterWithoutConfig = action.appliesTo(objectPath); + await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'paused' }); + actionFilterWithConfig = action.appliesTo(timerObjectPath); + + actionError = new Error('Unable to run pause timer action. No domainObject provided.'); + expect(actionResults).toEqual(actionError); + expect(actionFilterWithoutConfig).toBe(undefined); + expect(actionFilterWithConfig).toBe(false); + + action = openmct.actions.getAction('timer.restart'); + actionResults = action.invoke(objectPath); + actionFilterWithoutConfig = action.appliesTo(objectPath); + await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' }); + actionFilterWithConfig = action.appliesTo(timerObjectPath); + + actionError = new Error('Unable to run restart timer action. No domainObject provided.'); + expect(actionResults).toEqual(actionError); + expect(actionFilterWithoutConfig).toBe(undefined); + expect(actionFilterWithConfig).toBe(false); + }); + + it('displays a started timer ticking down to a future date', async () => { + const newBaseTime = 1634774400000; // Oct 21 2021, 12:00 AM + openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime); + + jasmine.clock().tick(5000); + await Vue.nextTick(); + + const timerElement = element.querySelector('.c-timer'); + const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); + const timerValue = timerElement.querySelector('.c-timer__value').innerText; + + expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); + expect(timerDirectionIcon.classList.contains('icon-minus')).toBe(true); + expect(timerValue).toBe('0D 23:59:55'); + }); + + it('displays a started timer ticking up from a past date', async () => { + const newBaseTime = 1634601600000; // Oct 19, 2021, 12:00 AM + openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime); + + jasmine.clock().tick(5000); + await Vue.nextTick(); + + const timerElement = element.querySelector('.c-timer'); + const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); + const timerValue = timerElement.querySelector('.c-timer__value').innerText; + + expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); + expect(timerDirectionIcon.classList.contains('icon-plus')).toBe(true); + expect(timerValue).toBe('1D 00:00:05'); + }); + + it('displays a paused timer correctly in the DOM', async () => { + jasmine.clock().tick(5000); + await Vue.nextTick(); + + let action = openmct.actions.getAction('timer.pause'); + if (action) { + action.invoke(timerObjectPath, timerView); + } + + await Vue.nextTick(); + const timerElement = element.querySelector('.c-timer'); + const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + let timerValue = timerElement.querySelector('.c-timer__value').innerText; + + expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true); + expect(timerValue).toBe('0D 23:59:55'); + + jasmine.clock().tick(5000); + await Vue.nextTick(); + expect(timerValue).toBe('0D 23:59:55'); + + action = openmct.actions.getAction('timer.start'); + if (action) { + action.invoke(timerObjectPath, timerView); + } + + await Vue.nextTick(); + action = openmct.actions.getAction('timer.pause'); + if (action) { + action.invoke(timerObjectPath, timerView); + } + + await Vue.nextTick(); + timerValue = timerElement.querySelector('.c-timer__value').innerText; + expect(timerValue).toBe('1D 00:00:00'); + }); + + it('displays a stopped timer correctly in the DOM', async () => { + const action = openmct.actions.getAction('timer.stop'); + if (action) { + action.invoke(timerObjectPath, timerView); + } + + await Vue.nextTick(); + const timerElement = element.querySelector('.c-timer'); + const timerValue = timerElement.querySelector('.c-timer__value').innerText; + const timerResetButton = timerElement.querySelector('.c-timer__ctrl-reset'); + const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + + expect(timerResetButton.classList.contains('hide')).toBe(true); + expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true); + expect(timerValue).toBe('--:--:--'); + }); + + it('displays a restarted timer correctly in the DOM', async () => { + const action = openmct.actions.getAction('timer.restart'); + if (action) { + action.invoke(timerObjectPath, timerView); + } + + jasmine.clock().tick(5000); + await Vue.nextTick(); + const timerElement = element.querySelector('.c-timer'); + const timerValue = timerElement.querySelector('.c-timer__value').innerText; + const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + + expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); + expect(timerValue).toBe('0D 00:00:05'); + }); + }); }); diff --git a/src/plugins/userIndicator/components/UserIndicator.vue b/src/plugins/userIndicator/components/UserIndicator.vue index 9cef01e463..99d51874ad 100644 --- a/src/plugins/userIndicator/components/UserIndicator.vue +++ b/src/plugins/userIndicator/components/UserIndicator.vue @@ -21,34 +21,33 @@ --> diff --git a/src/plugins/userIndicator/plugin.js b/src/plugins/userIndicator/plugin.js index d653a40fd5..ffa44a2193 100644 --- a/src/plugins/userIndicator/plugin.js +++ b/src/plugins/userIndicator/plugin.js @@ -24,33 +24,32 @@ import UserIndicator from './components/UserIndicator.vue'; import Vue from 'vue'; export default function UserIndicatorPlugin() { + function addIndicator(openmct) { + const userIndicator = new Vue({ + components: { + UserIndicator + }, + provide: { + openmct: openmct + }, + template: '' + }); - function addIndicator(openmct) { - const userIndicator = new Vue ({ - components: { - UserIndicator - }, - provide: { - openmct: openmct - }, - template: '' - }); + openmct.indicators.add({ + key: 'user-indicator', + element: userIndicator.$mount().$el, + priority: openmct.priority.HIGH + }); + } - openmct.indicators.add({ - key: 'user-indicator', - element: userIndicator.$mount().$el, - priority: openmct.priority.HIGH - }); + return function install(openmct) { + if (openmct.user.hasProvider()) { + addIndicator(openmct); + } else { + // back up if user provider added after indicator installed + openmct.user.on('providerAdded', () => { + addIndicator(openmct); + }); } - - return function install(openmct) { - if (openmct.user.hasProvider()) { - addIndicator(openmct); - } else { - // back up if user provider added after indicator installed - openmct.user.on('providerAdded', () => { - addIndicator(openmct); - }); - } - }; + }; } diff --git a/src/plugins/userIndicator/pluginSpec.js b/src/plugins/userIndicator/pluginSpec.js index 511a701f75..29ce5ffb4f 100644 --- a/src/plugins/userIndicator/pluginSpec.js +++ b/src/plugins/userIndicator/pluginSpec.js @@ -20,81 +20,82 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; import Vue from 'vue'; import ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvider'; const USERNAME = 'Coach McGuirk'; describe('The User Indicator plugin', () => { - let openmct; - let element; - let child; - let appHolder; - let userIndicator; - let provider; + let openmct; + let element; + let child; + let appHolder; + let userIndicator; + let provider; - beforeEach((done) => { - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; - document.body.appendChild(appHolder); + beforeEach((done) => { + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + document.body.appendChild(appHolder); - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); - openmct = createOpenMct(); - openmct.on('start', done); - openmct.start(appHolder); + openmct = createOpenMct(); + openmct.on('start', done); + openmct.start(appHolder); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('will not show, if there is no user provider', () => { + userIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'user-indicator' + ); + + expect(userIndicator).toBe(undefined); + }); + + describe('with a user provider installed', () => { + beforeEach(() => { + provider = new ExampleUserProvider(openmct); + provider.autoLogin(USERNAME); + + openmct.user.setProvider(provider); + + return Vue.nextTick(); }); - afterEach(() => { - return resetApplicationState(openmct); + it('exists', () => { + userIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'user-indicator' + ).element; + + const hasClockIndicator = userIndicator !== null && userIndicator !== undefined; + expect(hasClockIndicator).toBe(true); }); - it('will not show, if there is no user provider', () => { - userIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'user-indicator'); - - expect(userIndicator).toBe(undefined); - }); - - describe('with a user provider installed', () => { - - beforeEach(() => { - provider = new ExampleUserProvider(openmct); - provider.autoLogin(USERNAME); - - openmct.user.setProvider(provider); - - return Vue.nextTick(); - }); - - it('exists', () => { - userIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'user-indicator').element; - - const hasClockIndicator = userIndicator !== null && userIndicator !== undefined; - expect(hasClockIndicator).toBe(true); - }); - - it('contains the logged in user name', (done) => { - openmct.user.getCurrentUser().then(async (user) => { - await Vue.nextTick(); - - userIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'user-indicator').element; - - const userName = userIndicator.textContent.trim(); - - expect(user.name).toEqual(USERNAME); - expect(userName).toContain(USERNAME); - }).finally(done); - }); + it('contains the logged in user name', (done) => { + openmct.user + .getCurrentUser() + .then(async (user) => { + await Vue.nextTick(); + userIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'user-indicator' + ).element; + + const userName = userIndicator.textContent.trim(); + + expect(user.name).toEqual(USERNAME); + expect(userName).toContain(USERNAME); + }) + .finally(done); }); + }); }); diff --git a/src/plugins/utcTimeSystem/DurationFormat.js b/src/plugins/utcTimeSystem/DurationFormat.js index 455828294b..657e8c2f70 100644 --- a/src/plugins/utcTimeSystem/DurationFormat.js +++ b/src/plugins/utcTimeSystem/DurationFormat.js @@ -1,10 +1,7 @@ - import moment from 'moment'; -const DATE_FORMAT = "HH:mm:ss"; -const DATE_FORMATS = [ - DATE_FORMAT -]; +const DATE_FORMAT = 'HH:mm:ss'; +const DATE_FORMATS = [DATE_FORMAT]; /** * Formatter for duration. Uses moment to produce a date from a given @@ -18,20 +15,20 @@ const DATE_FORMATS = [ * @memberof platform/commonUI/formats */ class DurationFormat { - constructor() { - this.key = "duration"; - } - format(value) { - return moment.utc(value).format(DATE_FORMAT); - } + constructor() { + this.key = 'duration'; + } + format(value) { + return moment.utc(value).format(DATE_FORMAT); + } - parse(text) { - return moment.duration(text).asMilliseconds(); - } + parse(text) { + return moment.duration(text).asMilliseconds(); + } - validate(text) { - return moment.utc(text, DATE_FORMATS, true).isValid(); - } + validate(text) { + return moment.utc(text, DATE_FORMATS, true).isValid(); + } } export default DurationFormat; diff --git a/src/plugins/utcTimeSystem/LocalClock.js b/src/plugins/utcTimeSystem/LocalClock.js index b84e67e1a7..6983aaf72c 100644 --- a/src/plugins/utcTimeSystem/LocalClock.js +++ b/src/plugins/utcTimeSystem/LocalClock.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import DefaultClock from "../../utils/clock/DefaultClock"; +import DefaultClock from '../../utils/clock/DefaultClock'; /** * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the * application based on UTC time values provided by a ticking local clock, @@ -30,33 +30,32 @@ import DefaultClock from "../../utils/clock/DefaultClock"; */ export default class LocalClock extends DefaultClock { - constructor(period = 100) { - super(); + constructor(period = 100) { + super(); - this.key = 'local'; - this.name = 'Local Clock'; - this.description = "Provides UTC timestamps every second from the local system clock."; + this.key = 'local'; + this.name = 'Local Clock'; + this.description = 'Provides UTC timestamps every second from the local system clock.'; - this.period = period; - this.timeoutHandle = undefined; - this.lastTick = Date.now(); - } - - start() { - this.timeoutHandle = setTimeout(this.tick.bind(this), this.period); - } - - stop() { - if (this.timeoutHandle) { - clearTimeout(this.timeoutHandle); - this.timeoutHandle = undefined; - } - } - - tick() { - const now = Date.now(); - super.tick(now); - this.timeoutHandle = setTimeout(this.tick.bind(this), this.period); + this.period = period; + this.timeoutHandle = undefined; + this.lastTick = Date.now(); + } + + start() { + this.timeoutHandle = setTimeout(this.tick.bind(this), this.period); + } + + stop() { + if (this.timeoutHandle) { + clearTimeout(this.timeoutHandle); + this.timeoutHandle = undefined; } + } + tick() { + const now = Date.now(); + super.tick(now); + this.timeoutHandle = setTimeout(this.tick.bind(this), this.period); + } } diff --git a/src/plugins/utcTimeSystem/UTCTimeFormat.js b/src/plugins/utcTimeSystem/UTCTimeFormat.js index 79fae9df8b..54699794e2 100644 --- a/src/plugins/utcTimeSystem/UTCTimeFormat.js +++ b/src/plugins/utcTimeSystem/UTCTimeFormat.js @@ -31,58 +31,57 @@ import moment from 'moment'; * @memberof platform/commonUI/formats */ export default class UTCTimeFormat { - constructor() { - this.key = 'utc'; - this.DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'; - this.DATE_FORMATS = { - PRECISION_DEFAULT: this.DATE_FORMAT, - PRECISION_DEFAULT_WITH_ZULU: this.DATE_FORMAT + 'Z', - PRECISION_SECONDS: 'YYYY-MM-DD HH:mm:ss', - PRECISION_MINUTES: 'YYYY-MM-DD HH:mm', - PRECISION_DAYS: 'YYYY-MM-DD' - }; + constructor() { + this.key = 'utc'; + this.DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'; + this.DATE_FORMATS = { + PRECISION_DEFAULT: this.DATE_FORMAT, + PRECISION_DEFAULT_WITH_ZULU: this.DATE_FORMAT + 'Z', + PRECISION_SECONDS: 'YYYY-MM-DD HH:mm:ss', + PRECISION_MINUTES: 'YYYY-MM-DD HH:mm', + PRECISION_DAYS: 'YYYY-MM-DD' + }; + } + + /** + * @param {string} formatString + * @returns the value of formatString if the value is a string type and exists in the DATE_FORMATS array; otherwise the DATE_FORMAT value. + */ + isValidFormatString(formatString) { + return Object.values(this.DATE_FORMATS).includes(formatString); + } + + /** + * @param {number} value The value to format. + * @returns {string} the formatted date(s). If multiple values were requested, then an array of + * formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position + * in the array. + */ + format(value, formatString) { + if (value !== undefined) { + const utc = moment.utc(value); + + if (formatString !== undefined && !this.isValidFormatString(formatString)) { + throw 'Invalid format requested from UTC Time Formatter '; + } + + let format = formatString || this.DATE_FORMATS.PRECISION_DEFAULT; + + return utc.format(format) + (formatString ? '' : 'Z'); + } else { + return value; + } + } + + parse(text) { + if (typeof text === 'number') { + return text; } - /** - * @param {string} formatString - * @returns the value of formatString if the value is a string type and exists in the DATE_FORMATS array; otherwise the DATE_FORMAT value. - */ - isValidFormatString(formatString) { - return Object.values(this.DATE_FORMATS).includes(formatString); - } - - /** - * @param {number} value The value to format. - * @returns {string} the formatted date(s). If multiple values were requested, then an array of - * formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position - * in the array. - */ - format(value, formatString) { - if (value !== undefined) { - const utc = moment.utc(value); - - if (formatString !== undefined && !this.isValidFormatString(formatString)) { - throw "Invalid format requested from UTC Time Formatter "; - } - - let format = formatString || this.DATE_FORMATS.PRECISION_DEFAULT; - - return utc.format(format) + (formatString ? '' : 'Z'); - } else { - return value; - } - } - - parse(text) { - if (typeof text === 'number') { - return text; - } - - return moment.utc(text, Object.values(this.DATE_FORMATS)).valueOf(); - } - - validate(text) { - return moment.utc(text, Object.values(this.DATE_FORMATS), true).isValid(); - } + return moment.utc(text, Object.values(this.DATE_FORMATS)).valueOf(); + } + validate(text) { + return moment.utc(text, Object.values(this.DATE_FORMATS), true).isValid(); + } } diff --git a/src/plugins/utcTimeSystem/UTCTimeSystem.js b/src/plugins/utcTimeSystem/UTCTimeSystem.js index d8f806ec5e..5002948152 100644 --- a/src/plugins/utcTimeSystem/UTCTimeSystem.js +++ b/src/plugins/utcTimeSystem/UTCTimeSystem.js @@ -21,24 +21,23 @@ *****************************************************************************/ define([], function () { + /** + * This time system supports UTC dates. + * @implements TimeSystem + * @constructor + */ + function UTCTimeSystem() { /** - * This time system supports UTC dates. - * @implements TimeSystem - * @constructor + * Metadata used to identify the time system in + * the UI */ - function UTCTimeSystem() { + this.key = 'utc'; + this.name = 'UTC'; + this.cssClass = 'icon-clock'; + this.timeFormat = 'utc'; + this.durationFormat = 'duration'; + this.isUTCBased = true; + } - /** - * Metadata used to identify the time system in - * the UI - */ - this.key = 'utc'; - this.name = 'UTC'; - this.cssClass = 'icon-clock'; - this.timeFormat = 'utc'; - this.durationFormat = 'duration'; - this.isUTCBased = true; - } - - return UTCTimeSystem; + return UTCTimeSystem; }); diff --git a/src/plugins/utcTimeSystem/plugin.js b/src/plugins/utcTimeSystem/plugin.js index fc5bf347f2..0a5bfc3da0 100644 --- a/src/plugins/utcTimeSystem/plugin.js +++ b/src/plugins/utcTimeSystem/plugin.js @@ -30,11 +30,11 @@ import DurationFormat from './DurationFormat'; * clock source that ticks every 100ms, providing UTC times. */ export default function () { - return function (openmct) { - const timeSystem = new UTCTimeSystem(); - openmct.time.addTimeSystem(timeSystem); - openmct.time.addClock(new LocalClock(100)); - openmct.telemetry.addFormat(new UTCTimeFormat()); - openmct.telemetry.addFormat(new DurationFormat()); - }; + return function (openmct) { + const timeSystem = new UTCTimeSystem(); + openmct.time.addTimeSystem(timeSystem); + openmct.time.addClock(new LocalClock(100)); + openmct.telemetry.addFormat(new UTCTimeFormat()); + openmct.telemetry.addFormat(new DurationFormat()); + }; } diff --git a/src/plugins/utcTimeSystem/pluginSpec.js b/src/plugins/utcTimeSystem/pluginSpec.js index 893875564d..9b78567bb7 100644 --- a/src/plugins/utcTimeSystem/pluginSpec.js +++ b/src/plugins/utcTimeSystem/pluginSpec.js @@ -22,173 +22,170 @@ import LocalClock from './LocalClock.js'; import UTCTimeSystem from './UTCTimeSystem'; -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; import UTCTimeFormat from './UTCTimeFormat.js'; -describe("The UTC Time System", () => { - const UTC_SYSTEM_AND_FORMAT_KEY = 'utc'; - const DURATION_FORMAT_KEY = 'duration'; - let openmct; - let utcTimeSystem; - let mockTimeout; +describe('The UTC Time System', () => { + const UTC_SYSTEM_AND_FORMAT_KEY = 'utc'; + const DURATION_FORMAT_KEY = 'duration'; + let openmct; + let utcTimeSystem; + let mockTimeout; + + beforeEach(() => { + openmct = createOpenMct(); + openmct.install(openmct.plugins.UTCTimeSystem()); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('plugin', function () { + beforeEach(function () { + mockTimeout = jasmine.createSpy('timeout'); + utcTimeSystem = new UTCTimeSystem(mockTimeout); + }); + + it('is installed', () => { + let timeSystems = openmct.time.getAllTimeSystems(); + let utc = timeSystems.find((ts) => ts.key === UTC_SYSTEM_AND_FORMAT_KEY); + + expect(utc).not.toEqual(-1); + }); + + it('can be set to be the main time system', () => { + openmct.time.timeSystem(UTC_SYSTEM_AND_FORMAT_KEY, { + start: 0, + end: 1 + }); + + expect(openmct.time.timeSystem().key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); + }); + + it('uses the utc time format', () => { + expect(utcTimeSystem.timeFormat).toBe(UTC_SYSTEM_AND_FORMAT_KEY); + }); + + it('is UTC based', () => { + expect(utcTimeSystem.isUTCBased).toBe(true); + }); + + it('defines expected metadata', () => { + expect(utcTimeSystem.key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); + expect(utcTimeSystem.name).toBeDefined(); + expect(utcTimeSystem.cssClass).toBeDefined(); + expect(utcTimeSystem.durationFormat).toBeDefined(); + }); + }); + + describe('LocalClock class', function () { + let clock; + const timeoutHandle = {}; + + beforeEach(function () { + mockTimeout = jasmine.createSpy('timeout'); + mockTimeout.and.returnValue(timeoutHandle); + + clock = new LocalClock(0); + clock.start(); + }); + + it('calls listeners on tick with current time', function () { + const mockListener = jasmine.createSpy('listener'); + clock.on('tick', mockListener); + clock.tick(); + expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number)); + }); + }); + + describe('UTC Time Format', () => { + let utcTimeFormatter; beforeEach(() => { - openmct = createOpenMct(); - openmct.install(openmct.plugins.UTCTimeSystem()); + utcTimeFormatter = openmct.telemetry.getFormatter(UTC_SYSTEM_AND_FORMAT_KEY); }); - afterEach(() => { - return resetApplicationState(openmct); + it('is installed by the plugin', () => { + expect(utcTimeFormatter).toBeDefined(); }); - describe("plugin", function () { + it('formats from ms since Unix epoch into Open MCT UTC time format', () => { + const TIME_IN_MS = 1638574560945; + const TIME_AS_STRING = '2021-12-03 23:36:00.945Z'; - beforeEach(function () { - mockTimeout = jasmine.createSpy("timeout"); - utcTimeSystem = new UTCTimeSystem(mockTimeout); - }); - - it("is installed", () => { - let timeSystems = openmct.time.getAllTimeSystems(); - let utc = timeSystems.find(ts => ts.key === UTC_SYSTEM_AND_FORMAT_KEY); - - expect(utc).not.toEqual(-1); - }); - - it("can be set to be the main time system", () => { - openmct.time.timeSystem(UTC_SYSTEM_AND_FORMAT_KEY, { - start: 0, - end: 1 - }); - - expect(openmct.time.timeSystem().key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); - }); - - it("uses the utc time format", () => { - expect(utcTimeSystem.timeFormat).toBe(UTC_SYSTEM_AND_FORMAT_KEY); - }); - - it("is UTC based", () => { - expect(utcTimeSystem.isUTCBased).toBe(true); - }); - - it("defines expected metadata", () => { - expect(utcTimeSystem.key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); - expect(utcTimeSystem.name).toBeDefined(); - expect(utcTimeSystem.cssClass).toBeDefined(); - expect(utcTimeSystem.durationFormat).toBeDefined(); - }); + const formattedTime = utcTimeFormatter.format(TIME_IN_MS); + expect(formattedTime).toEqual(TIME_AS_STRING); }); - describe("LocalClock class", function () { - let clock; - const timeoutHandle = {}; + it('formats from ms since Unix epoch into terse UTC formats', () => { + const utcTimeFormatterInstance = new UTCTimeFormat(); - beforeEach(function () { - mockTimeout = jasmine.createSpy("timeout"); - mockTimeout.and.returnValue(timeoutHandle); + const TIME_IN_MS = 1638574560945; + const EXPECTED_FORMATS = { + PRECISION_DEFAULT: '2021-12-03 23:36:00.945', + PRECISION_SECONDS: '2021-12-03 23:36:00', + PRECISION_MINUTES: '2021-12-03 23:36', + PRECISION_DAYS: '2021-12-03' + }; - clock = new LocalClock(0); - clock.start(); - }); - - it("calls listeners on tick with current time", function () { - const mockListener = jasmine.createSpy("listener"); - clock.on('tick', mockListener); - clock.tick(); - expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number)); - }); + Object.keys(EXPECTED_FORMATS).forEach((formatKey) => { + const formattedTime = utcTimeFormatterInstance.format( + TIME_IN_MS, + utcTimeFormatterInstance.DATE_FORMATS[formatKey] + ); + expect(formattedTime).toEqual(EXPECTED_FORMATS[formatKey]); + }); }); - describe("UTC Time Format", () => { - let utcTimeFormatter; + it('parses from Open MCT UTC time format to ms since Unix epoch.', () => { + const TIME_IN_MS = 1638574560945; + const TIME_AS_STRING = '2021-12-03 23:36:00.945Z'; - beforeEach(() => { - utcTimeFormatter = openmct.telemetry.getFormatter(UTC_SYSTEM_AND_FORMAT_KEY); - }); - - it("is installed by the plugin", () => { - expect(utcTimeFormatter).toBeDefined(); - }); - - it("formats from ms since Unix epoch into Open MCT UTC time format", () => { - const TIME_IN_MS = 1638574560945; - const TIME_AS_STRING = "2021-12-03 23:36:00.945Z"; - - const formattedTime = utcTimeFormatter.format(TIME_IN_MS); - expect(formattedTime).toEqual(TIME_AS_STRING); - - }); - - it("formats from ms since Unix epoch into terse UTC formats", () => { - const utcTimeFormatterInstance = new UTCTimeFormat(); - - const TIME_IN_MS = 1638574560945; - const EXPECTED_FORMATS = { - PRECISION_DEFAULT: "2021-12-03 23:36:00.945", - PRECISION_SECONDS: "2021-12-03 23:36:00", - PRECISION_MINUTES: "2021-12-03 23:36", - PRECISION_DAYS: "2021-12-03" - }; - - Object.keys(EXPECTED_FORMATS).forEach((formatKey) => { - const formattedTime = utcTimeFormatterInstance.format(TIME_IN_MS, utcTimeFormatterInstance.DATE_FORMATS[formatKey]); - expect(formattedTime).toEqual(EXPECTED_FORMATS[formatKey]); - }); - }); - - it("parses from Open MCT UTC time format to ms since Unix epoch.", () => { - const TIME_IN_MS = 1638574560945; - const TIME_AS_STRING = "2021-12-03 23:36:00.945Z"; - - const parsedTime = utcTimeFormatter.parse(TIME_AS_STRING); - expect(parsedTime).toEqual(TIME_IN_MS); - }); - - it("validates correctly formatted Open MCT UTC times.", () => { - const TIME_AS_STRING = "2021-12-03 23:36:00.945Z"; - - const isValid = utcTimeFormatter.validate(TIME_AS_STRING); - expect(isValid).toBeTrue(); - }); + const parsedTime = utcTimeFormatter.parse(TIME_AS_STRING); + expect(parsedTime).toEqual(TIME_IN_MS); }); - describe("Duration Format", () => { - let durationTimeFormatter; + it('validates correctly formatted Open MCT UTC times.', () => { + const TIME_AS_STRING = '2021-12-03 23:36:00.945Z'; - beforeEach(() => { - durationTimeFormatter = openmct.telemetry.getFormatter(DURATION_FORMAT_KEY); - }); - - it("is installed by the plugin", () => { - expect(durationTimeFormatter).toBeDefined(); - }); - - it("formats from ms into Open MCT duration format", () => { - const TIME_IN_MS = 2000; - const TIME_AS_STRING = "00:00:02"; - - const formattedTime = durationTimeFormatter.format(TIME_IN_MS); - expect(formattedTime).toEqual(TIME_AS_STRING); - - }); - - it("parses from Open MCT duration format to ms", () => { - const TIME_IN_MS = 2000; - const TIME_AS_STRING = "00:00:02"; - - const parsedTime = durationTimeFormatter.parse(TIME_AS_STRING); - expect(parsedTime).toEqual(TIME_IN_MS); - }); - - it("validates correctly formatted Open MCT duration strings.", () => { - const TIME_AS_STRING = "00:00:02"; - - const isValid = durationTimeFormatter.validate(TIME_AS_STRING); - expect(isValid).toBeTrue(); - }); + const isValid = utcTimeFormatter.validate(TIME_AS_STRING); + expect(isValid).toBeTrue(); }); + }); + + describe('Duration Format', () => { + let durationTimeFormatter; + + beforeEach(() => { + durationTimeFormatter = openmct.telemetry.getFormatter(DURATION_FORMAT_KEY); + }); + + it('is installed by the plugin', () => { + expect(durationTimeFormatter).toBeDefined(); + }); + + it('formats from ms into Open MCT duration format', () => { + const TIME_IN_MS = 2000; + const TIME_AS_STRING = '00:00:02'; + + const formattedTime = durationTimeFormatter.format(TIME_IN_MS); + expect(formattedTime).toEqual(TIME_AS_STRING); + }); + + it('parses from Open MCT duration format to ms', () => { + const TIME_IN_MS = 2000; + const TIME_AS_STRING = '00:00:02'; + + const parsedTime = durationTimeFormatter.parse(TIME_AS_STRING); + expect(parsedTime).toEqual(TIME_IN_MS); + }); + + it('validates correctly formatted Open MCT duration strings.', () => { + const TIME_AS_STRING = '00:00:02'; + + const isValid = durationTimeFormatter.validate(TIME_AS_STRING); + expect(isValid).toBeTrue(); + }); + }); }); diff --git a/src/plugins/viewDatumAction/ViewDatumAction.js b/src/plugins/viewDatumAction/ViewDatumAction.js index 8285a5a47a..6c4f7a8218 100644 --- a/src/plugins/viewDatumAction/ViewDatumAction.js +++ b/src/plugins/viewDatumAction/ViewDatumAction.js @@ -24,51 +24,51 @@ import MetadataListView from './components/MetadataList.vue'; import Vue from 'vue'; export default class ViewDatumAction { - constructor(openmct) { - this.name = 'View Full Datum'; - this.key = 'viewDatumAction'; - this.description = 'View full value of datum received'; - this.cssClass = 'icon-object'; + constructor(openmct) { + this.name = 'View Full Datum'; + this.key = 'viewDatumAction'; + this.description = 'View full value of datum received'; + this.cssClass = 'icon-object'; - this._openmct = openmct; + this._openmct = openmct; + } + invoke(objectPath, view) { + let viewContext = view.getViewContext && view.getViewContext(); + const row = viewContext.row; + let attributes = row.getDatum && row.getDatum(); + let component = new Vue({ + components: { + MetadataListView + }, + provide: { + name: this.name, + attributes + }, + template: '' + }); + + this._openmct.overlays.overlay({ + element: component.$mount().$el, + size: 'large', + dismissable: true, + onDestroy: () => { + component.$destroy(); + } + }); + } + appliesTo(objectPath, view = {}) { + let viewContext = (view.getViewContext && view.getViewContext()) || {}; + const row = viewContext.row; + if (!row) { + return false; } - invoke(objectPath, view) { - let viewContext = view.getViewContext && view.getViewContext(); - const row = viewContext.row; - let attributes = row.getDatum && row.getDatum(); - let component = new Vue ({ - components: { - MetadataListView - }, - provide: { - name: this.name, - attributes - }, - template: '' - }); - this._openmct.overlays.overlay({ - element: component.$mount().$el, - size: 'large', - dismissable: true, - onDestroy: () => { - component.$destroy(); - } - }); + let datum = row.getDatum; + let enabled = row.viewDatumAction; + if (enabled && datum) { + return true; } - appliesTo(objectPath, view = {}) { - let viewContext = (view.getViewContext && view.getViewContext()) || {}; - const row = viewContext.row; - if (!row) { - return false; - } - let datum = row.getDatum; - let enabled = row.viewDatumAction; - if (enabled && datum) { - return true; - } - - return false; - } + return false; + } } diff --git a/src/plugins/viewDatumAction/components/MetadataList.vue b/src/plugins/viewDatumAction/components/MetadataList.vue index f1d03c707a..8676f0e475 100644 --- a/src/plugins/viewDatumAction/components/MetadataList.vue +++ b/src/plugins/viewDatumAction/components/MetadataList.vue @@ -20,26 +20,23 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/viewDatumAction/components/metadata-list.scss b/src/plugins/viewDatumAction/components/metadata-list.scss index 778d4f3ded..4ecf235bf3 100644 --- a/src/plugins/viewDatumAction/components/metadata-list.scss +++ b/src/plugins/viewDatumAction/components/metadata-list.scss @@ -1,30 +1,30 @@ .c-attributes-view { - display: flex; - flex: 1 1 auto; - flex-direction: column; + display: flex; + flex: 1 1 auto; + flex-direction: column; - > * { - flex: 0 0 auto; + > * { + flex: 0 0 auto; + } + + &__content { + $p: 3px; + + display: grid; + grid-template-columns: max-content 1fr; + grid-row-gap: $p; + + li { + display: contents; } - &__content { - $p: 3px; - - display: grid; - grid-template-columns: max-content 1fr; - grid-row-gap: $p; - - li { display: contents; } - - [class*="__grid-item"] { - border-bottom: 1px solid rgba(#999, 0.2); - padding: 0 5px $p 0; - } - - [class*="__label"] { - opacity: 0.8; - } + [class*='__grid-item'] { + border-bottom: 1px solid rgba(#999, 0.2); + padding: 0 5px $p 0; } - + [class*='__label'] { + opacity: 0.8; + } + } } diff --git a/src/plugins/viewDatumAction/plugin.js b/src/plugins/viewDatumAction/plugin.js index ca586fbf6d..0e9842aaeb 100644 --- a/src/plugins/viewDatumAction/plugin.js +++ b/src/plugins/viewDatumAction/plugin.js @@ -23,7 +23,7 @@ import ViewDatumAction from './ViewDatumAction.js'; export default function plugin() { - return function install(openmct) { - openmct.actions.register(new ViewDatumAction(openmct)); - }; + return function install(openmct) { + openmct.actions.register(new ViewDatumAction(openmct)); + }; } diff --git a/src/plugins/viewDatumAction/pluginSpec.js b/src/plugins/viewDatumAction/pluginSpec.js index 4dfeeb1d3d..203048cfd6 100644 --- a/src/plugins/viewDatumAction/pluginSpec.js +++ b/src/plugins/viewDatumAction/pluginSpec.js @@ -19,75 +19,73 @@ * 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 { createOpenMct, resetApplicationState } from 'utils/testing'; -describe("the plugin", () => { - let openmct; - let viewDatumAction; - let mockObjectPath; - let mockView; - let mockDatum; +describe('the plugin', () => { + let openmct; + let viewDatumAction; + let mockObjectPath; + let mockView; + let mockDatum; - beforeEach((done) => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - openmct.on('start', done); - openmct.startHeadless(); + openmct.on('start', done); + openmct.startHeadless(); - viewDatumAction = openmct.actions._allActions.viewDatumAction; + viewDatumAction = openmct.actions._allActions.viewDatumAction; - mockObjectPath = [{ - name: 'mock object', - type: 'telemetry-table', - identifier: { - key: 'mock-object', - namespace: '' + mockObjectPath = [ + { + name: 'mock object', + type: 'telemetry-table', + identifier: { + key: 'mock-object', + namespace: '' + } + } + ]; + + mockDatum = { + time: 123456789, + sin: 0.4455512, + cos: 0.4455512 + }; + + mockView = { + getViewContext: () => { + return { + row: { + viewDatumAction: true, + getDatum: () => { + return mockDatum; } - }]; - - mockDatum = { - time: 123456789, - sin: 0.4455512, - cos: 0.4455512 + } }; + } + }; + }); - mockView = { - getViewContext: () => { - return { - row: { - viewDatumAction: true, - getDatum: () => { - return mockDatum; - } - } - }; - } - }; + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('installs the view datum action', () => { + expect(viewDatumAction).toBeDefined(); + }); + + describe('when invoked', () => { + beforeEach(() => { + openmct.overlays.overlay = function (options) {}; + + spyOn(openmct.overlays, 'overlay'); + + viewDatumAction.invoke(mockObjectPath, mockView); }); - afterEach(() => { - return resetApplicationState(openmct); - }); - - it('installs the view datum action', () => { - expect(viewDatumAction).toBeDefined(); - }); - - describe('when invoked', () => { - - beforeEach(() => { - openmct.overlays.overlay = function (options) {}; - - spyOn(openmct.overlays, 'overlay'); - - viewDatumAction.invoke(mockObjectPath, mockView); - }); - - it('creates an overlay', () => { - expect(openmct.overlays.overlay).toHaveBeenCalled(); - }); + it('creates an overlay', () => { + expect(openmct.overlays.overlay).toHaveBeenCalled(); }); + }); }); diff --git a/src/plugins/viewLargeAction/plugin.js b/src/plugins/viewLargeAction/plugin.js index 0b3d9fb7b9..4335f678eb 100644 --- a/src/plugins/viewLargeAction/plugin.js +++ b/src/plugins/viewLargeAction/plugin.js @@ -23,7 +23,7 @@ import ViewLargeAction from './viewLargeAction.js'; export default function plugin() { - return function install(openmct) { - openmct.actions.register(new ViewLargeAction(openmct)); - }; + return function install(openmct) { + openmct.actions.register(new ViewLargeAction(openmct)); + }; } diff --git a/src/plugins/viewLargeAction/viewLargeAction.js b/src/plugins/viewLargeAction/viewLargeAction.js index 04b0d8810e..30f6e647db 100644 --- a/src/plugins/viewLargeAction/viewLargeAction.js +++ b/src/plugins/viewLargeAction/viewLargeAction.js @@ -25,71 +25,74 @@ import Preview from '@/ui/preview/Preview.vue'; import Vue from 'vue'; export default class ViewLargeAction { - constructor(openmct) { - this.openmct = openmct; + constructor(openmct) { + this.openmct = openmct; - this.cssClass = 'icon-items-expand'; - this.description = 'View Large'; - this.group = 'windowing'; - this.key = 'large.view'; - this.name = 'Large View'; - this.priority = 1; - this.showInStatusBar = true; + this.cssClass = 'icon-items-expand'; + this.description = 'View Large'; + this.group = 'windowing'; + this.key = 'large.view'; + this.name = 'Large View'; + this.priority = 1; + this.showInStatusBar = true; + } + + invoke(objectPath, view) { + performance.mark('viewlarge.start'); + const childElement = view?.parentElement?.firstChild; + if (!childElement) { + const message = 'ViewLargeAction: missing element'; + this.openmct.notifications.error(message); + throw new Error(message); } - invoke(objectPath, view) { - performance.mark('viewlarge.start'); - const childElement = view?.parentElement?.firstChild; - if (!childElement) { - const message = "ViewLargeAction: missing element"; - this.openmct.notifications.error(message); - throw new Error(message); - } + this._expand(objectPath, view); + } - this._expand(objectPath, view); - } + appliesTo(objectPath, view) { + const childElement = view?.parentElement?.firstChild; - appliesTo(objectPath, view) { - const childElement = view?.parentElement?.firstChild; + return ( + childElement && + !childElement.classList.contains('js-main-container') && + !this.openmct.router.isNavigatedObject(objectPath) + ); + } - return childElement && !childElement.classList.contains('js-main-container') - && !this.openmct.router.isNavigatedObject(objectPath); - } + _expand(objectPath, view) { + const element = this._getPreview(objectPath, view); + view.onPreviewModeChange?.({ isPreviewing: true }); - _expand(objectPath, view) { - const element = this._getPreview(objectPath, view); - view.onPreviewModeChange?.({ isPreviewing: true }); + this.overlay = this.openmct.overlays.overlay({ + element, + size: 'large', + autoHide: false, + onDestroy: () => { + this.preview.$destroy(); + this.preview = undefined; + delete this.preview; + view.onPreviewModeChange?.(); + } + }); + } - this.overlay = this.openmct.overlays.overlay({ - element, - size: 'large', - autoHide: false, - onDestroy: () => { - this.preview.$destroy(); - this.preview = undefined; - delete this.preview; - view.onPreviewModeChange?.(); - } - }); - } + _getPreview(objectPath, view) { + this.preview = new Vue({ + components: { + Preview + }, + provide: { + openmct: this.openmct, + objectPath + }, + data() { + return { + view + }; + }, + template: '' + }); - _getPreview(objectPath, view) { - this.preview = new Vue({ - components: { - Preview - }, - provide: { - openmct: this.openmct, - objectPath - }, - data() { - return { - view - }; - }, - template: '' - }); - - return this.preview.$mount().$el; - } + return this.preview.$mount().$el; + } } diff --git a/src/plugins/webPage/WebPageViewProvider.js b/src/plugins/webPage/WebPageViewProvider.js index 43ba670eff..74630c9ad8 100644 --- a/src/plugins/webPage/WebPageViewProvider.js +++ b/src/plugins/webPage/WebPageViewProvider.js @@ -24,38 +24,38 @@ import WebPageComponent from './components/WebPage.vue'; import Vue from 'vue'; export default function WebPage(openmct) { - return { - key: 'webPage', - name: 'Web Page', - cssClass: 'icon-page', - canView: function (domainObject) { - return domainObject.type === 'webPage'; - }, - view: function (domainObject) { - let component; + return { + key: 'webPage', + name: 'Web Page', + cssClass: 'icon-page', + canView: function (domainObject) { + return domainObject.type === 'webPage'; + }, + view: function (domainObject) { + let component; - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - WebPageComponent: WebPageComponent - }, - provide: { - openmct, - domainObject - }, - template: '' - }); - }, - destroy: function (element) { - component.$destroy(); - component = undefined; - } - }; + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + WebPageComponent: WebPageComponent + }, + provide: { + openmct, + domainObject + }, + template: '' + }); }, - priority: function () { - return 1; + destroy: function (element) { + component.$destroy(); + component = undefined; } - }; + }; + }, + priority: function () { + return 1; + } + }; } diff --git a/src/plugins/webPage/components/WebPage.vue b/src/plugins/webPage/components/WebPage.vue index 4f53cd8d59..13ac6a487d 100644 --- a/src/plugins/webPage/components/WebPage.vue +++ b/src/plugins/webPage/components/WebPage.vue @@ -20,25 +20,25 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/webPage/plugin.js b/src/plugins/webPage/plugin.js index 7daf222e2a..0350bebc5a 100644 --- a/src/plugins/webPage/plugin.js +++ b/src/plugins/webPage/plugin.js @@ -23,23 +23,24 @@ import WebPageViewProvider from './WebPageViewProvider.js'; export default function plugin() { - return function install(openmct) { - openmct.objectViews.addProvider(new WebPageViewProvider(openmct)); + return function install(openmct) { + openmct.objectViews.addProvider(new WebPageViewProvider(openmct)); - openmct.types.addType('webPage', { - name: "Web Page", - description: "Embed a web page or web-based image in a resizeable window component. Note that the URL being embedded must allow iframing.", - creatable: true, - cssClass: 'icon-page', - form: [ - { - "key": "url", - "name": "URL", - "control": "textfield", - "required": true, - "cssClass": "l-input-lg" - } - ] - }); - }; + openmct.types.addType('webPage', { + name: 'Web Page', + description: + 'Embed a web page or web-based image in a resizeable window component. Note that the URL being embedded must allow iframing.', + creatable: true, + cssClass: 'icon-page', + form: [ + { + key: 'url', + name: 'URL', + control: 'textfield', + required: true, + cssClass: 'l-input-lg' + } + ] + }); + }; } diff --git a/src/plugins/webPage/pluginSpec.js b/src/plugins/webPage/pluginSpec.js index 3326cb90b0..e77fabbe1e 100644 --- a/src/plugins/webPage/pluginSpec.js +++ b/src/plugins/webPage/pluginSpec.js @@ -20,87 +20,85 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createOpenMct, resetApplicationState } from "utils/testing"; -import WebPagePlugin from "./plugin"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import WebPagePlugin from './plugin'; function getView(openmct, domainObj, objectPath) { - const applicableViews = openmct.objectViews.get(domainObj, objectPath); - const webpageView = applicableViews.find((viewProvider) => viewProvider.key === 'webPage'); + const applicableViews = openmct.objectViews.get(domainObj, objectPath); + const webpageView = applicableViews.find((viewProvider) => viewProvider.key === 'webPage'); - return webpageView.view(domainObj); + return webpageView.view(domainObj); } function destroyView(view) { - return view.destroy(); + return view.destroy(); } -describe("The web page plugin", function () { - let mockDomainObject; - let mockDomainObjectPath; - let openmct; - let element; - let child; - let view; +describe('The web page plugin', function () { + let mockDomainObject; + let mockDomainObjectPath; + let openmct; + let element; + let child; + let view; - beforeEach((done) => { - mockDomainObjectPath = [ - { - name: 'mock webpage', - type: 'webpage', - identifier: { - key: 'mock-webpage', - namespace: '' - } - } - ]; + beforeEach((done) => { + mockDomainObjectPath = [ + { + name: 'mock webpage', + type: 'webpage', + identifier: { + key: 'mock-webpage', + namespace: '' + } + } + ]; - mockDomainObject = { - displayFormat: "", - name: "Unnamed WebPage", - type: "webPage", - location: "f69c21ac-24ef-450c-8e2f-3d527087d285", - modified: 1627483839783, - url: "123", - displayText: "123", - persisted: 1627483839783, - id: "3d9c243d-dffb-446b-8474-d9931a99d679", - identifier: { - namespace: "", - key: "3d9c243d-dffb-446b-8474-d9931a99d679" - } - }; + mockDomainObject = { + displayFormat: '', + name: 'Unnamed WebPage', + type: 'webPage', + location: 'f69c21ac-24ef-450c-8e2f-3d527087d285', + modified: 1627483839783, + url: '123', + displayText: '123', + persisted: 1627483839783, + id: '3d9c243d-dffb-446b-8474-d9931a99d679', + identifier: { + namespace: '', + key: '3d9c243d-dffb-446b-8474-d9931a99d679' + } + }; - openmct = createOpenMct(); - openmct.install(new WebPagePlugin()); + openmct = createOpenMct(); + openmct.install(new WebPagePlugin()); - element = document.createElement('div'); - element.style.width = '640px'; - element.style.height = '480px'; - child = document.createElement('div'); - child.style.width = '640px'; - child.style.height = '480px'; - element.appendChild(child); + element = document.createElement('div'); + element.style.width = '640px'; + element.style.height = '480px'; + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + element.appendChild(child); - openmct.on('start', done); - openmct.startHeadless(); + openmct.on('start', done); + openmct.startHeadless(); + }); + afterEach(() => { + destroyView(view); + + return resetApplicationState(openmct); + }); + + describe('the view', () => { + beforeEach(() => { + view = getView(openmct, mockDomainObject, mockDomainObjectPath); + view.show(child, true); }); - afterEach(() => { - destroyView(view); - - return resetApplicationState(openmct); + it('provides a view', () => { + expect(view).toBeDefined(); }); - - describe('the view', () => { - beforeEach(() => { - view = getView(openmct, mockDomainObject, mockDomainObjectPath); - view.show(child, true); - }); - - it('provides a view', () => { - expect(view).toBeDefined(); - }); - }); - + }); }); diff --git a/src/selection/Selection.js b/src/selection/Selection.js index 1ee2624a6b..dd84f6b62f 100644 --- a/src/selection/Selection.js +++ b/src/selection/Selection.js @@ -28,217 +28,218 @@ import _ from 'lodash'; * @private */ export default class Selection extends EventEmitter { - constructor(openmct) { - super(); + constructor(openmct) { + super(); - this.openmct = openmct; - this.selected = []; + this.openmct = openmct; + this.selected = []; + } + /** + * Gets the selected object. + * @public + */ + get() { + return this.selected; + } + /** + * Selects the selectable object and emits the 'change' event. + * + * @param {object} selectable an object with element and context properties + * @param {Boolean} isMultiSelectEvent flag indication shift key is pressed or not + * @private + */ + select(selectable, isMultiSelectEvent) { + if (!Array.isArray(selectable)) { + selectable = [selectable]; } - /** - * Gets the selected object. - * @public - */ - get() { - return this.selected; + + let multiSelect = + isMultiSelectEvent && + this.parentSupportsMultiSelect(selectable) && + this.isPeer(selectable) && + !this.selectionContainsParent(selectable); + + if (multiSelect) { + this.handleMultiSelect(selectable); + } else { + this.handleSingleSelect(selectable); } - /** - * Selects the selectable object and emits the 'change' event. - * - * @param {object} selectable an object with element and context properties - * @param {Boolean} isMultiSelectEvent flag indication shift key is pressed or not - * @private - */ - select(selectable, isMultiSelectEvent) { - if (!Array.isArray(selectable)) { - selectable = [selectable]; - } - - let multiSelect = isMultiSelectEvent - && this.parentSupportsMultiSelect(selectable) - && this.isPeer(selectable) - && !this.selectionContainsParent(selectable); - - if (multiSelect) { - this.handleMultiSelect(selectable); - } else { - this.handleSingleSelect(selectable); - } + } + /** + * @private + */ + handleMultiSelect(selectable) { + if (this.elementSelected(selectable)) { + this.remove(selectable); + } else { + this.addSelectionAttributes(selectable); + this.selected.push(selectable); } - /** - * @private - */ - handleMultiSelect(selectable) { - if (this.elementSelected(selectable)) { - this.remove(selectable); - } else { - this.addSelectionAttributes(selectable); - this.selected.push(selectable); - } - this.emit('change', this.selected); + this.emit('change', this.selected); + } + /** + * @private + */ + handleSingleSelect(selectable) { + if (!_.isEqual([selectable], this.selected)) { + this.setSelectionStyles(selectable); + this.selected = [selectable]; + + this.emit('change', this.selected); } - /** - * @private - */ - handleSingleSelect(selectable) { - if (!_.isEqual([selectable], this.selected)) { - this.setSelectionStyles(selectable); - this.selected = [selectable]; + } + /** + * @private + */ + elementSelected(selectable) { + return this.selected.some((selectionPath) => _.isEqual(selectionPath, selectable)); + } + /** + * @private + */ + remove(selectable) { + this.selected = this.selected.filter((selectionPath) => !_.isEqual(selectionPath, selectable)); - this.emit('change', this.selected); - } + if (this.selected.length === 0) { + this.removeSelectionAttributes(selectable); + selectable[1].element.click(); // Select the parent if there is no selection. + } else { + this.removeSelectionAttributes(selectable, true); } - /** - * @private - */ - elementSelected(selectable) { - return this.selected.some(selectionPath => _.isEqual(selectionPath, selectable)); + } + /** + * @private + */ + setSelectionStyles(selectable) { + this.selected.forEach((selectionPath) => this.removeSelectionAttributes(selectionPath)); + this.addSelectionAttributes(selectable); + } + removeSelectionAttributes(selectionPath, keepParentStyle) { + if (selectionPath[0] && selectionPath[0].element) { + selectionPath[0].element.removeAttribute('s-selected'); } - /** - * @private - */ - remove(selectable) { - this.selected = this.selected.filter(selectionPath => !_.isEqual(selectionPath, selectable)); - if (this.selected.length === 0) { - this.removeSelectionAttributes(selectable); - selectable[1].element.click(); // Select the parent if there is no selection. - } else { - this.removeSelectionAttributes(selectable, true); - } + if (selectionPath[1] && selectionPath[1].element && !keepParentStyle) { + selectionPath[1].element.removeAttribute('s-selected-parent'); } - /** - * @private - */ - setSelectionStyles(selectable) { - this.selected.forEach(selectionPath => this.removeSelectionAttributes(selectionPath)); - this.addSelectionAttributes(selectable); + } + /** + * Adds selection attributes to the selected element and its parent. + * @private + */ + addSelectionAttributes(selectable) { + if (selectable[0] && selectable[0].element) { + selectable[0].element.setAttribute('s-selected', ''); } - removeSelectionAttributes(selectionPath, keepParentStyle) { - if (selectionPath[0] && selectionPath[0].element) { - selectionPath[0].element.removeAttribute('s-selected'); - } - if (selectionPath[1] && selectionPath[1].element && !keepParentStyle) { - selectionPath[1].element.removeAttribute('s-selected-parent'); - } + if (selectable[1] && selectable[1].element) { + selectable[1].element.setAttribute('s-selected-parent', ''); } - /** - * Adds selection attributes to the selected element and its parent. - * @private - */ - addSelectionAttributes(selectable) { - if (selectable[0] && selectable[0].element) { - selectable[0].element.setAttribute('s-selected', ""); - } - - if (selectable[1] && selectable[1].element) { - selectable[1].element.setAttribute('s-selected-parent', ""); - } + } + /** + * @private + */ + parentSupportsMultiSelect(selectable) { + return selectable[1] && selectable[1].context.supportsMultiSelect; + } + /** + * @private + */ + selectionContainsParent(selectable) { + return this.selected.some((selectionPath) => _.isEqual(selectionPath[0], selectable[1])); + } + /** + * @private + */ + isPeer(selectable) { + return this.selected.some((selectionPath) => _.isEqual(selectionPath[1], selectable[1])); + } + /** + * @private + */ + isSelectable(element) { + if (!element) { + return false; } - /** - * @private - */ - parentSupportsMultiSelect(selectable) { - return selectable[1] && selectable[1].context.supportsMultiSelect; + + return Boolean(element.closest('[data-selectable]')); + } + /** + * @private + */ + capture(selectable) { + let capturingContainsSelectable = this.capturing && this.capturing.includes(selectable); + + if (!this.capturing || capturingContainsSelectable) { + this.capturing = []; } - /** - * @private - */ - selectionContainsParent(selectable) { - return this.selected.some(selectionPath => _.isEqual(selectionPath[0], selectable[1])); + + this.capturing.push(selectable); + } + /** + * @private + */ + selectCapture(selectable, event) { + if (!this.capturing) { + return; } - /** - * @private - */ - isPeer(selectable) { - return this.selected.some(selectionPath => _.isEqual(selectionPath[1], selectable[1])); + + let reversedCapturing = this.capturing.reverse(); + delete this.capturing; + this.select(reversedCapturing, event.shiftKey); + } + /** + * Attaches the click handlers to the element. + * + * @param element an html element + * @param context object which defines item or other arbitrary properties. + * e.g. { + * item: domainObject, + * elementProxy: element, + * controller: fixedController + * } + * @param select a flag to select the element if true + * @returns a function that removes the click handlers from the element + * @public + */ + selectable(element, context, select) { + if (!this.isSelectable(element)) { + return () => {}; } - /** - * @private - */ - isSelectable(element) { - if (!element) { - return false; - } - return Boolean(element.closest('[data-selectable]')); + let selectable = { + context: context, + element: element + }; + + const capture = this.capture.bind(this, selectable); + const selectCapture = this.selectCapture.bind(this, selectable); + let removeMutable = false; + + element.addEventListener('click', capture, true); + element.addEventListener('click', selectCapture); + + if (context.item && context.item.isMutable !== true) { + removeMutable = true; + context.item = this.openmct.objects.toMutable(context.item); } - /** - * @private - */ - capture(selectable) { - let capturingContainsSelectable = this.capturing && this.capturing.includes(selectable); - if (!this.capturing || capturingContainsSelectable) { - this.capturing = []; - } - - this.capturing.push(selectable); + if (select) { + if (typeof select === 'object') { + element.dispatchEvent(select); + } else if (typeof select === 'boolean') { + element.click(); + } } - /** - * @private - */ - selectCapture(selectable, event) { - if (!this.capturing) { - return; - } - let reversedCapturing = this.capturing.reverse(); - delete this.capturing; - this.select(reversedCapturing, event.shiftKey); - } - /** - * Attaches the click handlers to the element. - * - * @param element an html element - * @param context object which defines item or other arbitrary properties. - * e.g. { - * item: domainObject, - * elementProxy: element, - * controller: fixedController - * } - * @param select a flag to select the element if true - * @returns a function that removes the click handlers from the element - * @public - */ - selectable(element, context, select) { - if (!this.isSelectable(element)) { - return () => { }; - } + return function () { + element.removeEventListener('click', capture, true); + element.removeEventListener('click', selectCapture); - let selectable = { - context: context, - element: element - }; - - const capture = this.capture.bind(this, selectable); - const selectCapture = this.selectCapture.bind(this, selectable); - let removeMutable = false; - - element.addEventListener('click', capture, true); - element.addEventListener('click', selectCapture); - - if (context.item && context.item.isMutable !== true) { - removeMutable = true; - context.item = this.openmct.objects.toMutable(context.item); - } - - if (select) { - if (typeof select === 'object') { - element.dispatchEvent(select); - } else if (typeof select === 'boolean') { - element.click(); - } - } - - return (function () { - element.removeEventListener('click', capture, true); - element.removeEventListener('click', selectCapture); - - if (context.item !== undefined && context.item.isMutable && removeMutable === true) { - this.openmct.objects.destroyMutable(context.item); - } - }).bind(this); - } + if (context.item !== undefined && context.item.isMutable && removeMutable === true) { + this.openmct.objects.destroyMutable(context.item); + } + }.bind(this); + } } diff --git a/src/styles/_about.scss b/src/styles/_about.scss index 3f2a0e3a15..16fc97484d 100644 --- a/src/styles/_about.scss +++ b/src/styles/_about.scss @@ -22,105 +22,107 @@ // Used by About screen, licenses, etc. .c-splash-image { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + background-image: url('../ui/layout/assets/images/bg-splash.jpg'); + margin-top: 30px; // Don't overlap with close "X" button + + &:before, + &:after { background-position: center; background-repeat: no-repeat; - background-size: cover; - background-image: url('../ui/layout/assets/images/bg-splash.jpg'); - margin-top: 30px; // Don't overlap with close "X" button + position: absolute; + background-image: url('../ui/layout/assets/images/logo-openmct-shdw.svg'); + background-size: contain; + content: ''; + } - &:before, - &:after { - background-position: center; - background-repeat: no-repeat; - position: absolute; - background-image: url('../ui/layout/assets/images/logo-openmct-shdw.svg'); - background-size: contain; - content: ''; - } + &:before { + // NASA logo, dude + $w: 5%; + $m: 10px; + background-image: url('../ui/layout/assets/images/logo-nasa.svg'); + top: $m; + right: auto; + bottom: auto; + left: $m; + height: auto; + width: $w * 2; + padding-bottom: $w; + padding-top: $w; + } - &:before { - // NASA logo, dude - $w: 5%; - $m: 10px; - background-image: url('../ui/layout/assets/images/logo-nasa.svg'); - top: $m; - right: auto; - bottom: auto; - left: $m; - height: auto; - width: $w * 2; - padding-bottom: $w; - padding-top: $w; - } - - &:after { - // App logo - $d: 25%; - top: $d; - right: $d; - bottom: $d; - left: $d; - } + &:after { + // App logo + $d: 25%; + top: $d; + right: $d; + bottom: $d; + left: $d; + } } .c-about { - &--splash { - // Large initial image after click on app logo with text beneath - @include abs(); - display: flex; - flex-direction: column; - } + &--splash { + // Large initial image after click on app logo with text beneath + @include abs(); + display: flex; + flex-direction: column; + } + > * + * { + margin-top: $interiorMargin; + } + + &__image, + &__text { + flex: 1 1 auto; + } + + &__image { + height: 35%; + } + + &__text { + height: 65%; + overflow: auto; > * + * { - margin-top: $interiorMargin; + border-top: 1px solid $colorInteriorBorder; + margin-top: 1em; } + } - &__image, - &__text { - flex: 1 1 auto; + &--licenses { + padding: 0 10%; + .c-license { + + .c-license { + border-top: 1px solid $colorInteriorBorder; + margin-top: 2em; + } } + } - &__image { - height: 35%; - } + a { + color: $colorAboutLink; + } - &__text { - height: 65%; - overflow: auto; - > * + * { - border-top: 1px solid $colorInteriorBorder; - margin-top: 1em; - } - } + em { + color: pushBack($colorBodyFg, 20%); + } - &--licenses { - padding: 0 10%; - .c-license { - + .c-license { - border-top: 1px solid $colorInteriorBorder; - margin-top: 2em; - } - } - } + h1, + h2, + h3 { + font-weight: normal; + margin-bottom: 0.25em; + } - a { - color: $colorAboutLink; - } + h1 { + font-size: 2.25em; + } - em { - color: pushBack($colorBodyFg, 20%); - } - - h1, h2, h3 { - font-weight: normal; - margin-bottom: .25em; - } - - h1 { - font-size: 2.25em; - } - - h2 { - font-size: 1.5em; - } + h2 { + font-size: 1.5em; + } } diff --git a/src/styles/_animations.scss b/src/styles/_animations.scss index f95e73560c..d62a663402 100644 --- a/src/styles/_animations.scss +++ b/src/styles/_animations.scss @@ -1,91 +1,101 @@ @keyframes rotation { - 100% { transform: rotate(360deg); } + 100% { + transform: rotate(360deg); + } } @keyframes rotation-centered { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } @keyframes clock-hands { - 0% { transform: translate(-50%, -50%) rotate(0deg); } - 100% { transform: translate(-50%, -50%) rotate(360deg); } + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } } @keyframes clock-hands-sticky { - 0% { - transform: translate(-50%, -50%) rotate(0deg); - } - 7% { - transform: translate(-50%, -50%) rotate(0deg); - } - 8% { - transform: translate(-50%, -50%) rotate(30deg); - } - 15% { - transform: translate(-50%, -50%) rotate(30deg); - } - 16% { - transform: translate(-50%, -50%) rotate(60deg); - } - 24% { - transform: translate(-50%, -50%) rotate(60deg); - } - 25% { - transform: translate(-50%, -50%) rotate(90deg); - } - 32% { - transform: translate(-50%, -50%) rotate(90deg); - } - 33% { - transform: translate(-50%, -50%) rotate(120deg); - } - 40% { - transform: translate(-50%, -50%) rotate(120deg); - } - 41% { - transform: translate(-50%, -50%) rotate(150deg); - } - 49% { - transform: translate(-50%, -50%) rotate(150deg); - } - 50% { - transform: translate(-50%, -50%) rotate(180deg); - } - 57% { - transform: translate(-50%, -50%) rotate(180deg); - } - 58% { - transform: translate(-50%, -50%) rotate(210deg); - } - 65% { - transform: translate(-50%, -50%) rotate(210deg); - } - 66% { - transform: translate(-50%, -50%) rotate(240deg); - } - 74% { - transform: translate(-50%, -50%) rotate(240deg); - } - 75% { - transform: translate(-50%, -50%) rotate(270deg); - } - 82% { - transform: translate(-50%, -50%) rotate(270deg); - } - 83% { - transform: translate(-50%, -50%) rotate(300deg); - } - 90% { - transform: translate(-50%, -50%) rotate(300deg); - } - 91% { - transform: translate(-50%, -50%) rotate(330deg); - } - 99% { - transform: translate(-50%, -50%) rotate(330deg); - } - 100% { - transform: translate(-50%, -50%) rotate(360deg); - } + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 7% { + transform: translate(-50%, -50%) rotate(0deg); + } + 8% { + transform: translate(-50%, -50%) rotate(30deg); + } + 15% { + transform: translate(-50%, -50%) rotate(30deg); + } + 16% { + transform: translate(-50%, -50%) rotate(60deg); + } + 24% { + transform: translate(-50%, -50%) rotate(60deg); + } + 25% { + transform: translate(-50%, -50%) rotate(90deg); + } + 32% { + transform: translate(-50%, -50%) rotate(90deg); + } + 33% { + transform: translate(-50%, -50%) rotate(120deg); + } + 40% { + transform: translate(-50%, -50%) rotate(120deg); + } + 41% { + transform: translate(-50%, -50%) rotate(150deg); + } + 49% { + transform: translate(-50%, -50%) rotate(150deg); + } + 50% { + transform: translate(-50%, -50%) rotate(180deg); + } + 57% { + transform: translate(-50%, -50%) rotate(180deg); + } + 58% { + transform: translate(-50%, -50%) rotate(210deg); + } + 65% { + transform: translate(-50%, -50%) rotate(210deg); + } + 66% { + transform: translate(-50%, -50%) rotate(240deg); + } + 74% { + transform: translate(-50%, -50%) rotate(240deg); + } + 75% { + transform: translate(-50%, -50%) rotate(270deg); + } + 82% { + transform: translate(-50%, -50%) rotate(270deg); + } + 83% { + transform: translate(-50%, -50%) rotate(300deg); + } + 90% { + transform: translate(-50%, -50%) rotate(300deg); + } + 91% { + transform: translate(-50%, -50%) rotate(330deg); + } + 99% { + transform: translate(-50%, -50%) rotate(330deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } } diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss index 4f43d85252..8c99132946 100644 --- a/src/styles/_constants-espresso.scss +++ b/src/styles/_constants-espresso.scss @@ -23,36 +23,36 @@ /************************************************** ESPRESSO THEME CONSTANTS */ // Fonts -$heroFont: "Helvetica Neue", Helvetica, Arial, sans-serif; +$heroFont: 'Helvetica Neue', Helvetica, Arial, sans-serif; $headerFont: $heroFont; $bodyFont: $heroFont; @mixin heroFont($size: 1em) { - font-family: $heroFont; - font-size: $size; + font-family: $heroFont; + font-size: $size; } @mixin headerFont($size: 1em) { - font-family: $headerFont; - font-size: $size; + font-family: $headerFont; + font-size: $size; } @mixin bodyFont($size: 1em) { - font-family: $bodyFont; - font-size: $size; + font-family: $bodyFont; + font-size: $size; } // Functions @function buttonBg($c: $colorBtnBg) { - @return linear-gradient(lighten($c, 5%), $c); + @return linear-gradient(lighten($c, 5%), $c); } @function pullForward($val, $amt) { - @return lighten($val, $amt); + @return lighten($val, $amt); } @function pushBack($val, $amt) { - @return darken($val, $amt); + @return darken($val, $amt); } // Constants @@ -73,8 +73,10 @@ $colorHeadFg: $colorBodyFg; $colorKey: #0099cc; $colorKeyFg: #fff; $colorKeyHov: lighten($colorKey, 10%); -$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%) contrast(101%); -$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%) contrast(100%); +$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%) + contrast(101%); +$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%) + contrast(100%); $colorKeySelectedBg: $colorKey; $uiColor: #0093ff; // Resize bars, splitter bars, etc. $colorInteriorBorder: rgba($colorBodyFg, 0.2); @@ -101,11 +103,14 @@ $sideBarHeaderFg: rgba($colorBodyFg, 0.7); $colorStatusFg: #888; $colorStatusDefault: #ccc; $colorStatusInfo: #60ba7b; -$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%) contrast(92%); +$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%) + contrast(92%); $colorStatusAlert: #ffb66c; -$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%) contrast(101%); +$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%) + contrast(101%); $colorStatusError: #da0004; -$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%) contrast(115%); +$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%) + contrast(115%); $colorStatusBtnBg: #666; // Where is this used? $colorStatusPartialBg: #3f5e8b; $colorStatusCompleteBg: #457638; @@ -114,11 +119,11 @@ $colorAlertFg: #fff; $colorError: #ff3c00; $colorErrorFg: #fff; $colorWarningHi: #990000; -$colorWarningHiFg: #FF9594; +$colorWarningHiFg: #ff9594; $colorWarningLo: #ff9900; $colorWarningLoFg: #523400; $colorDiagnostic: #a4b442; -$colorDiagnosticFg: #39461A; +$colorDiagnosticFg: #39461a; $colorCommand: #3693bd; $colorCommandFg: #fff; $colorInfo: #2294a2; @@ -164,7 +169,10 @@ $borderMissing: 1px dashed $colorAlert !important; $editUIColor: $uiColor; // Base color $editUIColorBg: $editUIColor; $editUIColorFg: #fff; -$editUIColorHov: pullForward(saturate($uiColor, 10%), 10%); // Hover color when $editUIColor is applied as a base color +$editUIColorHov: pullForward( + saturate($uiColor, 10%), + 10% +); // Hover color when $editUIColor is applied as a base color $editUIBaseColor: #344b8d; // Base color, toolbar bg $editUIBaseColorHov: pullForward($editUIBaseColor, 20%); $editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc. @@ -183,7 +191,10 @@ $editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make h $editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color $editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px; $editFrameMovebarColorBg: $editFrameColor; // Movebar bg color -$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text +$editFrameMovebarColorFg: pullForward( + $editFrameMovebarColorBg, + 20% +); // Grippy lines, container size text $editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style $editFrameHovMovebarColorFg: pullForward($editFrameMovebarColorFg, 10%); $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); // Selected style @@ -241,7 +252,7 @@ $controlDisabledOpacity: 0.2; $colorMenuBg: $colorBodyBg; $colorMenuFg: $colorBodyFg; $colorMenuIc: $colorKey; -$filterMenu: brightness(1.4); +$filterMenu: brightness(1.4); $colorMenuHovBg: rgba($colorKey, 0.5); $colorMenuHovFg: $colorBodyFgEm; $colorMenuHovIc: $colorMenuHovFg; @@ -314,25 +325,25 @@ $colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%); // Staleness $colorTelemStale: cyan; -$colorTelemStaleFg: #002A2A; +$colorTelemStaleFg: #002a2a; $styleTelemStale: italic; // Limits -$colorLimitYellowBg: #B18B05; -$colorLimitYellowFg: #FEEEB5; -$colorLimitYellowIc: #FDC707; -$colorLimitOrangeBg: #B36B00; -$colorLimitOrangeFg: #FFE0B2; +$colorLimitYellowBg: #b18b05; +$colorLimitYellowFg: #feeeb5; +$colorLimitYellowIc: #fdc707; +$colorLimitOrangeBg: #b36b00; +$colorLimitOrangeFg: #ffe0b2; $colorLimitOrangeIc: #ff9900; $colorLimitRedBg: #940000; $colorLimitRedFg: #ffa489; $colorLimitRedIc: #ff4222; -$colorLimitPurpleBg: #891BB3; -$colorLimitPurpleFg: #EDBEFF; +$colorLimitPurpleBg: #891bb3; +$colorLimitPurpleFg: #edbeff; $colorLimitPurpleIc: #c327ff; -$colorLimitCyanBg: #4BA6B3; -$colorLimitCyanFg: #D3FAFF; -$colorLimitCyanIc: #6BEDFF; +$colorLimitCyanBg: #4ba6b3; +$colorLimitCyanFg: #d3faff; +$colorLimitCyanIc: #6bedff; // Events $colorEventPurpleFg: #6433ff; @@ -473,36 +484,34 @@ $transIn: all $transInTime ease-in-out; $transOut: all $transOutTime ease-in-out; $transInTransform: transform $transInTime ease-in-out; $transOutTransform: transform $transOutTime ease-in-out; -$transInBounce: all 200ms cubic-bezier(.47,.01,.25,1.5); -$transInBounceBig: all 300ms cubic-bezier(.2,1.6,.6,3); +$transInBounce: all 200ms cubic-bezier(0.47, 0.01, 0.25, 1.5); +$transInBounceBig: all 300ms cubic-bezier(0.2, 1.6, 0.6, 3); // Discrete items, like Notebook entries, Widget rules $createBtnTextTransform: uppercase; -$colorDiscreteItemBg: rgba($colorBodyFg,0.1); -$colorDiscreteItemCurrentBg: rgba($colorOk,0.3); +$colorDiscreteItemBg: rgba($colorBodyFg, 0.1); +$colorDiscreteItemCurrentBg: rgba($colorOk, 0.3); $scrollContainer: $colorBodyBg; -; - @mixin discreteItem() { - background: $colorDiscreteItemBg; - border: none; - border-radius: $controlCr; + background: $colorDiscreteItemBg; + border: none; + border-radius: $controlCr; - .c-input-inline:hover { - background: $colorBodyBg; - } + .c-input-inline:hover { + background: $colorBodyBg; + } - &--current-match { - background: $colorDiscreteItemCurrentBg; - } + &--current-match { + background: $colorDiscreteItemCurrentBg; + } } @mixin discreteItemInnerElem() { - border: 1px solid rgba(#fff, 0.1); - border-radius: $controlCr; + border: 1px solid rgba(#fff, 0.1); + border-radius: $controlCr; } @mixin themedButton($c: $colorBtnBg) { - background: linear-gradient(pullForward($c, 5%), $c); - box-shadow: rgba(black, 0.5) 0 0.5px 2px; + background: linear-gradient(pullForward($c, 5%), $c); + box-shadow: rgba(black, 0.5) 0 0.5px 2px; } diff --git a/src/styles/_constants-maelstrom.scss b/src/styles/_constants-maelstrom.scss index 385c304ad5..73c4dc4d14 100644 --- a/src/styles/_constants-maelstrom.scss +++ b/src/styles/_constants-maelstrom.scss @@ -30,33 +30,33 @@ $headerFont: 'Michroma', sans-serif; $bodyFont: 'Chakra Petch', sans-serif; @mixin heroFont($size: 1em) { - font-family: $heroFont; - font-size: $size; + font-family: $heroFont; + font-size: $size; } @mixin headerFont($size: 1em) { - font-family: $headerFont; - font-size: $size * 0.8; // This font is comparatively large, so reduce it a bit - text-transform: uppercase; - word-spacing: 0.25em; + font-family: $headerFont; + font-size: $size * 0.8; // This font is comparatively large, so reduce it a bit + text-transform: uppercase; + word-spacing: 0.25em; } @mixin bodyFont($size: 1em) { - font-family: $bodyFont; - font-size: $size; + font-family: $bodyFont; + font-size: $size; } // Functions @function buttonBg($c: $colorBtnBg) { - @return linear-gradient(lighten($c, 5%), $c); + @return linear-gradient(lighten($c, 5%), $c); } @function pullForward($val, $amt) { - @return lighten($val, $amt); + @return lighten($val, $amt); } @function pushBack($val, $amt) { - @return darken($val, $amt); + @return darken($val, $amt); } // Constants @@ -77,8 +77,10 @@ $colorHeadFg: $colorBodyFg; $colorKey: #0099cc; $colorKeyFg: #fff; $colorKeyHov: #26d8ff; -$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%) contrast(101%); -$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%) contrast(100%); +$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%) + contrast(101%); +$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%) + contrast(100%); $colorKeySelectedBg: $colorKey; $uiColor: #0093ff; // Resize bars, splitter bars, etc. $colorInteriorBorder: rgba($colorBodyFg, 0.2); @@ -105,11 +107,14 @@ $sideBarHeaderFg: rgba($colorBodyFg, 0.7); $colorStatusFg: #999; $colorStatusDefault: #ccc; $colorStatusInfo: #60ba7b; -$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%) contrast(92%); +$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%) + contrast(92%); $colorStatusAlert: #ffb66c; -$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%) contrast(101%); +$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%) + contrast(101%); $colorStatusError: #da0004; -$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%) contrast(115%); +$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%) + contrast(115%); $colorStatusBtnBg: #666; // Where is this used? $colorStatusPartialBg: #3f5e8b; $colorStatusCompleteBg: #457638; @@ -118,11 +123,11 @@ $colorAlertFg: #fff; $colorError: #ff3c00; $colorErrorFg: #fff; $colorWarningHi: #990000; -$colorWarningHiFg: #FF9594; +$colorWarningHiFg: #ff9594; $colorWarningLo: #ff9900; $colorWarningLoFg: #523400; $colorDiagnostic: #a4b442; -$colorDiagnosticFg: #39461A; +$colorDiagnosticFg: #39461a; $colorCommand: #3693bd; $colorCommandFg: #fff; $colorInfo: #2294a2; @@ -168,7 +173,10 @@ $borderMissing: 1px dashed $colorAlert !important; $editUIColor: $uiColor; // Base color $editUIColorBg: $editUIColor; $editUIColorFg: #fff; -$editUIColorHov: pullForward(saturate($uiColor, 10%), 10%); // Hover color when $editUIColor is applied as a base color +$editUIColorHov: pullForward( + saturate($uiColor, 10%), + 10% +); // Hover color when $editUIColor is applied as a base color $editUIBaseColor: #344b8d; // Base color, toolbar bg $editUIBaseColorHov: pullForward($editUIBaseColor, 20%); $editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc. @@ -187,7 +195,10 @@ $editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make h $editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color $editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px; $editFrameMovebarColorBg: $editFrameColor; // Movebar bg color -$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text +$editFrameMovebarColorFg: pullForward( + $editFrameMovebarColorBg, + 20% +); // Grippy lines, container size text $editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style $editFrameHovMovebarColorFg: pullForward($editFrameMovebarColorFg, 10%); $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); // Selected style @@ -244,7 +255,7 @@ $controlDisabledOpacity: 0.2; $colorMenuBg: $colorBodyBg; $colorMenuFg: $colorBodyFg; $colorMenuIc: $colorKey; -$filterMenu: brightness(1.4); +$filterMenu: brightness(1.4); $colorMenuHovBg: rgba($colorKey, 0.5); $colorMenuHovFg: $colorBodyFgEm; $colorMenuHovIc: $colorMenuHovFg; @@ -317,25 +328,25 @@ $colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%); // Staleness $colorTelemStale: cyan; -$colorTelemStaleFg: #002A2A; +$colorTelemStaleFg: #002a2a; $styleTelemStale: italic; // Limits -$colorLimitYellowBg: #B18B05; -$colorLimitYellowFg: #FEEEB5; -$colorLimitYellowIc: #FDC707; -$colorLimitOrangeBg: #B36B00; -$colorLimitOrangeFg: #FFE0B2; +$colorLimitYellowBg: #b18b05; +$colorLimitYellowFg: #feeeb5; +$colorLimitYellowIc: #fdc707; +$colorLimitOrangeBg: #b36b00; +$colorLimitOrangeFg: #ffe0b2; $colorLimitOrangeIc: #ff9900; $colorLimitRedBg: #940000; $colorLimitRedFg: #ffa489; $colorLimitRedIc: #ff4222; -$colorLimitPurpleBg: #891BB3; -$colorLimitPurpleFg: #EDBEFF; +$colorLimitPurpleBg: #891bb3; +$colorLimitPurpleFg: #edbeff; $colorLimitPurpleIc: #c327ff; -$colorLimitCyanBg: #4BA6B3; -$colorLimitCyanFg: #D3FAFF; -$colorLimitCyanIc: #6BEDFF; +$colorLimitCyanBg: #4ba6b3; +$colorLimitCyanFg: #d3faff; +$colorLimitCyanIc: #6bedff; // Events $colorEventPurpleFg: #6433ff; @@ -476,47 +487,47 @@ $transIn: all $transInTime ease-in-out; $transOut: all $transOutTime ease-in-out; $transInTransform: transform $transInTime ease-in-out; $transOutTransform: transform $transOutTime ease-in-out; -$transInBounce: all 200ms cubic-bezier(.47,.01,.25,1.5); -$transInBounceBig: all 300ms cubic-bezier(.2,1.6,.6,3); +$transInBounce: all 200ms cubic-bezier(0.47, 0.01, 0.25, 1.5); +$transInBounceBig: all 300ms cubic-bezier(0.2, 1.6, 0.6, 3); // Discrete items, like Notebook entries, Widget rules $createBtnTextTransform: uppercase; -$colorDiscreteItemBg: rgba($colorBodyFg,0.1); -$colorDiscreteItemCurrentBg: rgba($colorOk,0.3); +$colorDiscreteItemBg: rgba($colorBodyFg, 0.1); +$colorDiscreteItemCurrentBg: rgba($colorOk, 0.3); $scrollContainer: $colorBodyBg; @mixin discreteItem() { - background: rgba($colorBodyFg,0.1); - border: none; - border-radius: $controlCr; + background: rgba($colorBodyFg, 0.1); + border: none; + border-radius: $controlCr; - &--current-match { - background: $colorDiscreteItemCurrentBg; - } + &--current-match { + background: $colorDiscreteItemCurrentBg; + } } @mixin discreteItemInnerElem() { - border: 1px solid rgba(#fff, 0.1); - border-radius: $controlCr; + border: 1px solid rgba(#fff, 0.1); + border-radius: $controlCr; } @mixin themedButton($c: $colorBtnBg) { - background: linear-gradient(pullForward($c, 5%), $c); - box-shadow: rgba(black, 0.5) 0 0.5px 2px; + background: linear-gradient(pullForward($c, 5%), $c); + box-shadow: rgba(black, 0.5) 0 0.5px 2px; } /**************************************************** OVERRIDES */ .c-frame { - &:not(.no-frame) { - $bc: #666; - $bLR: 3px solid transparent; - $br: 20px; - background: none !important; - border-radius: $br; - border-top: 4px solid $bc !important; - border-bottom: 2px solid $bc !important; - border-left: $bLR !important;; - border-right: $bLR !important;; - padding: 5px 10px 10px 10px !important; - } + &:not(.no-frame) { + $bc: #666; + $bLR: 3px solid transparent; + $br: 20px; + background: none !important; + border-radius: $br; + border-top: 4px solid $bc !important; + border-bottom: 2px solid $bc !important; + border-left: $bLR !important; + border-right: $bLR !important; + padding: 5px 10px 10px 10px !important; + } } diff --git a/src/styles/_constants-mobile.scss b/src/styles/_constants-mobile.scss index 5ff8a95019..89a41874fa 100644 --- a/src/styles/_constants-mobile.scss +++ b/src/styles/_constants-mobile.scss @@ -45,23 +45,23 @@ $tabMaxW: 1024px; $desktopMinW: 1025px; /************************** MEDIA QUERIES: WINDOW CHECKS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */ -$screenPortrait: "(orientation: portrait)"; -$screenLandscape: "(orientation: landscape)"; +$screenPortrait: '(orientation: portrait)'; +$screenLandscape: '(orientation: landscape)'; //$mobileDevice: "(max-device-width: #{$tabMaxW})"; -$phoneCheck: "(max-device-width: #{$phoMaxW})"; -$tabletCheck: "(min-device-width: #{$tabMinW}) and (max-device-width: #{$tabMaxW})"; -$desktopCheck: "(min-device-width: #{$desktopMinW}) and (-webkit-min-device-pixel-ratio: 1)"; +$phoneCheck: '(max-device-width: #{$phoMaxW})'; +$tabletCheck: '(min-device-width: #{$tabMinW}) and (max-device-width: #{$tabMaxW})'; +$desktopCheck: '(min-device-width: #{$desktopMinW}) and (-webkit-min-device-pixel-ratio: 1)'; /************************** MEDIA QUERIES: WINDOWS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */ -$phonePortrait: "only screen and #{$screenPortrait} and #{$phoneCheck}"; -$phoneLandscape: "only screen and #{$screenLandscape} and #{$phoneCheck}"; +$phonePortrait: 'only screen and #{$screenPortrait} and #{$phoneCheck}'; +$phoneLandscape: 'only screen and #{$screenLandscape} and #{$phoneCheck}'; -$tabletPortrait: "only screen and #{$screenPortrait} and #{$tabletCheck}"; -$tabletLandscape: "only screen and #{$screenLandscape} and #{$tabletCheck}"; +$tabletPortrait: 'only screen and #{$screenPortrait} and #{$tabletCheck}'; +$tabletLandscape: 'only screen and #{$screenLandscape} and #{$tabletCheck}'; -$desktop: "only screen and #{$desktopCheck}"; +$desktop: 'only screen and #{$desktopCheck}'; /************************** DEVICE PARAMETERS FOR MENUS/REPRESENTATIONS */ $proporMenuOnly: 90%; @@ -69,81 +69,81 @@ $proporMenuWithView: 40%; // Phones in any orientation @mixin phone { - @media #{$phonePortrait}, + @media #{$phonePortrait}, #{$phoneLandscape} { - @content - } + @content; + } } //Phones in portrait orientation @mixin phonePortrait { - @media #{$phonePortrait} { - @content - } + @media #{$phonePortrait} { + @content; + } } // Phones in landscape orientation @mixin phoneLandscape { - @media #{$phoneLandscape} { - @content - } + @media #{$phoneLandscape} { + @content; + } } // Tablets in any orientation @mixin tablet { - @media #{$tabletPortrait}, + @media #{$tabletPortrait}, #{$tabletLandscape} { - @content - } + @content; + } } // Tablets in portrait orientation @mixin tabletPortrait { - @media #{$tabletPortrait} { - @content - } + @media #{$tabletPortrait} { + @content; + } } // Tablets in landscape orientation @mixin tabletLandscape { - @media #{$tabletLandscape} { - @content - } + @media #{$tabletLandscape} { + @content; + } } // Phones and tablets in any orientation @mixin phoneandtablet { - @media #{$phonePortrait}, + @media #{$phonePortrait}, #{$phoneLandscape}, #{$tabletPortrait}, #{$tabletLandscape} { - @content - } + @content; + } } // Desktop monitors in any orientation @mixin desktopandtablet { - // Keeping only for legacy - should not be used moving forward - // Use body.desktop, body.tablet instead. - @media #{$tabletPortrait}, + // Keeping only for legacy - should not be used moving forward + // Use body.desktop, body.tablet instead. + @media #{$tabletPortrait}, #{$tabletLandscape}, #{$desktop} { - @content - } + @content; + } } // Desktop monitors in any orientation @mixin desktop { - // Keeping only for legacy - should not be used moving forward - // Use body.desktop instead. - @media #{$desktop} { - @content - } + // Keeping only for legacy - should not be used moving forward + // Use body.desktop instead. + @media #{$desktop} { + @content; + } } // Transition used for the slide menu @mixin slMenuTransitions { - @include transition-duration(.35s); - transition-timing-function: ease; - backface-visibility: hidden; + @include transition-duration(0.35s); + transition-timing-function: ease; + backface-visibility: hidden; } diff --git a/src/styles/_constants-snow.scss b/src/styles/_constants-snow.scss index b5a0bc360c..5bd4ba4d54 100644 --- a/src/styles/_constants-snow.scss +++ b/src/styles/_constants-snow.scss @@ -23,36 +23,36 @@ /****************************************************** SNOW THEME CONSTANTS */ // Fonts -$heroFont: "Helvetica Neue", Helvetica, Arial, sans-serif; +$heroFont: 'Helvetica Neue', Helvetica, Arial, sans-serif; $headerFont: $heroFont; $bodyFont: $heroFont; @mixin heroFont($size: 1em) { - font-family: $heroFont; - font-size: $size; + font-family: $heroFont; + font-size: $size; } @mixin headerFont($size: 1em) { - font-family: $headerFont; - font-size: $size; + font-family: $headerFont; + font-size: $size; } @mixin bodyFont($size: 1em) { - font-family: $bodyFont; - font-size: $size; + font-family: $bodyFont; + font-size: $size; } // Functions @function buttonBg($c: $colorBtnBg) { - @return $c; + @return $c; } @function pullForward($val, $amt) { - @return darken($val, $amt); + @return darken($val, $amt); } @function pushBack($val, $amt) { - @return lighten($val, $amt); + @return lighten($val, $amt); } // General @@ -73,8 +73,10 @@ $colorHeadFg: $colorBodyFg; $colorKey: #0099cc; $colorKeyFg: #fff; $colorKeyHov: #00c0f6; -$colorKeyFilter: invert(37%) sepia(100%) saturate(686%) hue-rotate(157deg) brightness(102%) contrast(102%); -$colorKeyFilterHov: invert(69%) sepia(87%) saturate(3243%) hue-rotate(151deg) brightness(97%) contrast(102%); +$colorKeyFilter: invert(37%) sepia(100%) saturate(686%) hue-rotate(157deg) brightness(102%) + contrast(102%); +$colorKeyFilterHov: invert(69%) sepia(87%) saturate(3243%) hue-rotate(151deg) brightness(97%) + contrast(102%); $colorKeySelectedBg: $colorKey; $uiColor: #289fec; // Resize bars, splitter bars, etc. $colorInteriorBorder: rgba($colorBodyFg, 0.2); @@ -101,11 +103,14 @@ $sideBarHeaderFg: rgba($colorBodyFg, 0.7); $colorStatusFg: #999; $colorStatusDefault: #ccc; $colorStatusInfo: #60ba7b; -$colorStatusInfoFilter: invert(64%) sepia(42%) saturate(416%) hue-rotate(85deg) brightness(93%) contrast(93%); +$colorStatusInfoFilter: invert(64%) sepia(42%) saturate(416%) hue-rotate(85deg) brightness(93%) + contrast(93%); $colorStatusAlert: #ff8a0d; -$colorStatusAlertFilter: invert(89%) sepia(26%) saturate(5035%) hue-rotate(316deg) brightness(114%) contrast(107%); +$colorStatusAlertFilter: invert(89%) sepia(26%) saturate(5035%) hue-rotate(316deg) brightness(114%) + contrast(107%); $colorStatusError: #da0004; -$colorStatusErrorFilter: invert(8%) sepia(96%) saturate(4511%) hue-rotate(352deg) brightness(136%) contrast(114%); +$colorStatusErrorFilter: invert(8%) sepia(96%) saturate(4511%) hue-rotate(352deg) brightness(136%) + contrast(114%); $colorStatusBtnBg: #666; // Where is this used? $colorStatusPartialBg: #c9d6ff; $colorStatusCompleteBg: #a4e4b4; @@ -114,11 +119,11 @@ $colorAlertFg: #fff; $colorError: #ff3c00; $colorErrorFg: #fff; $colorWarningHi: #990000; -$colorWarningHiFg: #FF9594; +$colorWarningHiFg: #ff9594; $colorWarningLo: #ff9900; $colorWarningLoFg: #523400; $colorDiagnostic: #a4b442; -$colorDiagnosticFg: #39461A; +$colorDiagnosticFg: #39461a; $colorCommand: #3693bd; $colorCommandFg: #fff; $colorInfo: #2294a2; @@ -148,7 +153,7 @@ $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov $timeConductorAxisHoverFilter: brightness(0.8); $timeConductorActiveBg: $colorKey; -$timeConductorActivePanBg: #A0CDE1; +$timeConductorActivePanBg: #a0cde1; /************************************************** BROWSING */ $browseFrameColor: pullForward($colorBodyBg, 10%); @@ -164,7 +169,10 @@ $borderMissing: 1px dashed $colorAlert !important; $editUIColor: $uiColor; // Base color $editUIColorBg: $editUIColor; $editUIColorFg: #fff; -$editUIColorHov: pullForward(saturate($uiColor, 10%), 20%); // Hover color when $editUIColor is applied as a base color +$editUIColorHov: pullForward( + saturate($uiColor, 10%), + 20% +); // Hover color when $editUIColor is applied as a base color $editUIBaseColor: #cae1ff; // Base color, toolbar bg $editUIBaseColorHov: pushBack($editUIBaseColor, 20%); $editUIBaseColorFg: #4c4c4c; // Toolbar button icon colors, etc. @@ -183,7 +191,10 @@ $editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make h $editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color $editFrameSelectedShdw: rgba(black, 0.5) 0 1px 5px 2px; $editFrameMovebarColorBg: $editFrameColor; // Movebar bg color -$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text +$editFrameMovebarColorFg: pullForward( + $editFrameMovebarColorBg, + 20% +); // Grippy lines, container size text $editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style $editFrameHovMovebarColorFg: pullForward($editFrameMovebarColorFg, 10%); $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); // Selected style @@ -297,7 +308,7 @@ $colorTabCurrentBg: $colorBodyFg; //pullForward($colorTabBg, 10%); $colorTabCurrentFg: $colorBodyBg; //pullForward($colorTabFg, 10%); $colorTabsBaseline: $colorTabCurrentBg; - // Overlay +// Overlay $colorOvrBlocker: rgba(black, 0.7); $overlayCr: $interiorMarginLg; @@ -314,24 +325,24 @@ $colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%); // Staleness $colorTelemStale: #00c9c9; -$colorTelemStaleFg: #002A2A; +$colorTelemStaleFg: #002a2a; $styleTelemStale: italic; // Limits $colorLimitYellowBg: #ffe64d; $colorLimitYellowFg: #7f4f20; $colorLimitYellowIc: #e7a115; -$colorLimitOrangeBg: #B36B00; -$colorLimitOrangeFg: #FFE0B2; +$colorLimitOrangeBg: #b36b00; +$colorLimitOrangeFg: #ffe0b2; $colorLimitOrangeIc: #ff9900; $colorLimitRedBg: #ff0000; $colorLimitRedFg: #fff; $colorLimitRedIc: #ffa99a; -$colorLimitPurpleBg: #891BB3; -$colorLimitPurpleFg: #EDBEFF; +$colorLimitPurpleBg: #891bb3; +$colorLimitPurpleFg: #edbeff; $colorLimitPurpleIc: #c327ff; -$colorLimitCyanBg: #4BA6B3; -$colorLimitCyanFg: #D3FAFF; +$colorLimitCyanBg: #4ba6b3; +$colorLimitCyanFg: #d3faff; $colorLimitCyanIc: #1795c0; // Events @@ -473,34 +484,34 @@ $transIn: all $transInTime ease-in-out; $transOut: all $transOutTime ease-in-out; $transInTransform: transform $transInTime ease-in-out; $transOutTransform: transform $transOutTime ease-in-out; -$transInBounce: all 200ms cubic-bezier(.47,.01,.25,1.5); -$transInBounceBig: all 300ms cubic-bezier(.2,1.6,.6,3); +$transInBounce: all 200ms cubic-bezier(0.47, 0.01, 0.25, 1.5); +$transInBounceBig: all 300ms cubic-bezier(0.2, 1.6, 0.6, 3); // Discrete items, like Notebook entries, Widget rules $createBtnTextTransform: uppercase; -$colorDiscreteItemBg: rgba($colorBodyFg,0.1); -$colorDiscreteItemCurrentBg: rgba($colorOk,0.3); +$colorDiscreteItemBg: rgba($colorBodyFg, 0.1); +$colorDiscreteItemCurrentBg: rgba($colorOk, 0.3); $scrollContainer: rgba(102, 102, 102, 0.1); @mixin discreteItem() { - background: $colorDiscreteItemBg; - border: 1px solid $colorInteriorBorder; - border-radius: $controlCr; + background: $colorDiscreteItemBg; + border: 1px solid $colorInteriorBorder; + border-radius: $controlCr; - &--current-match { - background: $colorDiscreteItemCurrentBg; - } + &--current-match { + background: $colorDiscreteItemCurrentBg; + } - .c-input-inline:hover { - background: $colorBodyBg; - } + .c-input-inline:hover { + background: $colorBodyBg; + } } @mixin discreteItemInnerElem() { - border: 1px solid $colorBodyBg; - border-radius: $controlCr; + border: 1px solid $colorBodyBg; + border-radius: $controlCr; } @mixin themedButton($c: $colorBtnBg) { - background: $c; + background: $c; } diff --git a/src/styles/_constants.scss b/src/styles/_constants.scss index 19d2b24263..8718a918ad 100755 --- a/src/styles/_constants.scss +++ b/src/styles/_constants.scss @@ -109,7 +109,7 @@ $colorProgressBar: #0085ad; $progressAnimW: 500px; $progressBarMinH: 4px; /************************** FONT STYLING */ -$listFontSizes: 8,9,10,11,12,13,14,16,18,20,24,28,32,36,42,48,72,96,128; +$listFontSizes: 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 24, 28, 32, 36, 42, 48, 72, 96, 128; /************************** GLYPH CHAR UNICODES */ $glyph-icon-alert-rect: '\e900'; diff --git a/src/styles/_controls.scss b/src/styles/_controls.scss index 5e321aa5ee..636ee98c77 100644 --- a/src/styles/_controls.scss +++ b/src/styles/_controls.scss @@ -24,224 +24,224 @@ /******************************************************** CONTROL-SPECIFIC MIXINS */ @mixin menuOuter() { - border-radius: $basicCr; - box-shadow: $shdwMenu; - @if $shdwMenuInner != none { - box-shadow: $shdwMenuInner, $shdwMenu; - } - background: $colorMenuBg; - color: $colorMenuFg; - text-shadow: $shdwMenuText; - padding: $interiorMarginSm; - display: flex; - flex-direction: column; - position: absolute; - z-index: 100; + border-radius: $basicCr; + box-shadow: $shdwMenu; + @if $shdwMenuInner != none { + box-shadow: $shdwMenuInner, $shdwMenu; + } + background: $colorMenuBg; + color: $colorMenuFg; + text-shadow: $shdwMenuText; + padding: $interiorMarginSm; + display: flex; + flex-direction: column; + position: absolute; + z-index: 100; - > * { - flex: 0 0 auto; - } + > * { + flex: 0 0 auto; + } } @mixin menuPositioning() { - display: flex; - flex-direction: column; - position: absolute; - z-index: 100; + display: flex; + flex-direction: column; + position: absolute; + z-index: 100; - > * { - flex: 0 0 auto; - } + > * { + flex: 0 0 auto; + } } @mixin menuInner() { - li { - @include cControl(); - justify-content: start; - cursor: pointer; - display: flex; - padding: nth($menuItemPad, 1) nth($menuItemPad, 2); - white-space: nowrap; + li { + @include cControl(); + justify-content: start; + cursor: pointer; + display: flex; + padding: nth($menuItemPad, 1) nth($menuItemPad, 2); + white-space: nowrap; - @include hover { - background: $colorMenuHovBg; - color: $colorMenuHovFg; - &:before { - color: $colorMenuHovIc !important; - } - } - - &:not(.c-menu--no-icon &) { - &:before { - color: $colorMenuIc; - font-size: 1em; - margin-right: $interiorMargin; - min-width: 1em; - } - - &:not([class*='icon']):before { - content: ''; // Enable :before so that menu items without an icon still indent properly - } - - } + @include hover { + background: $colorMenuHovBg; + color: $colorMenuHovFg; + &:before { + color: $colorMenuHovIc !important; + } } + + &:not(.c-menu--no-icon &) { + &:before { + color: $colorMenuIc; + font-size: 1em; + margin-right: $interiorMargin; + min-width: 1em; + } + + &:not([class*='icon']):before { + content: ''; // Enable :before so that menu items without an icon still indent properly + } + } + } } /******************************************************** BUTTONS */ // Optionally can include icon in :before via markup button { - @include htmlInputReset(); + @include htmlInputReset(); } .c-button, .c-button--menu { - @include cButton(); + @include cButton(); } .c-button { - &--menu { - &:after { - content: $glyph-icon-arrow-down; - font-family: symbolsfont; - opacity: 0.5; - } + &--menu { + &:after { + content: $glyph-icon-arrow-down; + font-family: symbolsfont; + opacity: 0.5; + } + } + + &--swatched { + // Used with c-button--menu: a visual button with a larger swatch element adjacent to an icon + .c-swatch { + $d: 12px; + margin-left: $interiorMarginSm; + height: $d; + width: $d; + } + } + + &[class*='__collapse-button'] { + box-shadow: none; + background: $splitterBtnColorBg; + color: $splitterBtnColorFg; + border-radius: $smallCr; + line-height: 90%; + padding: 3px 10px; + + @include desktop() { + font-size: 6px; } - &--swatched { - // Used with c-button--menu: a visual button with a larger swatch element adjacent to an icon - .c-swatch { - $d: 12px; - margin-left: $interiorMarginSm; - height: $d; width: $d; - } + &:before { + content: $glyph-icon-arrow-down; + font-size: 1.1em; } + } - &[class*='__collapse-button'] { - box-shadow: none; - background: $splitterBtnColorBg; - color: $splitterBtnColorFg; - border-radius: $smallCr; - line-height: 90%; - padding: 3px 10px; + &.is-active { + background: $colorBtnActiveBg; + color: $colorBtnActiveFg; + } - @include desktop() { - font-size: 6px; - } - - &:before { - content: $glyph-icon-arrow-down; - font-size: 1.1em; - } - } - - &.is-active { - background: $colorBtnActiveBg; - color: $colorBtnActiveFg; - } - - &.is-selected { - background: $colorBtnSelectedBg; - color: $colorBtnSelectedFg; - } + &.is-selected { + background: $colorBtnSelectedBg; + color: $colorBtnSelectedFg; + } } /********* Icon Buttons and Links */ .c-click-icon { - @include cClickIcon(); + @include cClickIcon(); - &--section-collapse { - color: inherit; - display: block; - transition: transform $transOutTime; - &:before { - content: $glyph-icon-arrow-down; - font-family: symbolsfont; - } - - &.is-collapsed { - transform: rotate(180deg); - } + &--section-collapse { + color: inherit; + display: block; + transition: transform $transOutTime; + &:before { + content: $glyph-icon-arrow-down; + font-family: symbolsfont; } + + &.is-collapsed { + transform: rotate(180deg); + } + } } .c-click-link, .c-icon-link { - // A clickable element, typically inline, with an icon and label - @include cControl(); - cursor: pointer; + // A clickable element, typically inline, with an icon and label + @include cControl(); + cursor: pointer; } .c-icon-button, .c-click-swatch { - @include cClickIconButton(); + @include cClickIconButton(); - &--menu { - @include hasMenu(); - } + &--menu { + @include hasMenu(); + } } .c-icon-button--disabled { - @include cClickIconButtonLayout(); + @include cClickIconButtonLayout(); } .c-icon-link { - &:before { - // Icon - //color: $colorBtnMajorBg; - } + &:before { + // Icon + //color: $colorBtnMajorBg; + } } .c-icon-button { - [class*='label'] { - opacity: 0.8; - padding: 1px 0; + [class*='label'] { + opacity: 0.8; + padding: 1px 0; + } + + &--mixed { + @include mixedBg(); + } + + &--swatched { + // Color control, show swatch element + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: center; + + > [class*='swatch'] { + box-shadow: inset rgba($editUIBaseColorFg, 0.2) 0 0 0 1px; + flex: 0 0 auto; + height: 5px; + width: 100%; + margin-top: 1px; } - &--mixed { - @include mixedBg(); - } - - &--swatched { - // Color control, show swatch element - display: flex; - flex-flow: column nowrap; - align-items: center; - justify-content: center; - - > [class*='swatch'] { - box-shadow: inset rgba($editUIBaseColorFg, 0.2) 0 0 0 1px; - flex: 0 0 auto; - height: 5px; - width: 100%; - margin-top: 1px; - } - - &:before { - // Reduce size of icon to make a bit of room - flex: 1 1 auto; - font-size: 1.1em; - } + &:before { + // Reduce size of icon to make a bit of room + flex: 1 1 auto; + font-size: 1.1em; } + } } .c-list-button { - @include cControl(); - color: $colorBodyFg; - cursor: pointer; - justify-content: start; - padding: $interiorMargin; + @include cControl(); + color: $colorBodyFg; + cursor: pointer; + justify-content: start; + padding: $interiorMargin; - > * + * { - margin-left: $interiorMargin; - } + > * + * { + margin-left: $interiorMargin; + } - @include hover() { - background: $colorItemTreeHoverBg; - } + @include hover() { + background: $colorItemTreeHoverBg; + } - .c-button { - flex: 0 0 auto; - } + .c-button { + flex: 0 0 auto; + } } /******************************************************** DISCLOSURE CONTROLS */ @@ -249,340 +249,347 @@ button { // Provides a downward arrow icon that when clicked displays additional options and/or info. // Always placed AFTER an element .c-disclosure-button { - @include cClickIcon(); - margin-left: $interiorMarginSm; + @include cClickIcon(); + margin-left: $interiorMarginSm; - &:before { - content: $glyph-icon-arrow-down; - font-family: symbolsfont; - font-size: 0.7em; - } + &:before { + content: $glyph-icon-arrow-down; + font-family: symbolsfont; + font-size: 0.7em; + } } /********* Disclosure Triangle */ // Provides an arrow icon that when clicked expands an element to reveal its contents. // Used in tree items, plot legends. Always placed BEFORE an element. .c-disclosure-triangle { - $d: 12px; - color: $colorDisclosureCtrl; - display: flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - width: $d; - position: relative; - visibility: hidden; + $d: 12px; + color: $colorDisclosureCtrl; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: $d; + position: relative; + visibility: hidden; - &.is-enabled { - cursor: pointer; - visibility: visible; + &.is-enabled { + cursor: pointer; + visibility: visible; - &:hover { - color: $colorDisclosureCtrlHov; - } - - &:before { - $s: .65; - content: $glyph-icon-arrow-right-equilateral; - display: block; - font-family: symbolsfont; - font-size: 1rem * $s; - } + &:hover { + color: $colorDisclosureCtrlHov; } - &--expanded { - &:before { - transform: rotate(90deg); - } + &:before { + $s: 0.65; + content: $glyph-icon-arrow-right-equilateral; + display: block; + font-family: symbolsfont; + font-size: 1rem * $s; } + } + + &--expanded { + &:before { + transform: rotate(90deg); + } + } } /******************************************************** DRAG AFFORDANCES */ .c-grippy { - $d: 10px; - @include grippy($c: $colorItemTreeVC, $dir: 'y'); - width: $d; height: $d; + $d: 10px; + @include grippy($c: $colorItemTreeVC, $dir: 'y'); + width: $d; + height: $d; - &--vertical-drag { - cursor: ns-resize; - } + &--vertical-drag { + cursor: ns-resize; + } } /******************************************************** SECTION */ section { - flex: 0 1 auto; - overflow: hidden; - + section { - margin-top: $interiorMargin; - } + flex: 0 1 auto; + overflow: hidden; + + section { + margin-top: $interiorMargin; + } - .c-section__header { - @include propertiesHeader(); - display: flex; - flex: 0 0 auto; - align-items: center; - margin-bottom: $interiorMargin; + .c-section__header { + @include propertiesHeader(); + display: flex; + flex: 0 0 auto; + align-items: center; + margin-bottom: $interiorMargin; - > * + * { margin-left: $interiorMarginSm; } + > * + * { + margin-left: $interiorMarginSm; } + } - > [class*='__label'] { - flex: 1 1 auto; - text-transform: uppercase; - } + > [class*='__label'] { + flex: 1 1 auto; + text-transform: uppercase; + } } /******************************************************** FORM ELEMENTS */ -input, textarea { - font-family: inherit; - font-weight: inherit; - letter-spacing: inherit; +input, +textarea { + font-family: inherit; + font-weight: inherit; + letter-spacing: inherit; } -input[type=text], -input[type=search], -input[type=number], -input[type=password], -input[type=date], +input[type='text'], +input[type='search'], +input[type='number'], +input[type='password'], +input[type='date'], textarea { - @include reactive-input(); - &.numeric { - text-align: right; - } + @include reactive-input(); + &.numeric { + text-align: right; + } } -input[type=text], -input[type=search], -input[type=password], -input[type=date], +input[type='text'], +input[type='search'], +input[type='password'], +input[type='date'], textarea { - padding: $inputTextP; + padding: $inputTextP; } .c-input { - &--flex { - width: 100%; - min-width: 20px; + &--flex { + width: 100%; + min-width: 20px; + } + + &--datetime { + // Sized for values such as 2018-09-28 22:32:33.468Z + width: 160px; + } + + &--hrs-min-sec { + // Sized for values such as 00:25:00 + width: 60px; + } + + &-inline, + &--inline { + // A text input or contenteditable element that indicates edit affordance on hover and looks like an input on focus + @include inlineInput; + + &:hover, + &:focus { + background: $colorInputBg; + padding-left: $inputTextPLeftRight; + padding-right: $inputTextPLeftRight; } + } - &--datetime { - // Sized for values such as 2018-09-28 22:32:33.468Z - width: 160px; - } - - &--hrs-min-sec { - // Sized for values such as 00:25:00 - width: 60px; - } - - &-inline, - &--inline { - // A text input or contenteditable element that indicates edit affordance on hover and looks like an input on focus - @include inlineInput; - - &:hover, - &:focus { - background: $colorInputBg; - padding-left: $inputTextPLeftRight; - padding-right: $inputTextPLeftRight; - } - } - - &--labeled { - // TODO: replace .c-labeled-input with this - // An input used in the Toolbar - // Assumes label is before the input - @include cControl(); - - input { - margin-left: $interiorMarginSm; - } - } - - &--sm { - // Small inputs, like small numerics - width: 40px; - } - - &--autocomplete { - &__wrapper { - display: flex; - flex-direction: row; - align-items: center; - overflow: hidden; - width: 100%; - } - - &__input { - min-width: 100px; - width: 100%; - - // Fend off from afford-arrow - padding-right: 2.5em !important; - } - - &__options { - @include menuOuter(); - @include menuInner(); - display: flex; - - ul { - flex: 1 1 auto; - overflow: auto; - } - - li { - &:before { - color: var(--optionIconColor) !important; - font-size: 0.8em !important; - } - } - } - - &__afford-arrow { - $p: 2px; - font-size: 0.8em; - padding-bottom: $p; - padding-top: $p; - position: absolute; - right: 2px; - z-index: 2; - } - } -} - -input[type=number].c-input-number--no-spinners { - &::-webkit-inner-spin-button, - &::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; - } - -moz-appearance: textfield; -} - -.c-labeled-input { + &--labeled { + // TODO: replace .c-labeled-input with this // An input used in the Toolbar // Assumes label is before the input @include cControl(); input { - margin-left: $interiorMarginSm; + margin-left: $interiorMarginSm; } + } + + &--sm { + // Small inputs, like small numerics + width: 40px; + } + + &--autocomplete { + &__wrapper { + display: flex; + flex-direction: row; + align-items: center; + overflow: hidden; + width: 100%; + } + + &__input { + min-width: 100px; + width: 100%; + + // Fend off from afford-arrow + padding-right: 2.5em !important; + } + + &__options { + @include menuOuter(); + @include menuInner(); + display: flex; + + ul { + flex: 1 1 auto; + overflow: auto; + } + + li { + &:before { + color: var(--optionIconColor) !important; + font-size: 0.8em !important; + } + } + } + + &__afford-arrow { + $p: 2px; + font-size: 0.8em; + padding-bottom: $p; + padding-top: $p; + position: absolute; + right: 2px; + z-index: 2; + } + } } -.c-scrollcontainer{ - @include nice-input(); - margin-top: $interiorMargin; - background: $scrollContainer; - border-radius: $controlCr; - overflow: auto; - padding: $interiorMarginSm; +input[type='number'].c-input-number--no-spinners { + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + -moz-appearance: textfield; +} + +.c-labeled-input { + // An input used in the Toolbar + // Assumes label is before the input + @include cControl(); + + input { + margin-left: $interiorMarginSm; + } +} + +.c-scrollcontainer { + @include nice-input(); + margin-top: $interiorMargin; + background: $scrollContainer; + border-radius: $controlCr; + overflow: auto; + padding: $interiorMarginSm; } // SELECTS select { - @include appearanceNone(); - background-color: $colorSelectBg; - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{svgColorFromHex($colorSelectArw)}' d='M5 5l5-5H0z'/%3e%3c/svg%3e"); - color: $colorSelectFg; - box-shadow: $shdwSelect; - background-repeat: no-repeat, no-repeat; - background-position: right .4em top 80%, 0 0; - border: none; - border-radius: $controlCr; - padding: 2px 20px 2px $interiorMargin; + @include appearanceNone(); + background-color: $colorSelectBg; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{svgColorFromHex($colorSelectArw)}' d='M5 5l5-5H0z'/%3e%3c/svg%3e"); + color: $colorSelectFg; + box-shadow: $shdwSelect; + background-repeat: no-repeat, no-repeat; + background-position: right 0.4em top 80%, 0 0; + border: none; + border-radius: $controlCr; + padding: 2px 20px 2px $interiorMargin; - *, - option { - background: $colorBtnBg; - color: $colorBtnFg; - } + *, + option { + background: $colorBtnBg; + color: $colorBtnFg; + } } // CHECKBOX LISTS // __input followed by __label .c-checkbox-list { - // Rows - &__row + &__row { margin-top: $interiorMarginSm; } + // Rows + &__row + &__row { + margin-top: $interiorMarginSm; + } - // input and label in each __row - &__row { - > * + * { margin-left: $interiorMargin; } + // input and label in each __row + &__row { + > * + * { + margin-left: $interiorMargin; } + } - li { - white-space: nowrap; - } + li { + white-space: nowrap; + } } /******************************************************** TABS */ .c-tabs { - // Single horizontal strip of tabs, with a bottom divider line - @include userSelectNone(); - display: flex; - flex: 0 0 auto; - flex-wrap: wrap; - position: relative; // Required in case this is applied to a
+ diff --git a/src/ui/layout/search/search.scss b/src/ui/layout/search/search.scss index fe9c11d2e5..91f9173b13 100644 --- a/src/ui/layout/search/search.scss +++ b/src/ui/layout/search/search.scss @@ -22,123 +22,123 @@ /******************************* EXPANDED SEARCH 2022 */ .c-gsearch { - .l-shell__head & { - // Search input in the shell head - width: 20%; + .l-shell__head & { + // Search input in the shell head + width: 20%; - .c-search { - background: rgba($colorHeadFg, 0.2); - box-shadow: none; - } + .c-search { + background: rgba($colorHeadFg, 0.2); + box-shadow: none; } + } - &__results-wrapper { - @include menuOuter(); - display: flex; - flex-direction: column; - padding: $interiorMarginLg; - min-width: 500px; - max-height: 500px; - z-index: 60; + &__results-wrapper { + @include menuOuter(); + display: flex; + flex-direction: column; + padding: $interiorMarginLg; + min-width: 500px; + max-height: 500px; + z-index: 60; + } + + &__results, + &__results-section { + flex: 1 1 auto; + } + + &__results { + // Holds n __results-sections + padding-right: $interiorMargin; // Fend off scrollbar + overflow-y: auto; + + > * + * { + margin-top: $interiorMarginLg; } + } - &__results, - &__results-section { - flex: 1 1 auto; + &__results-section { + > * + * { + margin-top: $interiorMarginSm; } + } - &__results { - // Holds n __results-sections - padding-right: $interiorMargin; // Fend off scrollbar - overflow-y: auto; + &__results-section-title { + @include propertiesHeader(); + } - > * + * { - margin-top: $interiorMarginLg; - } - } - - &__results-section { - > * + * { - margin-top: $interiorMarginSm; - } - } - - &__results-section-title { - @include propertiesHeader(); - } - - &__result-pane-msg { - > * + * { - margin-top: $interiorMargin; - } + &__result-pane-msg { + > * + * { + margin-top: $interiorMargin; } + } } .c-gsearch-result { - display: flex; - padding: $interiorMargin $interiorMarginSm; + display: flex; + padding: $interiorMargin $interiorMarginSm; + + > * + * { + margin-left: $interiorMarginLg; + } + + + .c-gsearch-result { + border-top: 1px solid $colorInteriorBorder; + } + + &__type-icon, + &__more-options-button { + flex: 0 0 auto; + } + + &__type-icon { + color: $colorItemTreeIcon; + font-size: 2.2em; + + // TEMP: uses object-label component, hide label part + .c-object-label__name { + display: none; + } + } + + &__more-options-button { + display: none; // TEMP until enabled + } + + &__body { + flex: 1 1 auto; > * + * { - margin-left: $interiorMarginLg; + margin-top: $interiorMarginSm; } - + .c-gsearch-result { - border-top: 1px solid $colorInteriorBorder; + .c-location { + font-size: 0.9em; + opacity: 0.8; } + } - &__type-icon, - &__more-options-button { - flex: 0 0 auto; + &__tags { + display: flex; + + > * + * { + margin-left: $interiorMargin; } + } - &__type-icon { - color: $colorItemTreeIcon; - font-size: 2.2em; + &__title { + border-radius: $basicCr; + color: pullForward($colorBodyFg, 30%); + cursor: pointer; + font-size: 1.15em; + padding: 3px $interiorMarginSm; - // TEMP: uses object-label component, hide label part - .c-object-label__name { - display: none; - } + &:hover { + background-color: $colorItemTreeHoverBg; } + } - &__more-options-button { - display: none; // TEMP until enabled - } - - &__body { - flex: 1 1 auto; - - > * + * { - margin-top: $interiorMarginSm; - } - - .c-location { - font-size: 0.9em; - opacity: 0.8; - } - } - - &__tags { - display: flex; - - > * + * { - margin-left: $interiorMargin; - } - } - - &__title { - border-radius: $basicCr; - color: pullForward($colorBodyFg, 30%); - cursor: pointer; - font-size: 1.15em; - padding: 3px $interiorMarginSm; - - &:hover { - background-color: $colorItemTreeHoverBg; - } - } - - .c-tag { - font-size: 0.9em; - } + .c-tag { + font-size: 0.9em; + } } diff --git a/src/ui/layout/status-bar/Indicators.vue b/src/ui/layout/status-bar/Indicators.vue index ebcd749d60..886019a7d7 100644 --- a/src/ui/layout/status-bar/Indicators.vue +++ b/src/ui/layout/status-bar/Indicators.vue @@ -17,26 +17,25 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/layout/status-bar/NotificationBanner.vue b/src/ui/layout/status-bar/NotificationBanner.vue index 00590185d5..6f6c4bf1fe 100644 --- a/src/ui/layout/status-bar/NotificationBanner.vue +++ b/src/ui/layout/status-bar/NotificationBanner.vue @@ -17,37 +17,39 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/layout/status-bar/indicators.scss b/src/ui/layout/status-bar/indicators.scss index ea51401bc1..957356f9e9 100644 --- a/src/ui/layout/status-bar/indicators.scss +++ b/src/ui/layout/status-bar/indicators.scss @@ -1,132 +1,134 @@ .c-indicator { - @include cControl(); - @include cClickIconButtonLayout(); - border-radius: $controlCr; - overflow: visible; - position: relative; + @include cControl(); + @include cClickIconButtonLayout(); + border-radius: $controlCr; + overflow: visible; + position: relative; + text-transform: uppercase; + + button { text-transform: uppercase; + } - button { text-transform: uppercase; } + &.no-minify { + // For items that cannot be minified + display: flex; + flex-flow: row nowrap; + align-items: center; - &.no-minify { - // For items that cannot be minified - display: flex; - flex-flow: row nowrap; - align-items: center; - - > *, - &:before { - flex: 1 1 auto; - } - - &:before { - margin-right: $interiorMarginSm; - } + > *, + &:before { + flex: 1 1 auto; } - &:not(.no-minify) { - &:before { - margin-right: 0 !important; - } + &:before { + margin-right: $interiorMarginSm; } + } + + &:not(.no-minify) { + &:before { + margin-right: 0 !important; + } + } } .c-indicator__label { - // Label element. Appears as a hover bubble element when Indicators are minified; - // Appears as an inline element when not. - display: inline-block; - transition:none; - white-space: nowrap; + // Label element. Appears as a hover bubble element when Indicators are minified; + // Appears as an inline element when not. + display: inline-block; + transition: none; + white-space: nowrap; - a, - button, - .s-button, - .c-button { - // Make in label look like buttons - @include transition(background-color); - background-color: transparent; - border: 1px solid rgba($colorIndicatorMenuFg, 0.5); - border-radius: $controlCr; - box-sizing: border-box; - color: inherit; - font-size: inherit; - height: auto; - line-height: normal; - padding: 0 2px; - @include hover { - background-color: rgba($colorIndicatorMenuFg, 0.1); - border-color: rgba($colorIndicatorMenuFg, 0.75); - color: $colorIndicatorMenuFgHov; - } + a, + button, + .s-button, + .c-button { + // Make in label look like buttons + @include transition(background-color); + background-color: transparent; + border: 1px solid rgba($colorIndicatorMenuFg, 0.5); + border-radius: $controlCr; + box-sizing: border-box; + color: inherit; + font-size: inherit; + height: auto; + line-height: normal; + padding: 0 2px; + @include hover { + background-color: rgba($colorIndicatorMenuFg, 0.1); + border-color: rgba($colorIndicatorMenuFg, 0.75); + color: $colorIndicatorMenuFgHov; } + } - [class*='icon-'] { - // If any elements within label include the class 'icon-*' then deal with their :before's - &:before { - font-size: 0.8em; - margin-right: $interiorMarginSm; - } + [class*='icon-'] { + // If any elements within label include the class 'icon-*' then deal with their :before's + &:before { + font-size: 0.8em; + margin-right: $interiorMarginSm; } + } } .c-indicator__count { - display: none; // Only displays when Indicator is minified, see below + display: none; // Only displays when Indicator is minified, see below } [class*='minify-indicators'] { - // All styles for minified Indicators should go in here - .c-indicator:not(.no-minify) { - border: 1px solid transparent; // Hack to make minified sizing work in Safari. Have no idea why this works. + // All styles for minified Indicators should go in here + .c-indicator:not(.no-minify) { + border: 1px solid transparent; // Hack to make minified sizing work in Safari. Have no idea why this works. + overflow: visible; + transition: transform; + + @include hover() { + background: $colorIndicatorBgHov; + transition: transform 250ms ease-in 200ms; // Go-away transition + + .c-indicator__label { + box-shadow: $colorIndicatorMenuBgShdw; + transform: scale(1); overflow: visible; - transition: transform; - - @include hover() { - background: $colorIndicatorBgHov; - transition: transform 250ms ease-in 200ms; // Go-away transition - - .c-indicator__label { - box-shadow: $colorIndicatorMenuBgShdw; - transform: scale(1.0); - overflow: visible; - transition: transform 100ms ease-out 100ms; // Appear transition - } - } - .c-indicator__label { - transition: transform 250ms ease-in 200ms; // Go-away transition - background: $colorIndicatorMenuBg; - color: $colorIndicatorMenuFg; - border-radius: $controlCr; - right: 0; - top: 130%; - padding: $interiorMargin $interiorMargin; - position: absolute; - transform-origin: 90% 0; - transform: scale(0.0); - overflow: hidden; - z-index: 50; - - &:before { - // Infobubble-style arrow element - content: ''; - display: block; - position: absolute; - bottom: 100%; - right: 8px; - @include triangle('up', $size: 4px, $ratio: 1, $color: $colorIndicatorMenuBg); - } - } - - .c-indicator__count { - display: inline-block; - margin-left: $interiorMarginSm; - } + transition: transform 100ms ease-out 100ms; // Appear transition + } } + .c-indicator__label { + transition: transform 250ms ease-in 200ms; // Go-away transition + background: $colorIndicatorMenuBg; + color: $colorIndicatorMenuFg; + border-radius: $controlCr; + right: 0; + top: 130%; + padding: $interiorMargin $interiorMargin; + position: absolute; + transform-origin: 90% 0; + transform: scale(0); + overflow: hidden; + z-index: 50; + + &:before { + // Infobubble-style arrow element + content: ''; + display: block; + position: absolute; + bottom: 100%; + right: 8px; + @include triangle('up', $size: 4px, $ratio: 1, $color: $colorIndicatorMenuBg); + } + } + + .c-indicator__count { + display: inline-block; + margin-left: $interiorMarginSm; + } + } } /* Mobile */ // Hide the clock indicator when we're phone portrait body.phone.portrait { - .c-indicator.t-indicator-clock { - display: none; - } + .c-indicator.t-indicator-clock { + display: none; + } } diff --git a/src/ui/layout/status-bar/notification-banner.scss b/src/ui/layout/status-bar/notification-banner.scss index c818cd47a2..bac7845c2d 100644 --- a/src/ui/layout/status-bar/notification-banner.scss +++ b/src/ui/layout/status-bar/notification-banner.scss @@ -1,74 +1,74 @@ @mixin statusBannerColors($bg, $fg: $colorStatusFg) { - $bgPb: 10%; - $bgPbD: 10%; - background-color: darken($bg, $bgPb); - color: $fg; + $bgPb: 10%; + $bgPbD: 10%; + background-color: darken($bg, $bgPb); + color: $fg; + &:hover { + background-color: darken($bg, $bgPb - $bgPbD); + } + .s-action { + background-color: darken($bg, $bgPb + $bgPbD); &:hover { - background-color: darken($bg, $bgPb - $bgPbD); - } - .s-action { - background-color: darken($bg, $bgPb + $bgPbD); - &:hover { - background-color: darken($bg, $bgPb); - } + background-color: darken($bg, $bgPb); } + } } .c-message-banner { - $closeBtnSize: 7px; + $closeBtnSize: 7px; - border-radius: $controlCr; - @include statusBannerColors($colorStatusDefault, $colorStatusFg); - cursor: pointer; - display: flex; - align-items: center; - left: 50%; - top: 50%; - max-width: 50%; - max-height: 25px; - padding: $interiorMarginSm $interiorMargin $interiorMarginSm $interiorMarginLg; - position: absolute; - transform: translate(-50%, -50%); - z-index: 2; + border-radius: $controlCr; + @include statusBannerColors($colorStatusDefault, $colorStatusFg); + cursor: pointer; + display: flex; + align-items: center; + left: 50%; + top: 50%; + max-width: 50%; + max-height: 25px; + padding: $interiorMarginSm $interiorMargin $interiorMarginSm $interiorMarginLg; + position: absolute; + transform: translate(-50%, -50%); + z-index: 2; - > * + * { - margin-left: $interiorMargin; - } + > * + * { + margin-left: $interiorMargin; + } - &.ok { - @include statusBannerColors($colorOk, $colorOkFg); - } + &.ok { + @include statusBannerColors($colorOk, $colorOkFg); + } - &.info { - @include statusBannerColors($colorInfo, $colorInfoFg); - } - &.caution, - &.warning, - &.alert { - @include statusBannerColors($colorWarningLo,$colorWarningLoFg); - } - &.error { - @include statusBannerColors($colorWarningHi, $colorWarningHiFg); - } + &.info { + @include statusBannerColors($colorInfo, $colorInfoFg); + } + &.caution, + &.warning, + &.alert { + @include statusBannerColors($colorWarningLo, $colorWarningLoFg); + } + &.error { + @include statusBannerColors($colorWarningHi, $colorWarningHiFg); + } - &__message { - @include ellipsize(); - flex: 1 1 auto; - } + &__message { + @include ellipsize(); + flex: 1 1 auto; + } - &__progress-bar { - height: 7px; - width: 70px; + &__progress-bar { + height: 7px; + width: 70px; - // Only show the progress bar - .c-progress-bar { - &__text { - display: none; - } - } + // Only show the progress bar + .c-progress-bar { + &__text { + display: none; + } } + } - &__close-button { - font-size: 1.25em; - } + &__close-button { + font-size: 1.25em; + } } diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue index c8489e3fa4..3e7e2d5760 100644 --- a/src/ui/layout/tree-item.vue +++ b/src/ui/layout/tree-item.vue @@ -20,45 +20,44 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/mixins/context-menu-gesture.js b/src/ui/mixins/context-menu-gesture.js index ad7fa0de31..66f68b018d 100644 --- a/src/ui/mixins/context-menu-gesture.js +++ b/src/ui/mixins/context-menu-gesture.js @@ -1,61 +1,67 @@ export default { - inject: ['openmct'], - props: { - 'objectPath': { - type: Array, - default() { - return []; - } - } - }, - data() { - return { - contextClickActive: false - }; - }, - mounted() { - //TODO: touch support - this.$el.addEventListener('contextmenu', this.showContextMenu); - - function updateObject(oldObject, newObject) { - Object.assign(oldObject, newObject); - } - - this.objectPath.forEach(object => { - if (object) { - this.$once('hook:destroyed', - this.openmct.objects.observe(object, '*', updateObject.bind(this, object))); - } - }); - }, - destroyed() { - this.$el.removeEventListener('contextMenu', this.showContextMenu); - }, - methods: { - showContextMenu(event) { - if (this.readOnly) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - - let actionsCollection = this.openmct.actions.getActionsCollection(this.objectPath); - let actions = actionsCollection.getVisibleActions(); - let sortedActions = this.openmct.actions._groupAndSortActions(actions); - - const menuOptions = { - onDestroy: this.onContextMenuDestroyed - }; - - const menuItems = this.openmct.menus.actionsToMenuItems(sortedActions, actionsCollection.objectPath, actionsCollection.view); - this.openmct.menus.showMenu(event.clientX, event.clientY, menuItems, menuOptions); - this.contextClickActive = true; - this.$emit('context-click-active', true); - }, - onContextMenuDestroyed() { - this.contextClickActive = false; - this.$emit('context-click-active', false); - } + inject: ['openmct'], + props: { + objectPath: { + type: Array, + default() { + return []; + } } + }, + data() { + return { + contextClickActive: false + }; + }, + mounted() { + //TODO: touch support + this.$el.addEventListener('contextmenu', this.showContextMenu); + + function updateObject(oldObject, newObject) { + Object.assign(oldObject, newObject); + } + + this.objectPath.forEach((object) => { + if (object) { + this.$once( + 'hook:destroyed', + this.openmct.objects.observe(object, '*', updateObject.bind(this, object)) + ); + } + }); + }, + destroyed() { + this.$el.removeEventListener('contextMenu', this.showContextMenu); + }, + methods: { + showContextMenu(event) { + if (this.readOnly) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + let actionsCollection = this.openmct.actions.getActionsCollection(this.objectPath); + let actions = actionsCollection.getVisibleActions(); + let sortedActions = this.openmct.actions._groupAndSortActions(actions); + + const menuOptions = { + onDestroy: this.onContextMenuDestroyed + }; + + const menuItems = this.openmct.menus.actionsToMenuItems( + sortedActions, + actionsCollection.objectPath, + actionsCollection.view + ); + this.openmct.menus.showMenu(event.clientX, event.clientY, menuItems, menuOptions); + this.contextClickActive = true; + this.$emit('context-click-active', true); + }, + onContextMenuDestroyed() { + this.contextClickActive = false; + this.$emit('context-click-active', false); + } + } }; diff --git a/src/ui/mixins/object-link.js b/src/ui/mixins/object-link.js index f65a343fcd..b21b8bd11c 100644 --- a/src/ui/mixins/object-link.js +++ b/src/ui/mixins/object-link.js @@ -1,28 +1,28 @@ import objectPathToUrl from '../../tools/url'; export default { - inject: ['openmct'], - props: { - objectPath: { - type: Array, - default() { - return []; - } - } - }, - computed: { - objectLink() { - if (!this.objectPath.length) { - return; - } - - if (this.navigateToPath) { - return '#' + this.navigateToPath; - } - - const url = objectPathToUrl(this.openmct, this.objectPath); - - return url; - } + inject: ['openmct'], + props: { + objectPath: { + type: Array, + default() { + return []; + } } + }, + computed: { + objectLink() { + if (!this.objectPath.length) { + return; + } + + if (this.navigateToPath) { + return '#' + this.navigateToPath; + } + + const url = objectPathToUrl(this.openmct, this.objectPath); + + return url; + } + } }; diff --git a/src/ui/mixins/staleness-mixin.js b/src/ui/mixins/staleness-mixin.js index e082ff0b3d..80d1910133 100644 --- a/src/ui/mixins/staleness-mixin.js +++ b/src/ui/mixins/staleness-mixin.js @@ -23,46 +23,49 @@ import StalenessUtils from '@/utils/staleness'; export default { - data() { - return { - isStale: false - }; - }, - beforeDestroy() { - this.triggerUnsubscribeFromStaleness(); - }, - methods: { - subscribeToStaleness(domainObject, callback) { - if (!this.stalenessUtils) { - this.stalenessUtils = new StalenessUtils(this.openmct, domainObject); - } + data() { + return { + isStale: false + }; + }, + beforeDestroy() { + this.triggerUnsubscribeFromStaleness(); + }, + methods: { + subscribeToStaleness(domainObject, callback) { + if (!this.stalenessUtils) { + this.stalenessUtils = new StalenessUtils(this.openmct, domainObject); + } - this.requestStaleness(domainObject); - this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => { - this.handleStalenessResponse(stalenessResponse, callback); - }); - }, - async requestStaleness(domainObject) { - const stalenessResponse = await this.openmct.telemetry.isStale(domainObject); - if (stalenessResponse !== undefined) { - this.handleStalenessResponse(stalenessResponse); - } - }, - handleStalenessResponse(stalenessResponse, callback) { - if (this.stalenessUtils.shouldUpdateStaleness(stalenessResponse)) { - if (typeof callback === 'function') { - callback(stalenessResponse.isStale); - } else { - this.isStale = stalenessResponse.isStale; - } - } - }, - triggerUnsubscribeFromStaleness() { - if (this.unsubscribeFromStaleness) { - this.unsubscribeFromStaleness(); - delete this.unsubscribeFromStaleness; - this.stalenessUtils.destroy(); - } + this.requestStaleness(domainObject); + this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness( + domainObject, + (stalenessResponse) => { + this.handleStalenessResponse(stalenessResponse, callback); } + ); + }, + async requestStaleness(domainObject) { + const stalenessResponse = await this.openmct.telemetry.isStale(domainObject); + if (stalenessResponse !== undefined) { + this.handleStalenessResponse(stalenessResponse); + } + }, + handleStalenessResponse(stalenessResponse, callback) { + if (this.stalenessUtils.shouldUpdateStaleness(stalenessResponse)) { + if (typeof callback === 'function') { + callback(stalenessResponse.isStale); + } else { + this.isStale = stalenessResponse.isStale; + } + } + }, + triggerUnsubscribeFromStaleness() { + if (this.unsubscribeFromStaleness) { + this.unsubscribeFromStaleness(); + delete this.unsubscribeFromStaleness; + this.stalenessUtils.destroy(); + } } + } }; diff --git a/src/ui/mixins/toggle-mixin.js b/src/ui/mixins/toggle-mixin.js index 72ae339b06..eeda8f0842 100644 --- a/src/ui/mixins/toggle-mixin.js +++ b/src/ui/mixins/toggle-mixin.js @@ -1,31 +1,31 @@ export default { - data() { - return { - open: false - }; - }, - methods: { - toggle(event) { - if (this.open) { - if (this.isOpening) { - // Prevent document event handler from closing immediately - // after opening. Can't use stopPropagation because that - // would break other menus with similar behavior. - this.isOpening = false; + data() { + return { + open: false + }; + }, + methods: { + toggle(event) { + if (this.open) { + if (this.isOpening) { + // Prevent document event handler from closing immediately + // after opening. Can't use stopPropagation because that + // would break other menus with similar behavior. + this.isOpening = false; - return; - } - - document.removeEventListener('click', this.toggle); - this.open = false; - } else { - document.addEventListener('click', this.toggle); - this.open = true; - this.isOpening = true; - } + return; } - }, - destroyed() { + document.removeEventListener('click', this.toggle); + this.open = false; + } else { + document.addEventListener('click', this.toggle); + this.open = true; + this.isOpening = true; + } } + }, + destroyed() { + document.removeEventListener('click', this.toggle); + } }; diff --git a/src/ui/preview/Preview.vue b/src/ui/preview/Preview.vue index 0e14ac6a6e..d441bb4367 100644 --- a/src/ui/preview/Preview.vue +++ b/src/ui/preview/Preview.vue @@ -20,198 +20,211 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/preview/PreviewAction.js b/src/ui/preview/PreviewAction.js index cbf4c3b2d0..f2b8c07800 100644 --- a/src/ui/preview/PreviewAction.js +++ b/src/ui/preview/PreviewAction.js @@ -24,79 +24,81 @@ import Vue from 'vue'; import EventEmitter from 'EventEmitter'; export default class PreviewAction extends EventEmitter { - constructor(openmct) { - super(); - /** - * Metadata - */ - this.name = 'View'; - this.key = 'preview'; - this.description = 'View in large dialog'; - this.cssClass = 'icon-items-expand'; - this.group = 'windowing'; - this.priority = 1; + constructor(openmct) { + super(); + /** + * Metadata + */ + this.name = 'View'; + this.key = 'preview'; + this.description = 'View in large dialog'; + this.cssClass = 'icon-items-expand'; + this.group = 'windowing'; + this.priority = 1; - /** - * Dependencies - */ - this._openmct = openmct; + /** + * Dependencies + */ + this._openmct = openmct; - if (PreviewAction.isVisible === undefined) { - PreviewAction.isVisible = false; + if (PreviewAction.isVisible === undefined) { + PreviewAction.isVisible = false; + } + } + + invoke(objectPath, viewOptions) { + let preview = new Vue({ + components: { + Preview + }, + provide: { + openmct: this._openmct, + objectPath: objectPath + }, + data() { + return { + viewOptions + }; + }, + template: '' + }); + preview.$mount(); + + let overlay = this._openmct.overlays.overlay({ + element: preview.$el, + size: 'large', + autoHide: false, + buttons: [ + { + label: 'Done', + callback: () => overlay.dismiss() } - } + ], + onDestroy: () => { + PreviewAction.isVisible = false; + preview.$destroy(); + this.emit('isVisible', false); + } + }); - invoke(objectPath, viewOptions) { - let preview = new Vue({ - components: { - Preview - }, - provide: { - openmct: this._openmct, - objectPath: objectPath - }, - data() { - return { - viewOptions - }; - }, - template: '' - }); - preview.$mount(); + PreviewAction.isVisible = true; + this.emit('isVisible', true); + } - let overlay = this._openmct.overlays.overlay({ - element: preview.$el, - size: 'large', - autoHide: false, - buttons: [ - { - label: 'Done', - callback: () => overlay.dismiss() - } - ], - onDestroy: () => { - PreviewAction.isVisible = false; - preview.$destroy(); - this.emit('isVisible', false); - } - }); + appliesTo(objectPath, view = {}) { + const parentElement = view.parentElement; + const isObjectView = parentElement && parentElement.classList.contains('js-object-view'); - PreviewAction.isVisible = true; - this.emit('isVisible', true); - } + return ( + !PreviewAction.isVisible && + !this._openmct.router.isNavigatedObject(objectPath) && + !isObjectView + ); + } - appliesTo(objectPath, view = {}) { - const parentElement = view.parentElement; - const isObjectView = parentElement && parentElement.classList.contains('js-object-view'); + _preventPreview(objectPath) { + const noPreviewTypes = ['folder']; - return !PreviewAction.isVisible - && !this._openmct.router.isNavigatedObject(objectPath) - && !isObjectView; - } - - _preventPreview(objectPath) { - const noPreviewTypes = ['folder']; - - return noPreviewTypes.includes(objectPath[0].type); - } + return noPreviewTypes.includes(objectPath[0].type); + } } diff --git a/src/ui/preview/ViewHistoricalDataAction.js b/src/ui/preview/ViewHistoricalDataAction.js index b80450d69a..2a982b8650 100644 --- a/src/ui/preview/ViewHistoricalDataAction.js +++ b/src/ui/preview/ViewHistoricalDataAction.js @@ -23,22 +23,21 @@ import PreviewAction from './PreviewAction'; export default class ViewHistoricalDataAction extends PreviewAction { - constructor(openmct) { - super(openmct); + constructor(openmct) { + super(openmct); - this.name = 'View Historical Data'; - this.key = 'viewHistoricalData'; - this.description = 'View Historical Data in a Table or Plot'; - this.cssClass = 'icon-eye-open'; - this.hideInDefaultMenu = true; - } + this.name = 'View Historical Data'; + this.key = 'viewHistoricalData'; + this.description = 'View Historical Data in a Table or Plot'; + this.cssClass = 'icon-eye-open'; + this.hideInDefaultMenu = true; + } - appliesTo(objectPath, view = {}) { - let viewContext = view.getViewContext && view.getViewContext(); + appliesTo(objectPath, view = {}) { + let viewContext = view.getViewContext && view.getViewContext(); - return objectPath.length - && viewContext - && viewContext.row - && viewContext.row.viewHistoricalData; - } + return ( + objectPath.length && viewContext && viewContext.row && viewContext.row.viewHistoricalData + ); + } } diff --git a/src/ui/preview/plugin.js b/src/ui/preview/plugin.js index 200442d644..c9e8c5713b 100644 --- a/src/ui/preview/plugin.js +++ b/src/ui/preview/plugin.js @@ -23,8 +23,8 @@ import PreviewAction from './PreviewAction.js'; import ViewHistoricalDataAction from './ViewHistoricalDataAction'; export default function () { - return function (openmct) { - openmct.actions.register(new PreviewAction(openmct)); - openmct.actions.register(new ViewHistoricalDataAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new PreviewAction(openmct)); + openmct.actions.register(new ViewHistoricalDataAction(openmct)); + }; } diff --git a/src/ui/preview/preview-header.vue b/src/ui/preview/preview-header.vue index fb62c89aee..c94a30538a 100644 --- a/src/ui/preview/preview-header.vue +++ b/src/ui/preview/preview-header.vue @@ -20,167 +20,156 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/preview/preview.scss b/src/ui/preview/preview.scss index 418c19175b..7d5082f9cd 100644 --- a/src/ui/preview/preview.scss +++ b/src/ui/preview/preview.scss @@ -1,25 +1,28 @@ .l-preview-window { - display: flex; - flex-direction: column; - position: absolute; - top: 0; right: 0; bottom: 0; left: 0; + display: flex; + flex-direction: column; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; - > * + * { - margin-top: $interiorMargin; - } - - &__object-name { - flex: 0 0 auto; - } - - &__object-view { - flex: 1 1 auto; - height: 100%; // Chrome 73 - overflow: auto; - - > div:not([class]) { - // Target an immediate child div without a class and make it display: contents - display: contents; - } + > * + * { + margin-top: $interiorMargin; + } + + &__object-name { + flex: 0 0 auto; + } + + &__object-view { + flex: 1 1 auto; + height: 100%; // Chrome 73 + overflow: auto; + + > div:not([class]) { + // Target an immediate child div without a class and make it display: contents + display: contents; } + } } diff --git a/src/ui/registries/InspectorViewRegistry.js b/src/ui/registries/InspectorViewRegistry.js index f131cd051b..803afb2ec5 100644 --- a/src/ui/registries/InspectorViewRegistry.js +++ b/src/ui/registries/InspectorViewRegistry.js @@ -30,70 +30,71 @@ const DEFAULT_VIEW_PRIORITY = 0; * @memberof module:openmct */ export default class InspectorViewRegistry { - constructor() { - this.providers = {}; + constructor() { + this.providers = {}; + } + + /** + * + * @param {object} selection the object to be viewed + * @returns {module:openmct.InspectorViewRegistry[]} any providers + * which can provide views of this object + * @private for platform-internal use + */ + get(selection) { + function byPriority(providerA, providerB) { + const priorityA = providerA.priority?.() ?? DEFAULT_VIEW_PRIORITY; + const priorityB = providerB.priority?.() ?? DEFAULT_VIEW_PRIORITY; + + return priorityB - priorityA; } - /** - * - * @param {object} selection the object to be viewed - * @returns {module:openmct.InspectorViewRegistry[]} any providers - * which can provide views of this object - * @private for platform-internal use - */ - get(selection) { - function byPriority(providerA, providerB) { - const priorityA = providerA.priority?.() ?? DEFAULT_VIEW_PRIORITY; - const priorityB = providerB.priority?.() ?? DEFAULT_VIEW_PRIORITY; + return this.#getAllProviders() + .filter((provider) => provider.canView(selection)) + .map((provider) => { + const view = provider.view(selection); + view.key = provider.key; + view.name = provider.name; + view.glyph = provider.glyph; - return priorityB - priorityA; - } + return view; + }) + .sort(byPriority); + } - return this.#getAllProviders() - .filter(provider => provider.canView(selection)) - .map(provider => { - const view = provider.view(selection); - view.key = provider.key; - view.name = provider.name; - view.glyph = provider.glyph; + /** + * Registers a new type of view. + * + * @param {module:openmct.InspectorViewRegistry} provider the provider for this view + * @method addProvider + * @memberof module:openmct.InspectorViewRegistry# + */ + addProvider(provider) { + const key = provider.key; + const name = provider.name; - return view; - }).sort(byPriority); + if (key === undefined) { + throw "View providers must have a unique 'key' property defined"; } - /** - * Registers a new type of view. - * - * @param {module:openmct.InspectorViewRegistry} provider the provider for this view - * @method addProvider - * @memberof module:openmct.InspectorViewRegistry# - */ - addProvider(provider) { - const key = provider.key; - const name = provider.name; - - if (key === undefined) { - throw "View providers must have a unique 'key' property defined"; - } - - if (name === undefined) { - throw "View providers must have a 'name' property defined"; - } - - if (this.providers[key] !== undefined) { - console.warn(`Provider already defined for key '${key}'. Provider keys must be unique.`); - } - - this.providers[key] = provider; + if (name === undefined) { + throw "View providers must have a 'name' property defined"; } - getByProviderKey(key) { - return this.providers[key]; + if (this.providers[key] !== undefined) { + console.warn(`Provider already defined for key '${key}'. Provider keys must be unique.`); } - #getAllProviders() { - return Object.values(this.providers); - } + this.providers[key] = provider; + } + + getByProviderKey(key) { + return this.providers[key]; + } + + #getAllProviders() { + return Object.values(this.providers); + } } /** diff --git a/src/ui/registries/ToolbarRegistry.js b/src/ui/registries/ToolbarRegistry.js index a6803320aa..aab0080f51 100644 --- a/src/ui/registries/ToolbarRegistry.js +++ b/src/ui/registries/ToolbarRegistry.js @@ -21,102 +21,101 @@ *****************************************************************************/ define([], function () { + /** + * A ToolbarRegistry maintains the definitions for toolbars. + * + * @interface ToolbarRegistry + * @memberof module:openmct + */ + function ToolbarRegistry() { + this.providers = {}; + } - /** - * A ToolbarRegistry maintains the definitions for toolbars. - * - * @interface ToolbarRegistry - * @memberof module:openmct - */ - function ToolbarRegistry() { - this.providers = {}; + /** + * Gets toolbar controls from providers which can provide a toolbar for this selection. + * + * @param {object} selection the selection object + * @returns {Object[]} an array of objects defining controls for the toolbar + * @private for platform-internal use + */ + ToolbarRegistry.prototype.get = function (selection) { + const providers = this.getAllProviders().filter(function (provider) { + return provider.forSelection(selection); + }); + + const structure = []; + + providers.forEach((provider) => { + provider.toolbar(selection).forEach((item) => structure.push(item)); + }); + + return structure; + }; + + /** + * @private + */ + ToolbarRegistry.prototype.getAllProviders = function () { + return Object.values(this.providers); + }; + + /** + * @private + */ + ToolbarRegistry.prototype.getByProviderKey = function (key) { + return this.providers[key]; + }; + + /** + * Registers a new type of toolbar. + * + * @param {module:openmct.ToolbarRegistry} provider the provider for this toolbar + * @method addProvider + * @memberof module:openmct.ToolbarRegistry# + */ + ToolbarRegistry.prototype.addProvider = function (provider) { + const key = provider.key; + + if (key === undefined) { + throw "Toolbar providers must have a unique 'key' property defined."; } - /** - * Gets toolbar controls from providers which can provide a toolbar for this selection. - * - * @param {object} selection the selection object - * @returns {Object[]} an array of objects defining controls for the toolbar - * @private for platform-internal use - */ - ToolbarRegistry.prototype.get = function (selection) { - const providers = this.getAllProviders().filter(function (provider) { - return provider.forSelection(selection); - }); + if (this.providers[key] !== undefined) { + console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key); + } - const structure = []; + this.providers[key] = provider; + }; - providers.forEach(provider => { - provider.toolbar(selection).forEach(item => structure.push(item)); - }); + /** + * Exposes types of toolbars in Open MCT. + * + * @interface ToolbarProvider + * @property {string} key a unique identifier for this toolbar + * @property {string} name the human-readable name of this toolbar + * @property {string} [description] a longer-form description (typically + * a single sentence or short paragraph) of this kind of toolbar + * @memberof module:openmct + */ - return structure; - }; + /** + * Checks if this provider can supply toolbar for a selection. + * + * @method forSelection + * @memberof module:openmct.ToolbarProvider# + * @param {module:openmct.selection} selection + * @returns {boolean} 'true' if the toolbar applies to the provided selection, + * otherwise 'false'. + */ - /** - * @private - */ - ToolbarRegistry.prototype.getAllProviders = function () { - return Object.values(this.providers); - }; + /** + * Provides controls that comprise a toolbar. + * + * @method toolbar + * @memberof module:openmct.ToolbarProvider# + * @param {object} selection the selection object + * @returns {Object[]} an array of objects defining controls for the toolbar. + */ - /** - * @private - */ - ToolbarRegistry.prototype.getByProviderKey = function (key) { - return this.providers[key]; - }; - - /** - * Registers a new type of toolbar. - * - * @param {module:openmct.ToolbarRegistry} provider the provider for this toolbar - * @method addProvider - * @memberof module:openmct.ToolbarRegistry# - */ - ToolbarRegistry.prototype.addProvider = function (provider) { - const key = provider.key; - - if (key === undefined) { - throw "Toolbar providers must have a unique 'key' property defined."; - } - - if (this.providers[key] !== undefined) { - console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key); - } - - this.providers[key] = provider; - }; - - /** - * Exposes types of toolbars in Open MCT. - * - * @interface ToolbarProvider - * @property {string} key a unique identifier for this toolbar - * @property {string} name the human-readable name of this toolbar - * @property {string} [description] a longer-form description (typically - * a single sentence or short paragraph) of this kind of toolbar - * @memberof module:openmct - */ - - /** - * Checks if this provider can supply toolbar for a selection. - * - * @method forSelection - * @memberof module:openmct.ToolbarProvider# - * @param {module:openmct.selection} selection - * @returns {boolean} 'true' if the toolbar applies to the provided selection, - * otherwise 'false'. - */ - - /** - * Provides controls that comprise a toolbar. - * - * @method toolbar - * @memberof module:openmct.ToolbarProvider# - * @param {object} selection the selection object - * @returns {Object[]} an array of objects defining controls for the toolbar. - */ - - return ToolbarRegistry; + return ToolbarRegistry; }); diff --git a/src/ui/registries/ViewRegistry.js b/src/ui/registries/ViewRegistry.js index ea774b2662..e649fc3ac4 100644 --- a/src/ui/registries/ViewRegistry.js +++ b/src/ui/registries/ViewRegistry.js @@ -21,254 +21,254 @@ *****************************************************************************/ define(['EventEmitter'], function (EventEmitter) { - const DEFAULT_VIEW_PRIORITY = 100; + const DEFAULT_VIEW_PRIORITY = 100; - /** - * A ViewRegistry maintains the definitions for different kinds of views - * that may occur in different places in the user interface. - * @interface ViewRegistry - * @memberof module:openmct - */ - function ViewRegistry() { - EventEmitter.apply(this); - this.providers = {}; + /** + * A ViewRegistry maintains the definitions for different kinds of views + * that may occur in different places in the user interface. + * @interface ViewRegistry + * @memberof module:openmct + */ + function ViewRegistry() { + EventEmitter.apply(this); + this.providers = {}; + } + + ViewRegistry.prototype = Object.create(EventEmitter.prototype); + + /** + * @private for platform-internal use + * @param {*} item the object to be viewed + * @param {array} objectPath - The current contextual object path of the view object + * eg current domainObject is located under MyItems which is under Root + * @returns {module:openmct.ViewProvider[]} any providers + * which can provide views of this object + */ + ViewRegistry.prototype.get = function (item, objectPath) { + if (objectPath === undefined) { + throw 'objectPath must be provided to get applicable views for an object'; } - ViewRegistry.prototype = Object.create(EventEmitter.prototype); + function byPriority(providerA, providerB) { + let priorityA = providerA.priority ? providerA.priority(item) : DEFAULT_VIEW_PRIORITY; + let priorityB = providerB.priority ? providerB.priority(item) : DEFAULT_VIEW_PRIORITY; - /** - * @private for platform-internal use - * @param {*} item the object to be viewed - * @param {array} objectPath - The current contextual object path of the view object - * eg current domainObject is located under MyItems which is under Root - * @returns {module:openmct.ViewProvider[]} any providers - * which can provide views of this object - */ - ViewRegistry.prototype.get = function (item, objectPath) { - if (objectPath === undefined) { - throw "objectPath must be provided to get applicable views for an object"; - } + return priorityB - priorityA; + } - function byPriority(providerA, providerB) { - let priorityA = providerA.priority ? providerA.priority(item) : DEFAULT_VIEW_PRIORITY; - let priorityB = providerB.priority ? providerB.priority(item) : DEFAULT_VIEW_PRIORITY; + return this.getAllProviders() + .filter(function (provider) { + return provider.canView(item, objectPath); + }) + .sort(byPriority); + }; - return priorityB - priorityA; - } + /** + * @private + */ + ViewRegistry.prototype.getAllProviders = function () { + return Object.values(this.providers); + }; - return this.getAllProviders() - .filter(function (provider) { - return provider.canView(item, objectPath); - }).sort(byPriority); + /** + * Register a new type of view. + * + * @param {module:openmct.ViewProvider} provider the provider for this view + * @method addProvider + * @memberof module:openmct.ViewRegistry# + */ + ViewRegistry.prototype.addProvider = function (provider) { + const key = provider.key; + if (key === undefined) { + throw "View providers must have a unique 'key' property defined"; + } + + if (this.providers[key] !== undefined) { + console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key); + } + + const wrappedView = provider.view.bind(provider); + provider.view = (domainObject, objectPath) => { + const viewObject = wrappedView(domainObject, objectPath); + const wrappedShow = viewObject.show.bind(viewObject); + viewObject.key = key; // provide access to provider key on view object + viewObject.show = (element, isEditing, viewOptions) => { + viewObject.parentElement = element.parentElement; + wrappedShow(element, isEditing, viewOptions); + }; + + return viewObject; }; - /** - * @private - */ - ViewRegistry.prototype.getAllProviders = function () { - return Object.values(this.providers); - }; + this.providers[key] = provider; + }; - /** - * Register a new type of view. - * - * @param {module:openmct.ViewProvider} provider the provider for this view - * @method addProvider - * @memberof module:openmct.ViewRegistry# - */ - ViewRegistry.prototype.addProvider = function (provider) { - const key = provider.key; - if (key === undefined) { - throw "View providers must have a unique 'key' property defined"; - } + /** + * @private + */ + ViewRegistry.prototype.getByProviderKey = function (key) { + return this.providers[key]; + }; - if (this.providers[key] !== undefined) { - console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key); - } + /** + * Used internally to support seamless usage of new views with old + * views. + * @private + */ + ViewRegistry.prototype.getByVPID = function (vpid) { + return this.providers.filter(function (p) { + return p.vpid === vpid; + })[0]; + }; - const wrappedView = provider.view.bind(provider); - provider.view = (domainObject, objectPath) => { - const viewObject = wrappedView(domainObject, objectPath); - const wrappedShow = viewObject.show.bind(viewObject); - viewObject.key = key; // provide access to provider key on view object - viewObject.show = (element, isEditing, viewOptions) => { - viewObject.parentElement = element.parentElement; - wrappedShow(element, isEditing, viewOptions); - }; + /** + * A View is used to provide displayable content, and to react to + * associated life cycle events. + * + * @name View + * @interface + * @memberof module:openmct + */ - return viewObject; - }; + /** + * Populate the supplied DOM element with the contents of this view. + * + * View implementations should use this method to attach any + * listeners or acquire other resources that are necessary to keep + * the contents of this view up-to-date. + * + * @param {HTMLElement} container the DOM element to populate + * @method show + * @memberof module:openmct.View# + */ - this.providers[key] = provider; - }; + /** + * Indicates whether or not the application is in edit mode. This supports + * views that have distinct visual and behavioral elements when the + * navigated object is being edited. + * + * For cases where a completely separate view is desired for editing purposes, + * see {@link openmct.ViewProvider#edit} + * + * @param {boolean} isEditing + * @method show + * @memberof module:openmct.View# + */ - /** - * @private - */ - ViewRegistry.prototype.getByProviderKey = function (key) { - return this.providers[key]; - }; + /** + * Release any resources associated with this view. + * + * View implementations should use this method to detach any + * listeners or release other resources that are no longer necessary + * once a view is no longer used. + * + * @method destroy + * @memberof module:openmct.View# + */ - /** - * Used internally to support seamless usage of new views with old - * views. - * @private - */ - ViewRegistry.prototype.getByVPID = function (vpid) { - return this.providers.filter(function (p) { - return p.vpid === vpid; - })[0]; - }; + /** + * Returns the selection context. + * + * View implementations should use this method to customize + * the selection context. + * + * @method getSelectionContext + * @memberof module:openmct.View# + */ - /** - * A View is used to provide displayable content, and to react to - * associated life cycle events. - * - * @name View - * @interface - * @memberof module:openmct - */ + /** + * Exposes types of views in Open MCT. + * + * @interface ViewProvider + * @property {string} key a unique identifier for this view + * @property {string} name the human-readable name of this view + * @property {string} [description] a longer-form description (typically + * a single sentence or short paragraph) of this kind of view + * @property {string} [cssClass] the CSS class to apply to labels for this + * view (to add icons, for instance) + * @memberof module:openmct + */ - /** - * Populate the supplied DOM element with the contents of this view. - * - * View implementations should use this method to attach any - * listeners or acquire other resources that are necessary to keep - * the contents of this view up-to-date. - * - * @param {HTMLElement} container the DOM element to populate - * @method show - * @memberof module:openmct.View# - */ + /** + * Check if this provider can supply views for a domain object. + * + * When called by Open MCT, this may include additional arguments + * which are on the path to the object to be viewed; for instance, + * when viewing "A Folder" within "My Items", this method will be + * invoked with "A Folder" (as a domain object) as the first argument + * + * @method canView + * @memberof module:openmct.ViewProvider# + * @param {module:openmct.DomainObject} domainObject the domain object + * to be viewed + * @param {array} objectPath - The current contextual object path of the view object + * eg current domainObject is located under MyItems which is under Root + * @returns {boolean} 'true' if the view applies to the provided object, + * otherwise 'false'. + */ - /** - * Indicates whether or not the application is in edit mode. This supports - * views that have distinct visual and behavioral elements when the - * navigated object is being edited. - * - * For cases where a completely separate view is desired for editing purposes, - * see {@link openmct.ViewProvider#edit} - * - * @param {boolean} isEditing - * @method show - * @memberof module:openmct.View# - */ + /** + * An optional function that defines whether or not this view can be used to edit a given object. + * If not provided, will default to `false` and the view will not support editing. To support editing, + * return true from this function and then - + * * Return a {@link openmct.View} from the `view` function, using the `onEditModeChange` callback to + * add and remove editing elements from the view + * OR + * * Return a {@link openmct.View} from the `view` function defining a read-only view. + * AND + * * Define an {@link openmct.ViewProvider#Edit} function on the view provider that returns an + * editing-specific view. + * + * @method canEdit + * @memberof module:openmct.ViewProvider# + * @param {module:openmct.DomainObject} domainObject the domain object + * to be edited + * @param {array} objectPath - The current contextual object path of the view object + * eg current domainObject is located under MyItems which is under Root + * @returns {boolean} 'true' if the view can be used to edit the provided object, + * otherwise 'false'. + */ - /** - * Release any resources associated with this view. - * - * View implementations should use this method to detach any - * listeners or release other resources that are no longer necessary - * once a view is no longer used. - * - * @method destroy - * @memberof module:openmct.View# - */ + /** + * Optional method determining the priority of a given view. If this + * function is not defined on a view provider, then a default priority + * of 100 will be applicable for all objects supported by this view. + * + * @method priority + * @memberof module:openmct.ViewProvider# + * @param {module:openmct.DomainObject} domainObject the domain object + * to be viewed + * @returns {number} The priority of the view. If multiple views could apply + * to an object, the view that returns the lowest number will be + * the default view. + */ - /** - * Returns the selection context. - * - * View implementations should use this method to customize - * the selection context. - * - * @method getSelectionContext - * @memberof module:openmct.View# - */ + /** + * Provide a view of this object. + * + * When called by Open MCT, the following arguments will be passed to it: + * @param {object} domainObject - the domainObject that the view is provided for + * @param {array} objectPath - The current contextual object path of the view object + * eg current domainObject is located under MyItems which is under Root + * + * @method view + * @memberof module:openmct.ViewProvider# + * @param {*} object the object to be viewed + * @returns {module:openmct.View} a view of this domain object + */ - /** - * Exposes types of views in Open MCT. - * - * @interface ViewProvider - * @property {string} key a unique identifier for this view - * @property {string} name the human-readable name of this view - * @property {string} [description] a longer-form description (typically - * a single sentence or short paragraph) of this kind of view - * @property {string} [cssClass] the CSS class to apply to labels for this - * view (to add icons, for instance) - * @memberof module:openmct - */ - - /** - * Check if this provider can supply views for a domain object. - * - * When called by Open MCT, this may include additional arguments - * which are on the path to the object to be viewed; for instance, - * when viewing "A Folder" within "My Items", this method will be - * invoked with "A Folder" (as a domain object) as the first argument - * - * @method canView - * @memberof module:openmct.ViewProvider# - * @param {module:openmct.DomainObject} domainObject the domain object - * to be viewed - * @param {array} objectPath - The current contextual object path of the view object - * eg current domainObject is located under MyItems which is under Root - * @returns {boolean} 'true' if the view applies to the provided object, - * otherwise 'false'. - */ - - /** - * An optional function that defines whether or not this view can be used to edit a given object. - * If not provided, will default to `false` and the view will not support editing. To support editing, - * return true from this function and then - - * * Return a {@link openmct.View} from the `view` function, using the `onEditModeChange` callback to - * add and remove editing elements from the view - * OR - * * Return a {@link openmct.View} from the `view` function defining a read-only view. - * AND - * * Define an {@link openmct.ViewProvider#Edit} function on the view provider that returns an - * editing-specific view. - * - * @method canEdit - * @memberof module:openmct.ViewProvider# - * @param {module:openmct.DomainObject} domainObject the domain object - * to be edited - * @param {array} objectPath - The current contextual object path of the view object - * eg current domainObject is located under MyItems which is under Root - * @returns {boolean} 'true' if the view can be used to edit the provided object, - * otherwise 'false'. - */ - - /** - * Optional method determining the priority of a given view. If this - * function is not defined on a view provider, then a default priority - * of 100 will be applicable for all objects supported by this view. - * - * @method priority - * @memberof module:openmct.ViewProvider# - * @param {module:openmct.DomainObject} domainObject the domain object - * to be viewed - * @returns {number} The priority of the view. If multiple views could apply - * to an object, the view that returns the lowest number will be - * the default view. - */ - - /** - * Provide a view of this object. - * - * When called by Open MCT, the following arguments will be passed to it: - * @param {object} domainObject - the domainObject that the view is provided for - * @param {array} objectPath - The current contextual object path of the view object - * eg current domainObject is located under MyItems which is under Root - * - * @method view - * @memberof module:openmct.ViewProvider# - * @param {*} object the object to be viewed - * @returns {module:openmct.View} a view of this domain object - */ - - /** - * Provide an edit-mode specific view of this object. - * - * If optionally specified, this function will be called when the application - * enters edit mode. This will cause the active non-edit mode view and its - * dom element to be destroyed. - * - * @method edit - * @memberof module:openmct.ViewProvider# - * @param {*} object the object to be edit - * @returns {module:openmct.View} an editable view of this domain object - */ - - return ViewRegistry; + /** + * Provide an edit-mode specific view of this object. + * + * If optionally specified, this function will be called when the application + * enters edit mode. This will cause the active non-edit mode view and its + * dom element to be destroyed. + * + * @method edit + * @memberof module:openmct.ViewProvider# + * @param {*} object the object to be edit + * @returns {module:openmct.View} an editable view of this domain object + */ + return ViewRegistry; }); diff --git a/src/ui/router/ApplicationRouter.js b/src/ui/router/ApplicationRouter.js index fc6a08fe90..9717d05e36 100644 --- a/src/ui/router/ApplicationRouter.js +++ b/src/ui/router/ApplicationRouter.js @@ -26,7 +26,7 @@ const EventEmitter = require('EventEmitter'); const _ = require('lodash'); class ApplicationRouter extends EventEmitter { - /** + /** * events * change:params -> notify listeners w/ new, old, and changed. * change:path -> notify listeners w/ new, old paths. @@ -41,378 +41,369 @@ class ApplicationRouter extends EventEmitter { * route(path, handler); * start(); Start routing. */ - constructor(openmct) { - super(); + constructor(openmct) { + super(); - this.locationBar = new LocationBar(); - this.openmct = openmct; - this.routes = []; - this.started = false; + this.locationBar = new LocationBar(); + this.openmct = openmct; + this.routes = []; + this.started = false; - this.setHash = _.debounce(this.setHash.bind(this), 300); + this.setHash = _.debounce(this.setHash.bind(this), 300); - openmct.once('destroy', () => { - this.destroy(); - }); + openmct.once('destroy', () => { + this.destroy(); + }); + } + + // Public Methods + + destroy() { + this.locationBar.stop(); + } + + /** + * Delete a given query parameter from current url + * + * @param {string} paramName name of searchParam to delete from current url searchParams + */ + deleteSearchParam(paramName) { + let url = this.getHashRelativeURL(); + + url.searchParams.delete(paramName); + this.setLocationFromUrl(); + } + + /** + * object for accessing all current search parameters + * + * @returns {URLSearchParams} A {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|URLSearchParams} + */ + getAllSearchParams() { + return this.getHashRelativeURL().searchParams; + } + + /** + * Uniquely identifies a domain object. + * + * @typedef CurrentLocation + * @property {URL} url current url location + * @property {string} path current url location pathname + * @property {string} getQueryString a function which returns url search query + * @property {object} params object representing url searchParams + */ + + /** + * object for accessing current url location and search params + * + * @returns {CurrentLocation} A {@link CurrentLocation} + */ + getCurrentLocation() { + return this.currentLocation; + } + + /** + * Get current location URL Object + * + * @returns {URL} current url location + */ + getHashRelativeURL() { + return this.getCurrentLocation().url; + } + + /** + * Get current location URL Object searchParams + * + * @returns {object} object representing current url searchParams + */ + getParams() { + return this.currentLocation.params; + } + + /** + * Get a value of given param from current url searchParams + * + * @returns {string} value of paramName from current url searchParams + */ + getSearchParam(paramName) { + return this.getAllSearchParams().get(paramName); + } + + /** + * Navigate to given hash, update current location object, and notify listeners about location change + * + * @param {string} hash The URL hash to navigate to in the form of "#/browse/mine/{keyString}/{keyString}". + * Should not include any params. + */ + navigate(hash) { + this.handleLocationChange(hash.substring(1)); + } + + /** + * Check if a given object and current location object are same + * + * @param {Array} objectPath Object path of a given Domain Object + * + * @returns {Boolean} + */ + isNavigatedObject(objectPath) { + let targetObject = objectPath[0]; + let navigatedObject = this.path[0]; + + if (!targetObject.identifier) { + return false; } - // Public Methods + return this.openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier); + } - destroy() { - this.locationBar.stop(); + /** + * Add routes listeners + * + * @param {string} matcher Regex to match value in url + * @param {@function} callback function called when found match in url + */ + route(matcher, callback) { + this.routes.push({ + matcher, + callback + }); + } + + /** + * Set url hash using path and queryString + * + * @param {string} path path for url + * @param {string} queryString queryString for url + */ + set(path, queryString) { + this.setHash(`${path}?${queryString}`); + } + + /** + * Will replace all current search parameters with the ones defined in urlSearchParams + */ + setAllSearchParams() { + this.setLocationFromUrl(); + } + + /** + * To force update url based on value in currentLocation object + */ + setLocationFromUrl() { + this.updateTimeSettings(); + } + + /** + * Set url hash using path + * + * @param {string} path path for url + */ + setPath(path) { + this.handleLocationChange(path.substring(1)); + } + + /** + * Update param value from current url searchParams + * + * @param {string} paramName param name from current url searchParams + * @param {string} paramValue param value from current url searchParams + */ + setSearchParam(paramName, paramValue) { + let url = this.getHashRelativeURL(); + + url.searchParams.set(paramName, paramValue); + this.setLocationFromUrl(); + } + + /** + * start application routing, should be done after handlers are registered. + */ + start() { + if (this.started) { + throw new Error('Router already started!'); } - /** - * Delete a given query parameter from current url - * - * @param {string} paramName name of searchParam to delete from current url searchParams - */ - deleteSearchParam(paramName) { - let url = this.getHashRelativeURL(); + this.started = true; - url.searchParams.delete(paramName); - this.setLocationFromUrl(); + this.locationBar.onChange((p) => this.hashChanged(p)); + this.locationBar.start({ + root: location.pathname + }); + } + + /** + * Set url hash using path and searchParams object + * + * @param {string} path path for url + * @param {string} params oject representing searchParams key/value + */ + update(path, params) { + let searchParams = this.currentLocation.url.searchParams; + for (let [key, value] of Object.entries(params)) { + if (typeof value === 'undefined') { + searchParams.delete(key); + } else { + searchParams.set(key, value); + } } - /** - * object for accessing all current search parameters - * - * @returns {URLSearchParams} A {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|URLSearchParams} - */ - getAllSearchParams() { - return this.getHashRelativeURL().searchParams; + this.set(path, searchParams.toString()); + } + + /** + * Update route params. Takes an object of updates. New parameters + */ + updateParams(updateParams) { + let searchParams = this.currentLocation.url.searchParams; + Object.entries(updateParams).forEach(([key, value]) => { + if (typeof value === 'undefined') { + searchParams.delete(key); + } else { + searchParams.set(key, value); + } + }); + + this.setQueryString(searchParams.toString()); + } + + /** + * To force update url based on value in currentLocation object + */ + updateTimeSettings() { + const hash = `${this.currentLocation.path}?${this.currentLocation.getQueryString()}`; + + this.setHash(hash); + } + + // Private Methods + + /** + * @private + * Create currentLocation object + * + * @param {string} pathString USVString representing relative URL. + * + * @returns {CurrentLocation} A {@link CurrentLocation} + */ + createLocation(pathString) { + if (pathString[0] !== '/') { + pathString = '/' + pathString; } - /** - * Uniquely identifies a domain object. - * - * @typedef CurrentLocation - * @property {URL} url current url location - * @property {string} path current url location pathname - * @property {string} getQueryString a function which returns url search query - * @property {object} params object representing url searchParams - */ + let url = new URL(pathString, `${location.protocol}//${location.host}${location.pathname}`); - /** - * object for accessing current url location and search params - * - * @returns {CurrentLocation} A {@link CurrentLocation} - */ - getCurrentLocation() { - return this.currentLocation; + return { + url: url, + path: url.pathname, + getQueryString: () => url.search.replace(/^\?/, ''), + params: paramsToObject(url.searchParams) + }; + } + + /** + * @private + * Compare new and old path and on change emit event 'change:path' + * + * @param {string} newPath new path of url + * @param {string} oldPath old path of url + */ + doPathChange(newPath, oldPath) { + if (newPath === oldPath) { + return; } - /** - * Get current location URL Object - * - * @returns {URL} current url location - */ - getHashRelativeURL() { - return this.getCurrentLocation().url; + let route = this.routes.filter((r) => r.matcher.test(newPath))[0]; + if (route) { + route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params); } - /** - * Get current location URL Object searchParams - * - * @returns {object} object representing current url searchParams - */ - getParams() { - return this.currentLocation.params; + this.openmct.telemetry.abortAllRequests(); + + this.emit('change:path', newPath, oldPath); + } + + /** + * @private + * Compare new and old params and on change emit event 'change:params' + * + * @param {object} newParams new params of url + * @param {object} oldParams old params of url + */ + doParamsChange(newParams, oldParams) { + if (_.isEqual(newParams, oldParams)) { + return; } - /** - * Get a value of given param from current url searchParams - * - * @returns {string} value of paramName from current url searchParams - */ - getSearchParam(paramName) { - return this.getAllSearchParams().get(paramName); + let changedParams = {}; + Object.entries(newParams).forEach(([key, value]) => { + if (value !== oldParams[key]) { + changedParams[key] = value; + } + }); + Object.keys(oldParams).forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(newParams, key)) { + changedParams[key] = undefined; + } + }); + + this.emit('change:params', newParams, oldParams, changedParams); + } + + /** + * @private + * On location change, update currentLocation object and emit appropriate events + * + * @param {string} pathString USVString representing relative URL. + */ + handleLocationChange(pathString) { + let oldLocation = this.currentLocation; + let newLocation = this.createLocation(pathString); + + this.currentLocation = newLocation; + + if (!oldLocation) { + this.doPathChange(newLocation.path, null); + this.doParamsChange(newLocation.params, {}); + + return; } - /** - * Navigate to given hash, update current location object, and notify listeners about location change - * - * @param {string} hash The URL hash to navigate to in the form of "#/browse/mine/{keyString}/{keyString}". - * Should not include any params. - */ - navigate(hash) { - this.handleLocationChange(hash.substring(1)); - } + this.doPathChange(newLocation.path, oldLocation.path); - /** - * Check if a given object and current location object are same - * - * @param {Array} objectPath Object path of a given Domain Object - * - * @returns {Boolean} - */ - isNavigatedObject(objectPath) { - let targetObject = objectPath[0]; - let navigatedObject = this.path[0]; + this.doParamsChange(newLocation.params, oldLocation.params); + } - if (!targetObject.identifier) { - return false; - } + /** + * @private + * On hash changed, update currentLocation object and emit appropriate events + * + * @param {string} hash new hash for url + */ + hashChanged(hash) { + this.emit('change:hash', hash); + this.handleLocationChange(hash); + } - return this.openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier); - } + /** + * @private + * Set new hash for url + * + * @param {string} hash new hash for url + */ + setHash(hash) { + location.hash = '#' + hash.replace(/#/g, ''); + } - /** - * Add routes listeners - * - * @param {string} matcher Regex to match value in url - * @param {@function} callback function called when found match in url - */ - route(matcher, callback) { - this.routes.push({ - matcher, - callback - }); - } - - /** - * Set url hash using path and queryString - * - * @param {string} path path for url - * @param {string} queryString queryString for url - */ - set(path, queryString) { - this.setHash(`${path}?${queryString}`); - } - - /** - * Will replace all current search parameters with the ones defined in urlSearchParams - */ - setAllSearchParams() { - this.setLocationFromUrl(); - } - - /** - * To force update url based on value in currentLocation object - */ - setLocationFromUrl() { - this.updateTimeSettings(); - } - - /** - * Set url hash using path - * - * @param {string} path path for url - */ - setPath(path) { - this.handleLocationChange(path.substring(1)); - } - - /** - * Update param value from current url searchParams - * - * @param {string} paramName param name from current url searchParams - * @param {string} paramValue param value from current url searchParams - */ - setSearchParam(paramName, paramValue) { - let url = this.getHashRelativeURL(); - - url.searchParams.set(paramName, paramValue); - this.setLocationFromUrl(); - } - - /** - * start application routing, should be done after handlers are registered. - */ - start() { - if (this.started) { - throw new Error('Router already started!'); - } - - this.started = true; - - this.locationBar.onChange(p => this.hashChanged(p)); - this.locationBar.start({ - root: location.pathname - }); - } - - /** - * Set url hash using path and searchParams object - * - * @param {string} path path for url - * @param {string} params oject representing searchParams key/value - */ - update(path, params) { - let searchParams = this.currentLocation.url.searchParams; - for (let [key, value] of Object.entries(params)) { - if (typeof value === 'undefined') { - searchParams.delete(key); - } else { - searchParams.set(key, value); - } - } - - this.set(path, searchParams.toString()); - } - - /** - * Update route params. Takes an object of updates. New parameters - */ - updateParams(updateParams) { - let searchParams = this.currentLocation.url.searchParams; - Object.entries(updateParams).forEach(([key, value]) => { - if (typeof value === 'undefined') { - searchParams.delete(key); - } else { - searchParams.set(key, value); - } - }); - - this.setQueryString(searchParams.toString()); - } - - /** - * To force update url based on value in currentLocation object - */ - updateTimeSettings() { - const hash = `${this.currentLocation.path}?${this.currentLocation.getQueryString()}`; - - this.setHash(hash); - } - - // Private Methods - - /** - * @private - * Create currentLocation object - * - * @param {string} pathString USVString representing relative URL. - * - * @returns {CurrentLocation} A {@link CurrentLocation} - */ - createLocation(pathString) { - if (pathString[0] !== '/') { - pathString = '/' + pathString; - } - - let url = new URL( - pathString, - `${location.protocol}//${location.host}${location.pathname}` - ); - - return { - url: url, - path: url.pathname, - getQueryString: () => url.search.replace(/^\?/, ''), - params: paramsToObject(url.searchParams) - }; - } - - /** - * @private - * Compare new and old path and on change emit event 'change:path' - * - * @param {string} newPath new path of url - * @param {string} oldPath old path of url - */ - doPathChange(newPath, oldPath) { - if (newPath === oldPath) { - return; - } - - let route = this.routes.filter(r => r.matcher.test(newPath))[0]; - if (route) { - route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params); - } - - this.openmct.telemetry.abortAllRequests(); - - this.emit('change:path', newPath, oldPath); - } - - /** - * @private - * Compare new and old params and on change emit event 'change:params' - * - * @param {object} newParams new params of url - * @param {object} oldParams old params of url - */ - doParamsChange(newParams, oldParams) { - if (_.isEqual(newParams, oldParams)) { - return; - } - - let changedParams = {}; - Object.entries(newParams).forEach(([key, value]) => { - if (value !== oldParams[key]) { - changedParams[key] = value; - } - }); - Object.keys(oldParams).forEach(key => { - if (!Object.prototype.hasOwnProperty.call(newParams, key)) { - changedParams[key] = undefined; - } - }); - - this.emit('change:params', newParams, oldParams, changedParams); - } - - /** - * @private - * On location change, update currentLocation object and emit appropriate events - * - * @param {string} pathString USVString representing relative URL. - */ - handleLocationChange(pathString) { - let oldLocation = this.currentLocation; - let newLocation = this.createLocation(pathString); - - this.currentLocation = newLocation; - - if (!oldLocation) { - this.doPathChange(newLocation.path, null); - this.doParamsChange(newLocation.params, {}); - - return; - } - - this.doPathChange( - newLocation.path, - oldLocation.path - ); - - this.doParamsChange( - newLocation.params, - oldLocation.params - ); - } - - /** - * @private - * On hash changed, update currentLocation object and emit appropriate events - * - * @param {string} hash new hash for url - */ - hashChanged(hash) { - this.emit('change:hash', hash); - this.handleLocationChange(hash); - } - - /** - * @private - * Set new hash for url - * - * @param {string} hash new hash for url - */ - setHash(hash) { - location.hash = '#' + hash.replace(/#/g, ''); - } - - /** - * @private - * Set queryString part of current url - * - * @param {string} queryString queryString part of url - */ - setQueryString(queryString) { - this.handleLocationChange(`${this.currentLocation.path}?${queryString}`); - } + /** + * @private + * Set queryString part of current url + * + * @param {string} queryString queryString part of url + */ + setQueryString(queryString) { + this.handleLocationChange(`${this.currentLocation.path}?${queryString}`); + } } /** @@ -423,20 +414,20 @@ class ApplicationRouter extends EventEmitter { * @returns {Object} */ function paramsToObject(searchParams) { - let params = {}; - for (let [key, value] of searchParams.entries()) { - if (params[key]) { - if (!Array.isArray(params[key])) { - params[key] = [params[key]]; - } + let params = {}; + for (let [key, value] of searchParams.entries()) { + if (params[key]) { + if (!Array.isArray(params[key])) { + params[key] = [params[key]]; + } - params[key].push(value); - } else { - params[key] = value; - } + params[key].push(value); + } else { + params[key] = value; } + } - return params; + return params; } module.exports = ApplicationRouter; diff --git a/src/ui/router/ApplicationRouterSpec.js b/src/ui/router/ApplicationRouterSpec.js index 772058df2c..932765b3d2 100644 --- a/src/ui/router/ApplicationRouterSpec.js +++ b/src/ui/router/ApplicationRouterSpec.js @@ -7,84 +7,84 @@ let appHolder; let resolveFunction; xdescribe('Application router utility functions', () => { - beforeEach(done => { - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; + beforeEach((done) => { + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; - openmct = createOpenMct(); - openmct.install(openmct.plugins.MyItems()); + openmct = createOpenMct(); + openmct.install(openmct.plugins.MyItems()); - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); - openmct.on('start', () => { - resolveFunction = () => { - const success = window.location.hash !== null && window.location.hash !== ''; - if (success) { - done(); - } - }; + openmct.on('start', () => { + resolveFunction = () => { + const success = window.location.hash !== null && window.location.hash !== ''; + if (success) { + done(); + } + }; - openmct.router.on('change:hash', resolveFunction); - // We have a debounce set to 300ms on setHash, so if we don't flush, - // the above resolve function sometimes doesn't fire due to a race condition. - openmct.router.setHash.flush(); - openmct.router.setLocationFromUrl(); - }); - - openmct.start(appHolder); - - document.body.append(appHolder); + openmct.router.on('change:hash', resolveFunction); + // We have a debounce set to 300ms on setHash, so if we don't flush, + // the above resolve function sometimes doesn't fire due to a race condition. + openmct.router.setHash.flush(); + openmct.router.setLocationFromUrl(); }); - afterEach(() => { - openmct.router.removeListener('change:hash', resolveFunction); - appHolder.remove(); + openmct.start(appHolder); - return resetApplicationState(openmct); - }); + document.body.append(appHolder); + }); - it('has initial hash when loaded', () => { - const success = window.location.hash !== null; - expect(success).toBe(true); - }); + afterEach(() => { + openmct.router.removeListener('change:hash', resolveFunction); + appHolder.remove(); - it('The setSearchParam function sets an individual search parameter in the window location hash', () => { - openmct.router.setSearchParam('testParam1', 'testValue1'); + return resetApplicationState(openmct); + }); - const searchParams = openmct.router.getAllSearchParams(); - expect(searchParams.get('testParam1')).toBe('testValue1'); - }); + it('has initial hash when loaded', () => { + const success = window.location.hash !== null; + expect(success).toBe(true); + }); - it('The deleteSearchParam function deletes an individual search paramater in the window location hash', () => { - openmct.router.deleteSearchParam('testParam'); - const searchParams = openmct.router.getAllSearchParams(); - expect(searchParams.get('testParam')).toBe(null); - }); + it('The setSearchParam function sets an individual search parameter in the window location hash', () => { + openmct.router.setSearchParam('testParam1', 'testValue1'); - it('The setSearchParam function sets a multiple individual search parameters in the window location hash', () => { - openmct.router.setSearchParam('testParam1', 'testValue1'); - openmct.router.setSearchParam('testParam2', 'testValue2'); + const searchParams = openmct.router.getAllSearchParams(); + expect(searchParams.get('testParam1')).toBe('testValue1'); + }); - const searchParams = openmct.router.getAllSearchParams(); - expect(searchParams.get('testParam1')).toBe('testValue1'); - expect(searchParams.get('testParam2')).toBe('testValue2'); - }); + it('The deleteSearchParam function deletes an individual search paramater in the window location hash', () => { + openmct.router.deleteSearchParam('testParam'); + const searchParams = openmct.router.getAllSearchParams(); + expect(searchParams.get('testParam')).toBe(null); + }); - it('The setAllSearchParams function replaces all search paramaters in the window location hash', () => { - openmct.router.setSearchParam('testParam2', 'updatedtestValue2'); - openmct.router.setSearchParam('newTestParam3', 'newTestValue3'); + it('The setSearchParam function sets a multiple individual search parameters in the window location hash', () => { + openmct.router.setSearchParam('testParam1', 'testValue1'); + openmct.router.setSearchParam('testParam2', 'testValue2'); - const searchParams = openmct.router.getAllSearchParams(); - expect(searchParams.get('testParam2')).toBe('updatedtestValue2'); - expect(searchParams.get('newTestParam3')).toBe('newTestValue3'); - }); + const searchParams = openmct.router.getAllSearchParams(); + expect(searchParams.get('testParam1')).toBe('testValue1'); + expect(searchParams.get('testParam2')).toBe('testValue2'); + }); - it('The doPathChange function triggers aborting all requests when doing a path change', () => { - const abortSpy = spyOn(openmct.telemetry, 'abortAllRequests'); - openmct.router.doPathChange('newPath', 'oldPath'); - expect(abortSpy).toHaveBeenCalledTimes(1); - }); + it('The setAllSearchParams function replaces all search paramaters in the window location hash', () => { + openmct.router.setSearchParam('testParam2', 'updatedtestValue2'); + openmct.router.setSearchParam('newTestParam3', 'newTestValue3'); + + const searchParams = openmct.router.getAllSearchParams(); + expect(searchParams.get('testParam2')).toBe('updatedtestValue2'); + expect(searchParams.get('newTestParam3')).toBe('newTestValue3'); + }); + + it('The doPathChange function triggers aborting all requests when doing a path change', () => { + const abortSpy = spyOn(openmct.telemetry, 'abortAllRequests'); + openmct.router.doPathChange('newPath', 'oldPath'); + expect(abortSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/ui/router/Browse.js b/src/ui/router/Browse.js index 1c8f622457..39dc3ecc8c 100644 --- a/src/ui/router/Browse.js +++ b/src/ui/router/Browse.js @@ -1,156 +1,151 @@ -define([ +define([], function () { + return function install(openmct) { + let navigateCall = 0; + let browseObject; + let unobserve = undefined; + let currentObjectPath; + let isRoutingInProgress = false; -], function ( + openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot); + openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => { + isRoutingInProgress = true; + let navigatePath = results[1]; + clearMutationListeners(); -) { + navigateToPath(navigatePath, params.view); + }); - return function install(openmct) { - let navigateCall = 0; - let browseObject; - let unobserve = undefined; - let currentObjectPath; - let isRoutingInProgress = false; + openmct.router.on('change:params', onParamsChanged); - openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot); - openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => { - isRoutingInProgress = true; - let navigatePath = results[1]; - clearMutationListeners(); + function onParamsChanged(newParams, oldParams, changed) { + if (isRoutingInProgress) { + return; + } - navigateToPath(navigatePath, params.view); + if (changed.view && browseObject) { + let provider = openmct.objectViews.getByProviderKey(changed.view); + viewObject(browseObject, provider); + } + } + + function viewObject(object, viewProvider) { + currentObjectPath = openmct.router.path; + + openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath); + openmct.layout.$refs.browseBar.domainObject = object; + + openmct.layout.$refs.browseBar.viewKey = viewProvider.key; + } + + function updateDocumentTitleOnNameMutation(domainObject) { + if (typeof domainObject.name === 'string' && domainObject.name !== document.title) { + document.title = domainObject.name; + } + } + + function navigateToPath(path, currentViewKey) { + navigateCall++; + let currentNavigation = navigateCall; + + if (unobserve) { + unobserve(); + unobserve = undefined; + } + + //Split path into object identifiers + if (!Array.isArray(path)) { + path = path.split('/'); + } + + return pathToObjects(path).then((objects) => { + isRoutingInProgress = false; + + if (currentNavigation !== navigateCall) { + return; // Prevent race. + } + + objects = objects.reverse(); + + openmct.router.path = objects; + openmct.router.emit('afterNavigation'); + browseObject = objects[0]; + + openmct.layout.$refs.browseBar.domainObject = browseObject; + if (!browseObject) { + openmct.layout.$refs.browseObject.clear(); + + return; + } + + let currentProvider = openmct.objectViews.getByProviderKey(currentViewKey); + document.title = browseObject.name; //change document title to current object in main view + // assign listener to global for later clearing + unobserve = openmct.objects.observe(browseObject, '*', updateDocumentTitleOnNameMutation); + + if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) { + viewObject(browseObject, currentProvider); + + return; + } + + let defaultProvider = openmct.objectViews.get(browseObject, openmct.router.path)[0]; + if (defaultProvider) { + openmct.router.updateParams({ + view: defaultProvider.key + }); + } else { + openmct.router.updateParams({ + view: undefined + }); + openmct.layout.$refs.browseObject.clear(); + } + }); + } + + function pathToObjects(path) { + return Promise.all( + path.map((keyString) => { + let identifier = openmct.objects.parseKeyString(keyString); + if (openmct.objects.supportsMutation(identifier)) { + return openmct.objects.getMutable(identifier); + } else { + return openmct.objects.get(identifier); + } + }) + ); + } + + function navigateToFirstChildOfRoot() { + openmct.objects + .get('ROOT') + .then((rootObject) => { + const composition = openmct.composition.get(rootObject); + if (!composition) { + return; + } + + composition + .load() + .then((children) => { + let lastChild = children[children.length - 1]; + if (lastChild) { + let lastChildId = openmct.objects.makeKeyString(lastChild.identifier); + openmct.router.setPath(`#/browse/${lastChildId}`); + } + }) + .catch((e) => console.error(e)); + }) + .catch((e) => console.error(e)); + } + + function clearMutationListeners() { + if (openmct.router.path !== undefined) { + openmct.router.path.forEach((pathObject) => { + if (pathObject.isMutable) { + openmct.objects.destroyMutable(pathObject); + } }); - - openmct.router.on('change:params', onParamsChanged); - - function onParamsChanged(newParams, oldParams, changed) { - if (isRoutingInProgress) { - return; - } - - if (changed.view && browseObject) { - let provider = openmct - .objectViews - .getByProviderKey(changed.view); - viewObject(browseObject, provider); - } - } - - function viewObject(object, viewProvider) { - currentObjectPath = openmct.router.path; - - openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath); - openmct.layout.$refs.browseBar.domainObject = object; - - openmct.layout.$refs.browseBar.viewKey = viewProvider.key; - } - - function updateDocumentTitleOnNameMutation(domainObject) { - if (typeof domainObject.name === 'string' && domainObject.name !== document.title) { - document.title = domainObject.name; - } - } - - function navigateToPath(path, currentViewKey) { - navigateCall++; - let currentNavigation = navigateCall; - - if (unobserve) { - unobserve(); - unobserve = undefined; - } - - //Split path into object identifiers - if (!Array.isArray(path)) { - path = path.split('/'); - } - - return pathToObjects(path).then(objects => { - isRoutingInProgress = false; - - if (currentNavigation !== navigateCall) { - return; // Prevent race. - } - - objects = objects.reverse(); - - openmct.router.path = objects; - openmct.router.emit('afterNavigation'); - browseObject = objects[0]; - - openmct.layout.$refs.browseBar.domainObject = browseObject; - if (!browseObject) { - openmct.layout.$refs.browseObject.clear(); - - return; - } - - let currentProvider = openmct - .objectViews - .getByProviderKey(currentViewKey); - document.title = browseObject.name; //change document title to current object in main view - // assign listener to global for later clearing - unobserve = openmct.objects.observe(browseObject, '*', updateDocumentTitleOnNameMutation); - - if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) { - viewObject(browseObject, currentProvider); - - return; - } - - let defaultProvider = openmct.objectViews.get(browseObject, openmct.router.path)[0]; - if (defaultProvider) { - openmct.router.updateParams({ - view: defaultProvider.key - }); - } else { - openmct.router.updateParams({ - view: undefined - }); - openmct.layout.$refs.browseObject.clear(); - } - }); - } - - function pathToObjects(path) { - return Promise.all(path.map((keyString) => { - let identifier = openmct.objects.parseKeyString(keyString); - if (openmct.objects.supportsMutation(identifier)) { - return openmct.objects.getMutable(identifier); - } else { - return openmct.objects.get(identifier); - } - })); - } - - function navigateToFirstChildOfRoot() { - openmct.objects.get('ROOT') - .then(rootObject => { - const composition = openmct.composition.get(rootObject); - if (!composition) { - return; - } - - composition.load() - .then(children => { - let lastChild = children[children.length - 1]; - if (lastChild) { - let lastChildId = openmct.objects.makeKeyString(lastChild.identifier); - openmct.router.setPath(`#/browse/${lastChildId}`); - } - }) - .catch(e => console.error(e)); - }) - .catch(e => console.error(e)); - } - - function clearMutationListeners() { - if (openmct.router.path !== undefined) { - openmct.router.path.forEach((pathObject) => { - if (pathObject.isMutable) { - openmct.objects.destroyMutable(pathObject); - } - }); - } - } - }; + } + } + }; }); diff --git a/src/ui/toolbar/Toolbar.vue b/src/ui/toolbar/Toolbar.vue index dfe4661020..df220be9d9 100644 --- a/src/ui/toolbar/Toolbar.vue +++ b/src/ui/toolbar/Toolbar.vue @@ -20,28 +20,28 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-button.vue b/src/ui/toolbar/components/toolbar-button.vue index 3136a0820f..e530d97858 100644 --- a/src/ui/toolbar/components/toolbar-button.vue +++ b/src/ui/toolbar/components/toolbar-button.vue @@ -20,74 +20,72 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-checkbox.scss b/src/ui/toolbar/components/toolbar-checkbox.scss index 1614a97076..618c4ffc9c 100644 --- a/src/ui/toolbar/components/toolbar-checkbox.scss +++ b/src/ui/toolbar/components/toolbar-checkbox.scss @@ -1,45 +1,45 @@ .c-custom-checkbox { - $d: 14px; + $d: 14px; + display: flex; + align-items: center; + + label { + @include userSelectNone(); display: flex; align-items: center; + } - label { - @include userSelectNone(); - display: flex; - align-items: center; + &__box { + @include nice-input(); + display: flex; + align-items: center; + justify-content: center; + line-height: $d; + width: $d; + height: $d; + margin-right: $interiorMarginSm; + } + + input { + opacity: 0; + position: absolute; + + &:checked + label > .c-custom-checkbox__box { + background: $colorKey; + &:before { + color: $colorKeyFg; + content: $glyph-icon-check; + font-family: symbolsfont; + font-size: 0.6em; + } } - &__box { - @include nice-input(); - display: flex; - align-items: center; - justify-content: center; - line-height: $d; - width: $d; - height: $d; - margin-right: $interiorMarginSm; + &:not(:disabled) + label { + cursor: pointer; } - input { - opacity: 0; - position: absolute; - - &:checked + label > .c-custom-checkbox__box { - background: $colorKey; - &:before { - color: $colorKeyFg; - content: $glyph-icon-check; - font-family: symbolsfont; - font-size: 0.6em; - } - } - - &:not(:disabled) + label { - cursor: pointer; - } - - &:disabled + label { - opacity: 0.5; - } + &:disabled + label { + opacity: 0.5; } + } } diff --git a/src/ui/toolbar/components/toolbar-checkbox.vue b/src/ui/toolbar/components/toolbar-checkbox.vue index fc66a0d93a..e8723b809f 100644 --- a/src/ui/toolbar/components/toolbar-checkbox.vue +++ b/src/ui/toolbar/components/toolbar-checkbox.vue @@ -20,46 +20,46 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-color-picker.vue b/src/ui/toolbar/components/toolbar-color-picker.vue index 2165dfde51..0435fcf5fb 100644 --- a/src/ui/toolbar/components/toolbar-color-picker.vue +++ b/src/ui/toolbar/components/toolbar-color-picker.vue @@ -20,159 +20,155 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-input.vue b/src/ui/toolbar/components/toolbar-input.vue index e244694c60..36ec100395 100644 --- a/src/ui/toolbar/components/toolbar-input.vue +++ b/src/ui/toolbar/components/toolbar-input.vue @@ -20,64 +20,55 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-menu.vue b/src/ui/toolbar/components/toolbar-menu.vue index b31cbf4ac5..c7d11abf0f 100644 --- a/src/ui/toolbar/components/toolbar-menu.vue +++ b/src/ui/toolbar/components/toolbar-menu.vue @@ -20,57 +20,50 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-select-menu.vue b/src/ui/toolbar/components/toolbar-select-menu.vue index fbbf65efd5..d974e0dc0c 100644 --- a/src/ui/toolbar/components/toolbar-select-menu.vue +++ b/src/ui/toolbar/components/toolbar-select-menu.vue @@ -20,72 +20,64 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-separator.vue b/src/ui/toolbar/components/toolbar-separator.vue index e005a505ed..22f614fb38 100644 --- a/src/ui/toolbar/components/toolbar-separator.vue +++ b/src/ui/toolbar/components/toolbar-separator.vue @@ -20,16 +20,16 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-toggle-button.vue b/src/ui/toolbar/components/toolbar-toggle-button.vue index 91f51fddfa..5c716e9791 100644 --- a/src/ui/toolbar/components/toolbar-toggle-button.vue +++ b/src/ui/toolbar/components/toolbar-toggle-button.vue @@ -20,50 +20,48 @@ at runtime from the About dialog for additional information. --> + + diff --git a/src/utils/agent/Agent.js b/src/utils/agent/Agent.js index ba2d47d43b..1e9c9b84ab 100644 --- a/src/utils/agent/Agent.js +++ b/src/utils/agent/Agent.js @@ -21,115 +21,119 @@ *****************************************************************************/ /** * The query service handles calls for browser and userAgent -* info using a comparison between the userAgent and key -* device names -* @constructor -* @param window the broser object model -* @memberof /utils/agent -*/ + * info using a comparison between the userAgent and key + * device names + * @constructor + * @param window the broser object model + * @memberof /utils/agent + */ export default class Agent { - constructor(window) { - const userAgent = window.navigator.userAgent; - const matches = userAgent.match(/iPad|iPhone|Android/i) || []; + constructor(window) { + const userAgent = window.navigator.userAgent; + const matches = userAgent.match(/iPad|iPhone|Android/i) || []; - this.userAgent = userAgent; - this.mobileName = matches[0]; - this.window = window; - this.touchEnabled = (window.ontouchstart !== undefined); + this.userAgent = userAgent; + this.mobileName = matches[0]; + this.window = window; + this.touchEnabled = window.ontouchstart !== undefined; + } + /** + * Check if the user is on a mobile device. + * @returns {boolean} true on mobile + */ + isMobile() { + return Boolean(this.mobileName); + } + /** + * Check if the user is on a phone-sized mobile device. + * @returns {boolean} true on a phone + */ + isPhone() { + if (this.isMobile()) { + if (this.isAndroidTablet()) { + return false; + } else if (this.mobileName === 'iPad') { + return false; + } else { + return true; + } + } else { + return false; } - /** - * Check if the user is on a mobile device. - * @returns {boolean} true on mobile - */ - isMobile() { - return Boolean(this.mobileName); + } + /** + * Check if the user is on a tablet sized android device + * @returns {boolean} true on an android tablet + */ + isAndroidTablet() { + if (this.mobileName === 'Android') { + if (this.isPortrait() && this.window.innerWidth >= 768) { + return true; + } else if (this.isLandscape() && this.window.innerHeight >= 768) { + return true; + } + } else { + return false; } - /** - * Check if the user is on a phone-sized mobile device. - * @returns {boolean} true on a phone - */ - isPhone() { - if (this.isMobile()) { - if (this.isAndroidTablet()) { - return false; - } else if (this.mobileName === 'iPad') { - return false; - } else { - return true; - } - } else { - return false; - } - } - /** - * Check if the user is on a tablet sized android device - * @returns {boolean} true on an android tablet - */ - isAndroidTablet() { - if (this.mobileName === 'Android') { - if (this.isPortrait() && this.window.innerWidth >= 768) { - return true; - } else if (this.isLandscape() && this.window.innerHeight >= 768) { - return true; - } - } else { - return false; - } - } - /** - * Check if the user is on a tablet-sized mobile device. - * @returns {boolean} true on a tablet - */ - isTablet() { - return (this.isMobile() && !this.isPhone() && this.mobileName !== 'Android') || (this.isMobile() && this.isAndroidTablet()); - } - /** - * Check if the user's device is in a portrait-style - * orientation (display width is narrower than display height.) - * @returns {boolean} true in portrait mode - */ - isPortrait() { - const { screen } = this.window; - const hasScreenOrientation = screen && Object.prototype.hasOwnProperty.call(screen, 'orientation'); - const hasWindowOrientation = Object.prototype.hasOwnProperty.call(this.window, 'orientation'); + } + /** + * Check if the user is on a tablet-sized mobile device. + * @returns {boolean} true on a tablet + */ + isTablet() { + return ( + (this.isMobile() && !this.isPhone() && this.mobileName !== 'Android') || + (this.isMobile() && this.isAndroidTablet()) + ); + } + /** + * Check if the user's device is in a portrait-style + * orientation (display width is narrower than display height.) + * @returns {boolean} true in portrait mode + */ + isPortrait() { + const { screen } = this.window; + const hasScreenOrientation = + screen && Object.prototype.hasOwnProperty.call(screen, 'orientation'); + const hasWindowOrientation = Object.prototype.hasOwnProperty.call(this.window, 'orientation'); - if (hasScreenOrientation) { - return screen.orientation.type.includes('portrait'); - } else if (hasWindowOrientation) { - // Use window.orientation API if available (e.g. Safari mobile) - // which returns [-90, 0, 90, 180] based on device orientation. - const { orientation } = this.window; + if (hasScreenOrientation) { + return screen.orientation.type.includes('portrait'); + } else if (hasWindowOrientation) { + // Use window.orientation API if available (e.g. Safari mobile) + // which returns [-90, 0, 90, 180] based on device orientation. + const { orientation } = this.window; - return Math.abs(orientation / 90) % 2 === 0; - } else { - return this.window.innerWidth < this.window.innerHeight; - } + return Math.abs(orientation / 90) % 2 === 0; + } else { + return this.window.innerWidth < this.window.innerHeight; } - /** - * Check if the user's device is in a landscape-style - * orientation (display width is greater than display height.) - * @returns {boolean} true in landscape mode - */ - isLandscape() { - return !this.isPortrait(); - } - /** - * Check if the user's device supports a touch interface. - * @returns {boolean} true if touch is supported - */ - isTouch() { - return this.touchEnabled; - } - /** - * Check if the user agent matches a certain named device, - * as indicated by checking for a case-insensitive substring - * match. - * @param {string} name the name to check for - * @returns {boolean} true if the user agent includes that name - */ - isBrowser(name) { - name = name.toLowerCase(); + } + /** + * Check if the user's device is in a landscape-style + * orientation (display width is greater than display height.) + * @returns {boolean} true in landscape mode + */ + isLandscape() { + return !this.isPortrait(); + } + /** + * Check if the user's device supports a touch interface. + * @returns {boolean} true if touch is supported + */ + isTouch() { + return this.touchEnabled; + } + /** + * Check if the user agent matches a certain named device, + * as indicated by checking for a case-insensitive substring + * match. + * @param {string} name the name to check for + * @returns {boolean} true if the user agent includes that name + */ + isBrowser(name) { + name = name.toLowerCase(); - return this.userAgent.toLowerCase().indexOf(name) !== -1; - } + return this.userAgent.toLowerCase().indexOf(name) !== -1; + } } diff --git a/src/utils/agent/AgentSpec.js b/src/utils/agent/AgentSpec.js index e362c30cb6..570b441dfe 100644 --- a/src/utils/agent/AgentSpec.js +++ b/src/utils/agent/AgentSpec.js @@ -19,106 +19,105 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import Agent from "./Agent"; +import Agent from './Agent'; const TEST_USER_AGENTS = { - DESKTOP: - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36", - IPAD: - "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53", - IPHONE: - "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53" + DESKTOP: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36', + IPAD: 'Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53', + IPHONE: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53' }; -describe("The Agent", function () { - let testWindow; - let agent; +describe('The Agent', function () { + let testWindow; + let agent; - beforeEach(function () { - testWindow = { - innerWidth: 640, - innerHeight: 480, - navigator: { - userAgent: TEST_USER_AGENTS.DESKTOP - } - }; - }); + beforeEach(function () { + testWindow = { + innerWidth: 640, + innerHeight: 480, + navigator: { + userAgent: TEST_USER_AGENTS.DESKTOP + } + }; + }); - it("recognizes desktop devices as non-mobile", function () { - testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP; - agent = new Agent(testWindow); - expect(agent.isMobile()).toBeFalsy(); - expect(agent.isPhone()).toBeFalsy(); - expect(agent.isTablet()).toBeFalsy(); - }); + it('recognizes desktop devices as non-mobile', function () { + testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP; + agent = new Agent(testWindow); + expect(agent.isMobile()).toBeFalsy(); + expect(agent.isPhone()).toBeFalsy(); + expect(agent.isTablet()).toBeFalsy(); + }); - it("detects iPhones", function () { - testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE; - agent = new Agent(testWindow); - expect(agent.isMobile()).toBeTruthy(); - expect(agent.isPhone()).toBeTruthy(); - expect(agent.isTablet()).toBeFalsy(); - }); + it('detects iPhones', function () { + testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE; + agent = new Agent(testWindow); + expect(agent.isMobile()).toBeTruthy(); + expect(agent.isPhone()).toBeTruthy(); + expect(agent.isTablet()).toBeFalsy(); + }); - it("detects iPads", function () { - testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD; - agent = new Agent(testWindow); - expect(agent.isMobile()).toBeTruthy(); - expect(agent.isPhone()).toBeFalsy(); - expect(agent.isTablet()).toBeTruthy(); - }); + it('detects iPads', function () { + testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD; + agent = new Agent(testWindow); + expect(agent.isMobile()).toBeTruthy(); + expect(agent.isPhone()).toBeFalsy(); + expect(agent.isTablet()).toBeTruthy(); + }); - it("detects display orientation by innerHeight and innerWidth", function () { - agent = new Agent(testWindow); - testWindow.innerWidth = 1024; - testWindow.innerHeight = 400; - expect(agent.isPortrait()).toBeFalsy(); - expect(agent.isLandscape()).toBeTruthy(); - testWindow.innerWidth = 400; - testWindow.innerHeight = 1024; - expect(agent.isPortrait()).toBeTruthy(); - expect(agent.isLandscape()).toBeFalsy(); - }); + it('detects display orientation by innerHeight and innerWidth', function () { + agent = new Agent(testWindow); + testWindow.innerWidth = 1024; + testWindow.innerHeight = 400; + expect(agent.isPortrait()).toBeFalsy(); + expect(agent.isLandscape()).toBeTruthy(); + testWindow.innerWidth = 400; + testWindow.innerHeight = 1024; + expect(agent.isPortrait()).toBeTruthy(); + expect(agent.isLandscape()).toBeFalsy(); + }); - it("detects display orientation by screen.orientation", function () { - agent = new Agent(testWindow); - testWindow.screen = { - orientation: { - type: "landscape-primary" - } - }; - expect(agent.isPortrait()).toBeFalsy(); - expect(agent.isLandscape()).toBeTruthy(); - testWindow.screen = { - orientation: { - type: "portrait-primary" - } - }; - expect(agent.isPortrait()).toBeTruthy(); - expect(agent.isLandscape()).toBeFalsy(); - }); + it('detects display orientation by screen.orientation', function () { + agent = new Agent(testWindow); + testWindow.screen = { + orientation: { + type: 'landscape-primary' + } + }; + expect(agent.isPortrait()).toBeFalsy(); + expect(agent.isLandscape()).toBeTruthy(); + testWindow.screen = { + orientation: { + type: 'portrait-primary' + } + }; + expect(agent.isPortrait()).toBeTruthy(); + expect(agent.isLandscape()).toBeFalsy(); + }); - it("detects display orientation by window.orientation", function () { - agent = new Agent(testWindow); - testWindow.orientation = 90; - expect(agent.isPortrait()).toBeFalsy(); - expect(agent.isLandscape()).toBeTruthy(); - testWindow.orientation = 0; - expect(agent.isPortrait()).toBeTruthy(); - expect(agent.isLandscape()).toBeFalsy(); - }); + it('detects display orientation by window.orientation', function () { + agent = new Agent(testWindow); + testWindow.orientation = 90; + expect(agent.isPortrait()).toBeFalsy(); + expect(agent.isLandscape()).toBeTruthy(); + testWindow.orientation = 0; + expect(agent.isPortrait()).toBeTruthy(); + expect(agent.isLandscape()).toBeFalsy(); + }); - it("detects touch support", function () { - testWindow.ontouchstart = null; - expect(new Agent(testWindow).isTouch()).toBe(true); - delete testWindow.ontouchstart; - expect(new Agent(testWindow).isTouch()).toBe(false); - }); + it('detects touch support', function () { + testWindow.ontouchstart = null; + expect(new Agent(testWindow).isTouch()).toBe(true); + delete testWindow.ontouchstart; + expect(new Agent(testWindow).isTouch()).toBe(false); + }); - it("allows for checking browser type", function () { - testWindow.navigator.userAgent = "Chromezilla Safarifox"; - agent = new Agent(testWindow); - expect(agent.isBrowser("Chrome")).toBe(true); - expect(agent.isBrowser("Firefox")).toBe(false); - }); + it('allows for checking browser type', function () { + testWindow.navigator.userAgent = 'Chromezilla Safarifox'; + agent = new Agent(testWindow); + expect(agent.isBrowser('Chrome')).toBe(true); + expect(agent.isBrowser('Firefox')).toBe(false); + }); }); diff --git a/src/utils/clipboard.js b/src/utils/clipboard.js index b4f7645c88..5a35ca58a6 100644 --- a/src/utils/clipboard.js +++ b/src/utils/clipboard.js @@ -1,13 +1,13 @@ class Clipboard { - updateClipboard(newClip) { - // return promise - return navigator.clipboard.writeText(newClip); - } + updateClipboard(newClip) { + // return promise + return navigator.clipboard.writeText(newClip); + } - readClipboard() { - // return promise - return navigator.clipboard.readText(); - } + readClipboard() { + // return promise + return navigator.clipboard.readText(); + } } export default new Clipboard(); diff --git a/src/utils/clock/DefaultClock.js b/src/utils/clock/DefaultClock.js index 534a0ad4fb..1ee4fb11b6 100644 --- a/src/utils/clock/DefaultClock.js +++ b/src/utils/clock/DefaultClock.js @@ -30,60 +30,59 @@ import EventEmitter from 'EventEmitter'; */ export default class DefaultClock extends EventEmitter { - constructor() { - super(); + constructor() { + super(); - this.key = 'clock'; + this.key = 'clock'; - this.cssClass = 'icon-clock'; - this.name = 'Clock'; - this.description = "A default clock for openmct."; + this.cssClass = 'icon-clock'; + this.name = 'Clock'; + this.description = 'A default clock for openmct.'; + } + + tick(tickValue) { + this.emit('tick', tickValue); + this.lastTick = tickValue; + } + + /** + * Register a listener for the clock. When it ticks, the + * clock will provide the time from the configured endpoint + * + * @param listener + * @returns {function} a function for deregistering the provided listener + */ + on(event) { + let result = super.on.apply(this, arguments); + + if (this.listeners(event).length === 1) { + this.start(); } - tick(tickValue) { - this.emit("tick", tickValue); - this.lastTick = tickValue; + return result; + } + + /** + * Register a listener for the clock. When it ticks, the + * clock will provide the current local system time + * + * @param listener + * @returns {function} a function for deregistering the provided listener + */ + off(event) { + let result = super.off.apply(this, arguments); + + if (this.listeners(event).length === 0) { + this.stop(); } - /** - * Register a listener for the clock. When it ticks, the - * clock will provide the time from the configured endpoint - * - * @param listener - * @returns {function} a function for deregistering the provided listener - */ - on(event) { - let result = super.on.apply(this, arguments); - - if (this.listeners(event).length === 1) { - this.start(); - } - - return result; - } - - /** - * Register a listener for the clock. When it ticks, the - * clock will provide the current local system time - * - * @param listener - * @returns {function} a function for deregistering the provided listener - */ - off(event) { - let result = super.off.apply(this, arguments); - - if (this.listeners(event).length === 0) { - this.stop(); - } - - return result; - } - - /** - * @returns {number} The last value provided for a clock tick - */ - currentValue() { - return this.lastTick; - } + return result; + } + /** + * @returns {number} The last value provided for a clock tick + */ + currentValue() { + return this.lastTick; + } } diff --git a/src/utils/clock/Ticker.js b/src/utils/clock/Ticker.js index 210d576f57..af04b79751 100644 --- a/src/utils/clock/Ticker.js +++ b/src/utils/clock/Ticker.js @@ -21,65 +21,69 @@ *****************************************************************************/ class Ticker { - constructor() { - this.callbacks = []; - this.last = new Date() - 1000; + constructor() { + this.callbacks = []; + this.last = new Date() - 1000; + } + + /** + * Calls functions every second, as close to the actual second + * tick as is feasible. + * @constructor + * @memberof utils/clock + */ + tick() { + const timestamp = new Date(); + const millis = timestamp % 1000; + + // Only update callbacks if a second has actually passed. + if (timestamp >= this.last + 1000) { + this.callbacks.forEach(function (callback) { + callback(timestamp); + }); + this.last = timestamp - millis; } - /** - * Calls functions every second, as close to the actual second - * tick as is feasible. - * @constructor - * @memberof utils/clock - */ - tick() { - const timestamp = new Date(); - const millis = timestamp % 1000; + // Try to update at exactly the next second + this.timeoutHandle = setTimeout( + () => { + this.tick(); + }, + 1000 - millis, + true + ); + } - // Only update callbacks if a second has actually passed. - if (timestamp >= this.last + 1000) { - this.callbacks.forEach(function (callback) { - callback(timestamp); - }); - this.last = timestamp - millis; - } - - // Try to update at exactly the next second - this.timeoutHandle = setTimeout(() => { - this.tick(); - }, 1000 - millis, true); + /** + * Listen for clock ticks. The provided callback will + * be invoked with the current timestamp (in milliseconds + * since Jan 1 1970) at regular intervals, as near to the + * second boundary as possible. + * + * @param {Function} callback callback to invoke + * @returns {Function} a function to unregister this listener + */ + listen(callback) { + if (this.callbacks.length === 0) { + this.tick(); } - /** - * Listen for clock ticks. The provided callback will - * be invoked with the current timestamp (in milliseconds - * since Jan 1 1970) at regular intervals, as near to the - * second boundary as possible. - * - * @param {Function} callback callback to invoke - * @returns {Function} a function to unregister this listener - */ - listen(callback) { - if (this.callbacks.length === 0) { - this.tick(); - } + this.callbacks.push(callback); - this.callbacks.push(callback); + // Provide immediate feedback + callback(this.last); - // Provide immediate feedback - callback(this.last); + // Provide a deregistration function + return () => { + this.callbacks = this.callbacks.filter(function (cb) { + return cb !== callback; + }); - // Provide a deregistration function - return () => { - this.callbacks = this.callbacks.filter(function (cb) { - return cb !== callback; - }); - - if (this.callbacks.length === 0) { - clearTimeout(this.timeoutHandle); - } - }; - } + if (this.callbacks.length === 0) { + clearTimeout(this.timeoutHandle); + } + }; + } } let ticker = new Ticker(); diff --git a/src/utils/duration.js b/src/utils/duration.js index 087161aa23..d7e8fa62f1 100644 --- a/src/utils/duration.js +++ b/src/utils/duration.js @@ -26,50 +26,51 @@ const ONE_HOUR = ONE_MINUTE * 60; const ONE_DAY = ONE_HOUR * 24; function normalizeAge(num) { - const hundredtized = num * 100; - const isWhole = hundredtized % 100 === 0; + const hundredtized = num * 100; + const isWhole = hundredtized % 100 === 0; - return isWhole ? hundredtized / 100 : num; + return isWhole ? hundredtized / 100 : num; } function padLeadingZeros(num, numOfLeadingZeros) { - return num.toString().padStart(numOfLeadingZeros, '0'); + return num.toString().padStart(numOfLeadingZeros, '0'); } function toDoubleDigits(num) { - return padLeadingZeros(num, 2); + return padLeadingZeros(num, 2); } function toTripleDigits(num) { - return padLeadingZeros(num, 3); + return padLeadingZeros(num, 3); } function addTimeSuffix(value, suffix) { - return typeof value === 'number' && value > 0 ? `${value + suffix}` : ''; + return typeof value === 'number' && value > 0 ? `${value + suffix}` : ''; } export function millisecondsToDHMS(numericDuration) { - const ms = numericDuration || 0; - const dhms = [ - addTimeSuffix(Math.floor(normalizeAge(ms / ONE_DAY)), 'd'), - addTimeSuffix(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR)), 'h'), - addTimeSuffix(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE)), 'm'), - addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's'), - addTimeSuffix(Math.floor(normalizeAge(ms % ONE_SECOND)), "ms") - ].filter(Boolean).join(' '); + const ms = numericDuration || 0; + const dhms = [ + addTimeSuffix(Math.floor(normalizeAge(ms / ONE_DAY)), 'd'), + addTimeSuffix(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR)), 'h'), + addTimeSuffix(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE)), 'm'), + addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's'), + addTimeSuffix(Math.floor(normalizeAge(ms % ONE_SECOND)), 'ms') + ] + .filter(Boolean) + .join(' '); - return `${ dhms ? '+' : ''} ${dhms}`; + return `${dhms ? '+' : ''} ${dhms}`; } export function getPreciseDuration(value) { - const ms = value || 0; - - return [ - toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))), - toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))), - toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))), - toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))), - toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))) - ].join(":"); + const ms = value || 0; + return [ + toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))), + toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))), + toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))), + toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))), + toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))) + ].join(':'); } diff --git a/src/utils/raf.js b/src/utils/raf.js index d5c0c48fe5..1f4ee555e4 100644 --- a/src/utils/raf.js +++ b/src/utils/raf.js @@ -1,14 +1,14 @@ export default function raf(callback) { - let rendering = false; + let rendering = false; - return () => { - if (!rendering) { - rendering = true; + return () => { + if (!rendering) { + rendering = true; - requestAnimationFrame(() => { - callback(); - rendering = false; - }); - } - }; + requestAnimationFrame(() => { + callback(); + rendering = false; + }); + } + }; } diff --git a/src/utils/rafSpec.js b/src/utils/rafSpec.js index 0bf5ae9d9c..f03912b355 100644 --- a/src/utils/rafSpec.js +++ b/src/utils/rafSpec.js @@ -1,61 +1,65 @@ -import raf from "./raf"; +import raf from './raf'; describe('The raf utility function', () => { - it('Throttles function calls that arrive in quick succession using Request Animation Frame', () => { - const unthrottledFunction = jasmine.createSpy('unthrottledFunction'); - const throttledCallback = jasmine.createSpy('throttledCallback'); - const throttledFunction = raf(throttledCallback); + it('Throttles function calls that arrive in quick succession using Request Animation Frame', () => { + const unthrottledFunction = jasmine.createSpy('unthrottledFunction'); + const throttledCallback = jasmine.createSpy('throttledCallback'); + const throttledFunction = raf(throttledCallback); + for (let i = 0; i < 10; i++) { + unthrottledFunction(); + throttledFunction(); + } + + return new Promise((resolve) => { + requestAnimationFrame(resolve); + }).then(() => { + expect(unthrottledFunction).toHaveBeenCalledTimes(10); + expect(throttledCallback).toHaveBeenCalledTimes(1); + }); + }); + it('Only invokes callback once per animation frame', () => { + const throttledCallback = jasmine.createSpy('throttledCallback'); + const throttledFunction = raf(throttledCallback); + + for (let i = 0; i < 10; i++) { + throttledFunction(); + } + + return new Promise((resolve) => { + requestAnimationFrame(resolve); + }) + .then(() => { + return new Promise((resolve) => { + requestAnimationFrame(resolve); + }); + }) + .then(() => { + expect(throttledCallback).toHaveBeenCalledTimes(1); + }); + }); + it('Invokes callback again if called in subsequent animation frame', () => { + const throttledCallback = jasmine.createSpy('throttledCallback'); + const throttledFunction = raf(throttledCallback); + + for (let i = 0; i < 10; i++) { + throttledFunction(); + } + + return new Promise((resolve) => { + requestAnimationFrame(resolve); + }) + .then(() => { for (let i = 0; i < 10; i++) { - unthrottledFunction(); - throttledFunction(); + throttledFunction(); } return new Promise((resolve) => { - requestAnimationFrame(resolve); - }).then(() => { - expect(unthrottledFunction).toHaveBeenCalledTimes(10); - expect(throttledCallback).toHaveBeenCalledTimes(1); + requestAnimationFrame(resolve); }); - }); - it('Only invokes callback once per animation frame', () => { - const throttledCallback = jasmine.createSpy('throttledCallback'); - const throttledFunction = raf(throttledCallback); - - for (let i = 0; i < 10; i++) { - throttledFunction(); - } - - return new Promise(resolve => { - requestAnimationFrame(resolve); - }).then(() => { - return new Promise(resolve => { - requestAnimationFrame(resolve); - }); - }).then(() => { - expect(throttledCallback).toHaveBeenCalledTimes(1); - }); - }); - it('Invokes callback again if called in subsequent animation frame', () => { - const throttledCallback = jasmine.createSpy('throttledCallback'); - const throttledFunction = raf(throttledCallback); - - for (let i = 0; i < 10; i++) { - throttledFunction(); - } - - return new Promise(resolve => { - requestAnimationFrame(resolve); - }).then(() => { - for (let i = 0; i < 10; i++) { - throttledFunction(); - } - - return new Promise(resolve => { - requestAnimationFrame(resolve); - }); - }).then(() => { - expect(throttledCallback).toHaveBeenCalledTimes(2); - }); - }); + }) + .then(() => { + expect(throttledCallback).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/src/utils/staleness.js b/src/utils/staleness.js index 6c5e57632d..9603aba7f9 100644 --- a/src/utils/staleness.js +++ b/src/utils/staleness.js @@ -21,56 +21,56 @@ *****************************************************************************/ export default class StalenessUtils { - constructor(openmct, domainObject) { - this.openmct = openmct; - this.domainObject = domainObject; - this.metadata = this.openmct.telemetry.getMetadata(domainObject); - this.lastStalenessResponseTime = 0; + constructor(openmct, domainObject) { + this.openmct = openmct; + this.domainObject = domainObject; + this.metadata = this.openmct.telemetry.getMetadata(domainObject); + this.lastStalenessResponseTime = 0; - this.setTimeSystem(this.openmct.time.timeSystem()); - this.watchTimeSystem(); + this.setTimeSystem(this.openmct.time.timeSystem()); + this.watchTimeSystem(); + } + + shouldUpdateStaleness(stalenessResponse, id) { + const stalenessResponseTime = this.parseTime(stalenessResponse); + + if (stalenessResponseTime > this.lastStalenessResponseTime) { + this.lastStalenessResponseTime = stalenessResponseTime; + + return true; + } else { + return false; + } + } + + watchTimeSystem() { + this.openmct.time.on('timeSystem', this.setTimeSystem, this); + } + + unwatchTimeSystem() { + this.openmct.time.off('timeSystem', this.setTimeSystem, this); + } + + setTimeSystem(timeSystem) { + let metadataValue = { format: timeSystem.key }; + + if (this.metadata) { + metadataValue = this.metadata.value(timeSystem.key) ?? metadataValue; } - shouldUpdateStaleness(stalenessResponse, id) { - const stalenessResponseTime = this.parseTime(stalenessResponse); + const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); - if (stalenessResponseTime > this.lastStalenessResponseTime) { - this.lastStalenessResponseTime = stalenessResponseTime; + this.parseTime = (stalenessResponse) => { + const stalenessDatum = { + ...stalenessResponse, + source: stalenessResponse[timeSystem.key] + }; - return true; - } else { - return false; - } - } + return valueFormatter.parse(stalenessDatum); + }; + } - watchTimeSystem() { - this.openmct.time.on('timeSystem', this.setTimeSystem, this); - } - - unwatchTimeSystem() { - this.openmct.time.off('timeSystem', this.setTimeSystem, this); - } - - setTimeSystem(timeSystem) { - let metadataValue = { format: timeSystem.key }; - - if (this.metadata) { - metadataValue = this.metadata.value(timeSystem.key) ?? metadataValue; - } - - const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); - - this.parseTime = (stalenessResponse) => { - const stalenessDatum = { - ...stalenessResponse, - source: stalenessResponse[timeSystem.key] - }; - - return valueFormatter.parse(stalenessDatum); - }; - } - - destroy() { - this.unwatchTimeSystem(); - } + destroy() { + this.unwatchTimeSystem(); + } } diff --git a/src/utils/template/templateHelpers.js b/src/utils/template/templateHelpers.js index 70d381ce7d..884b0c6db8 100644 --- a/src/utils/template/templateHelpers.js +++ b/src/utils/template/templateHelpers.js @@ -1,14 +1,14 @@ export function convertTemplateToHTML(templateString) { - const template = document.createElement('template'); - template.innerHTML = templateString; + const template = document.createElement('template'); + template.innerHTML = templateString; - return template.content.cloneNode(true).children; + return template.content.cloneNode(true).children; } export function toggleClass(element, className) { - if (element.classList.contains(className)) { - element.classList.remove(className); - } else { - element.classList.add(className); - } + if (element.classList.contains(className)) { + element.classList.remove(className); + } else { + element.classList.add(className); + } } diff --git a/src/utils/template/templateHelpersSpec.js b/src/utils/template/templateHelpersSpec.js index 974041f461..fec50c5afb 100644 --- a/src/utils/template/templateHelpersSpec.js +++ b/src/utils/template/templateHelpersSpec.js @@ -19,7 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import { toggleClass } from "@/utils/template/templateHelpers"; +import { toggleClass } from '@/utils/template/templateHelpers'; const CLASS_AS_NON_EMPTY_STRING = 'class-to-toggle'; const CLASS_AS_EMPTY_STRING = ''; @@ -30,77 +30,86 @@ const CLASS_TERTIARY = 'yet-another-class-to-toggle'; const CLASS_TO_TOGGLE = CLASS_DEFAULT; describe('toggleClass', () => { - describe('type checking', () => { - const A_DOM_NODE = document.createElement('div'); - const NOT_A_DOM_NODE = 'not-a-dom-node'; - describe('errors', () => { - it('throws when "className" is an empty string', () => { - expect(() => toggleClass(A_DOM_NODE, CLASS_AS_EMPTY_STRING)).toThrow(); - }); - it('throws when "element" is not a DOM node', () => { - expect(() => toggleClass(NOT_A_DOM_NODE, CLASS_DEFAULT)).toThrow(); - }); - }); - describe('success', () => { - it('does not throw when "className" is not an empty string', () => { - expect(() => toggleClass(A_DOM_NODE, CLASS_AS_NON_EMPTY_STRING)).not.toThrow(); - }); - it('does not throw when "element" is a DOM node', () => { - expect(() => toggleClass(A_DOM_NODE, CLASS_DEFAULT)).not.toThrow(); - }); - }); + describe('type checking', () => { + const A_DOM_NODE = document.createElement('div'); + const NOT_A_DOM_NODE = 'not-a-dom-node'; + describe('errors', () => { + it('throws when "className" is an empty string', () => { + expect(() => toggleClass(A_DOM_NODE, CLASS_AS_EMPTY_STRING)).toThrow(); + }); + it('throws when "element" is not a DOM node', () => { + expect(() => toggleClass(NOT_A_DOM_NODE, CLASS_DEFAULT)).toThrow(); + }); }); - describe('adding a class', () => { - it('adds specified class to an element without any classes', () => { - // test case - const ELEMENT_WITHOUT_CLASS = document.createElement('div'); - toggleClass(ELEMENT_WITHOUT_CLASS, CLASS_TO_TOGGLE); - // expected - const ELEMENT_WITHOUT_CLASS_EXPECTED = document.createElement('div'); - ELEMENT_WITHOUT_CLASS_EXPECTED.classList.add(CLASS_TO_TOGGLE); - expect(ELEMENT_WITHOUT_CLASS).toEqual(ELEMENT_WITHOUT_CLASS_EXPECTED); - }); - it('adds specified class to an element that already has another class', () => { - // test case - const ELEMENT_WITH_SINGLE_CLASS = document.createElement('div'); - ELEMENT_WITH_SINGLE_CLASS.classList.add(CLASS_SECONDARY); - toggleClass(ELEMENT_WITH_SINGLE_CLASS, CLASS_TO_TOGGLE); - // expected - const ELEMENT_WITH_SINGLE_CLASS_EXPECTED = document.createElement('div'); - ELEMENT_WITH_SINGLE_CLASS_EXPECTED.classList.add(CLASS_SECONDARY, CLASS_TO_TOGGLE); - expect(ELEMENT_WITH_SINGLE_CLASS).toEqual(ELEMENT_WITH_SINGLE_CLASS_EXPECTED); - }); - it('adds specified class to an element that already has more than one other classes', () => { - // test case - const ELEMENT_WITH_MULTIPLE_CLASSES = document.createElement('div'); - ELEMENT_WITH_MULTIPLE_CLASSES.classList.add(CLASS_TO_TOGGLE, CLASS_SECONDARY); - toggleClass(ELEMENT_WITH_MULTIPLE_CLASSES, CLASS_TO_TOGGLE); - // expected - const ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED = document.createElement('div'); - ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED.classList.add(CLASS_SECONDARY); - expect(ELEMENT_WITH_MULTIPLE_CLASSES).toEqual(ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED); - }); + describe('success', () => { + it('does not throw when "className" is not an empty string', () => { + expect(() => toggleClass(A_DOM_NODE, CLASS_AS_NON_EMPTY_STRING)).not.toThrow(); + }); + it('does not throw when "element" is a DOM node', () => { + expect(() => toggleClass(A_DOM_NODE, CLASS_DEFAULT)).not.toThrow(); + }); }); - describe('removing a class', () => { - it('removes specified class from an element that only has the specified class', () => { - // test case - const ELEMENT_WITH_ONLY_SPECIFIED_CLASS = document.createElement('div'); - ELEMENT_WITH_ONLY_SPECIFIED_CLASS.classList.add(CLASS_TO_TOGGLE); - toggleClass(ELEMENT_WITH_ONLY_SPECIFIED_CLASS, CLASS_TO_TOGGLE); - // expected - const ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED = document.createElement('div'); - ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED.className = ''; - expect(ELEMENT_WITH_ONLY_SPECIFIED_CLASS).toEqual(ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED); - }); - it('removes specified class from an element that has specified class, and others', () => { - // test case - const ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS = document.createElement('div'); - ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS.classList.add(CLASS_TO_TOGGLE, CLASS_SECONDARY, CLASS_TERTIARY); - toggleClass(ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS, CLASS_TO_TOGGLE); - // expected - const ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED = document.createElement('div'); - ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED.classList.add(CLASS_SECONDARY, CLASS_TERTIARY); - expect(ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS).toEqual(ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED); - }); + }); + describe('adding a class', () => { + it('adds specified class to an element without any classes', () => { + // test case + const ELEMENT_WITHOUT_CLASS = document.createElement('div'); + toggleClass(ELEMENT_WITHOUT_CLASS, CLASS_TO_TOGGLE); + // expected + const ELEMENT_WITHOUT_CLASS_EXPECTED = document.createElement('div'); + ELEMENT_WITHOUT_CLASS_EXPECTED.classList.add(CLASS_TO_TOGGLE); + expect(ELEMENT_WITHOUT_CLASS).toEqual(ELEMENT_WITHOUT_CLASS_EXPECTED); }); + it('adds specified class to an element that already has another class', () => { + // test case + const ELEMENT_WITH_SINGLE_CLASS = document.createElement('div'); + ELEMENT_WITH_SINGLE_CLASS.classList.add(CLASS_SECONDARY); + toggleClass(ELEMENT_WITH_SINGLE_CLASS, CLASS_TO_TOGGLE); + // expected + const ELEMENT_WITH_SINGLE_CLASS_EXPECTED = document.createElement('div'); + ELEMENT_WITH_SINGLE_CLASS_EXPECTED.classList.add(CLASS_SECONDARY, CLASS_TO_TOGGLE); + expect(ELEMENT_WITH_SINGLE_CLASS).toEqual(ELEMENT_WITH_SINGLE_CLASS_EXPECTED); + }); + it('adds specified class to an element that already has more than one other classes', () => { + // test case + const ELEMENT_WITH_MULTIPLE_CLASSES = document.createElement('div'); + ELEMENT_WITH_MULTIPLE_CLASSES.classList.add(CLASS_TO_TOGGLE, CLASS_SECONDARY); + toggleClass(ELEMENT_WITH_MULTIPLE_CLASSES, CLASS_TO_TOGGLE); + // expected + const ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED = document.createElement('div'); + ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED.classList.add(CLASS_SECONDARY); + expect(ELEMENT_WITH_MULTIPLE_CLASSES).toEqual(ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED); + }); + }); + describe('removing a class', () => { + it('removes specified class from an element that only has the specified class', () => { + // test case + const ELEMENT_WITH_ONLY_SPECIFIED_CLASS = document.createElement('div'); + ELEMENT_WITH_ONLY_SPECIFIED_CLASS.classList.add(CLASS_TO_TOGGLE); + toggleClass(ELEMENT_WITH_ONLY_SPECIFIED_CLASS, CLASS_TO_TOGGLE); + // expected + const ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED = document.createElement('div'); + ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED.className = ''; + expect(ELEMENT_WITH_ONLY_SPECIFIED_CLASS).toEqual(ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED); + }); + it('removes specified class from an element that has specified class, and others', () => { + // test case + const ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS = document.createElement('div'); + ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS.classList.add( + CLASS_TO_TOGGLE, + CLASS_SECONDARY, + CLASS_TERTIARY + ); + toggleClass(ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS, CLASS_TO_TOGGLE); + // expected + const ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED = document.createElement('div'); + ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED.classList.add( + CLASS_SECONDARY, + CLASS_TERTIARY + ); + expect(ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS).toEqual( + ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED + ); + }); + }); }); diff --git a/src/utils/testing.js b/src/utils/testing.js index 27bf7bd7c4..7c80c87233 100644 --- a/src/utils/testing.js +++ b/src/utils/testing.js @@ -26,122 +26,121 @@ let nativeFunctions = []; let mockObjects = setMockObjects(); const DEFAULT_TIME_OPTIONS = { - timeSystemKey: 'utc', - bounds: { - start: 0, - end: 1 - } + timeSystemKey: 'utc', + bounds: { + start: 0, + end: 1 + } }; export function createOpenMct(timeSystemOptions = DEFAULT_TIME_OPTIONS) { - const openmct = new MCT(); - openmct.install(openmct.plugins.LocalStorage()); - openmct.install(openmct.plugins.UTCTimeSystem()); - openmct.setAssetPath('/base'); + const openmct = new MCT(); + openmct.install(openmct.plugins.LocalStorage()); + openmct.install(openmct.plugins.UTCTimeSystem()); + openmct.setAssetPath('/base'); - const timeSystemKey = timeSystemOptions.timeSystemKey; - const start = timeSystemOptions.bounds.start; - const end = timeSystemOptions.bounds.end; + const timeSystemKey = timeSystemOptions.timeSystemKey; + const start = timeSystemOptions.bounds.start; + const end = timeSystemOptions.bounds.end; - openmct.time.timeSystem(timeSystemKey, { - start, - end - }); + openmct.time.timeSystem(timeSystemKey, { + start, + end + }); - return openmct; + return openmct; } export function createMouseEvent(eventName) { - return new MouseEvent(eventName, { - bubbles: true, - cancelable: true, - view: window - }); + return new MouseEvent(eventName, { + bubbles: true, + cancelable: true, + view: window + }); } export function spyOnBuiltins(functionNames, object = window) { - functionNames.forEach(functionName => { - if (nativeFunctions[functionName]) { - throw `Builtin spy function already defined for ${functionName}`; - } + functionNames.forEach((functionName) => { + if (nativeFunctions[functionName]) { + throw `Builtin spy function already defined for ${functionName}`; + } - nativeFunctions.push({ - functionName, - object, - nativeFunction: object[functionName] - }); - spyOn(object, functionName); + nativeFunctions.push({ + functionName, + object, + nativeFunction: object[functionName] }); + spyOn(object, functionName); + }); } export function clearBuiltinSpies() { - nativeFunctions.forEach(clearBuiltinSpy); - nativeFunctions = []; + nativeFunctions.forEach(clearBuiltinSpy); + nativeFunctions = []; } export function resetApplicationState(openmct) { - let promise; + let promise; - clearBuiltinSpies(); + clearBuiltinSpies(); - if (openmct !== undefined) { - openmct.destroy(); - } + if (openmct !== undefined) { + openmct.destroy(); + } - if (window.location.hash !== '#' && window.location.hash !== '') { - promise = new Promise((resolve, reject) => { - window.addEventListener('hashchange', cleanup); - window.location.hash = '#'; + if (window.location.hash !== '#' && window.location.hash !== '') { + promise = new Promise((resolve, reject) => { + window.addEventListener('hashchange', cleanup); + window.location.hash = '#'; - function cleanup() { - window.removeEventListener('hashchange', cleanup); - resolve(); - } - }); - } else { - promise = Promise.resolve(); - } + function cleanup() { + window.removeEventListener('hashchange', cleanup); + resolve(); + } + }); + } else { + promise = Promise.resolve(); + } - return promise; + return promise; } // required: key // optional: element, keyCode, type export function simulateKeyEvent(opts) { + if (!opts.key) { + console.warn('simulateKeyEvent needs a key'); - if (!opts.key) { - console.warn('simulateKeyEvent needs a key'); + return; + } - return; - } + const el = opts.element || document; + const key = opts.key; + const keyCode = opts.keyCode || key; + const type = opts.type || 'keydown'; + const event = new Event(type); - const el = opts.element || document; - const key = opts.key; - const keyCode = opts.keyCode || key; - const type = opts.type || 'keydown'; - const event = new Event(type); + event.keyCode = keyCode; + event.key = key; - event.keyCode = keyCode; - event.key = key; - - el.dispatchEvent(event); + el.dispatchEvent(event); } function clearBuiltinSpy(funcDefinition) { - funcDefinition.object[funcDefinition.functionName] = funcDefinition.nativeFunction; + funcDefinition.object[funcDefinition.functionName] = funcDefinition.nativeFunction; } export function getLatestTelemetry(telemetry = [], opts = {}) { - let latest = []; - let timeFormat = opts.timeFormat || 'utc'; + let latest = []; + let timeFormat = opts.timeFormat || 'utc'; - if (telemetry.length) { - latest = telemetry.reduce((prev, cur) => { - return prev[timeFormat] > cur[timeFormat] ? prev : cur; - }); - } + if (telemetry.length) { + latest = telemetry.reduce((prev, cur) => { + return prev[timeFormat] > cur[timeFormat] ? prev : cur; + }); + } - return latest; + return latest; } // EXAMPLE: @@ -161,81 +160,80 @@ export function getLatestTelemetry(telemetry = [], opts = {}) { // } // }) export function getMockObjects(opts = {}) { - opts.type = opts.type || 'default'; - if (opts.objectKeyStrings && !Array.isArray(opts.objectKeyStrings)) { - throw `"getMockObjects" optional parameter "objectKeyStrings" must be an array of string object keys`; - } + opts.type = opts.type || 'default'; + if (opts.objectKeyStrings && !Array.isArray(opts.objectKeyStrings)) { + throw `"getMockObjects" optional parameter "objectKeyStrings" must be an array of string object keys`; + } - let requestedMocks = {}; + let requestedMocks = {}; - if (!opts.objectKeyStrings) { - requestedMocks = copyObj(mockObjects[opts.type]); + if (!opts.objectKeyStrings) { + requestedMocks = copyObj(mockObjects[opts.type]); + } else { + opts.objectKeyStrings.forEach((objKey) => { + if (mockObjects[opts.type] && mockObjects[opts.type][objKey]) { + requestedMocks[objKey] = copyObj(mockObjects[opts.type][objKey]); + } else { + throw `No mock object for object key "${objKey}" of type "${opts.type}"`; + } + }); + } + + // build out custom telemetry mappings if necessary + if (requestedMocks.telemetry && opts.telemetryConfig) { + let keys = opts.telemetryConfig.keys; + let format = opts.telemetryConfig.format || 'utc'; + let hints = opts.telemetryConfig.hints; + let values; + + // if utc, keep default + if (format === 'utc') { + // save for later if new keys + if (keys) { + format = requestedMocks.telemetry.telemetry.values.find((vals) => vals.key === 'utc'); + } } else { - opts.objectKeyStrings.forEach(objKey => { - if (mockObjects[opts.type] && mockObjects[opts.type][objKey]) { - requestedMocks[objKey] = copyObj(mockObjects[opts.type][objKey]); - } else { - throw `No mock object for object key "${objKey}" of type "${opts.type}"`; - } - }); + format = { + key: format, + name: 'Time', + format: format === 'local' ? 'local-format' : format, + hints: { + domain: 1 + } + }; } - // build out custom telemetry mappings if necessary - if (requestedMocks.telemetry && opts.telemetryConfig) { - let keys = opts.telemetryConfig.keys; - let format = opts.telemetryConfig.format || 'utc'; - let hints = opts.telemetryConfig.hints; - let values; - - // if utc, keep default - if (format === 'utc') { - // save for later if new keys - if (keys) { - format = requestedMocks.telemetry - .telemetry.values.find((vals) => vals.key === 'utc'); - } - } else { - format = { - key: format, - name: "Time", - format: format === 'local' ? 'local-format' : format, - hints: { - domain: 1 - } - }; - } - - if (keys) { - values = keys.map((key) => ({ - key, - name: key + ' attribute' - })); - values.push(format); // add time format back in - } else { - values = requestedMocks.telemetry.telemetry.values; - } - - if (hints) { - for (let val of values) { - if (hints[val.key]) { - val.hints = hints[val.key]; - } - } - } - - requestedMocks.telemetry.telemetry.values = values; + if (keys) { + values = keys.map((key) => ({ + key, + name: key + ' attribute' + })); + values.push(format); // add time format back in + } else { + values = requestedMocks.telemetry.telemetry.values; } - // overwrite any field keys - if (opts.overwrite) { - for (let mock in requestedMocks) { - if (opts.overwrite[mock]) { - requestedMocks[mock] = Object.assign(requestedMocks[mock], opts.overwrite[mock]); - } + if (hints) { + for (let val of values) { + if (hints[val.key]) { + val.hints = hints[val.key]; } + } } - return requestedMocks; + requestedMocks.telemetry.telemetry.values = values; + } + + // overwrite any field keys + if (opts.overwrite) { + for (let mock in requestedMocks) { + if (opts.overwrite[mock]) { + requestedMocks[mock] = Object.assign(requestedMocks[mock], opts.overwrite[mock]); + } + } + } + + return requestedMocks; } // EXAMPLE: @@ -246,107 +244,112 @@ export function getMockObjects(opts = {}) { // format: 'local' // }) export function getMockTelemetry(opts = {}) { - let count = opts.count || 2; - let format = opts.format || 'utc'; - let name = opts.name || 'Mock Telemetry Datum'; - let keyCount = 2; - let keys = false; - let telemetry = []; + let count = opts.count || 2; + let format = opts.format || 'utc'; + let name = opts.name || 'Mock Telemetry Datum'; + let keyCount = 2; + let keys = false; + let telemetry = []; - if (opts.keys && Array.isArray(opts.keys)) { - keyCount = opts.keys.length; - keys = opts.keys; - } else if (opts.keyCount) { - keyCount = opts.keyCount; + if (opts.keys && Array.isArray(opts.keys)) { + keyCount = opts.keys.length; + keys = opts.keys; + } else if (opts.keyCount) { + keyCount = opts.keyCount; + } + + for (let i = 1; i < count + 1; i++) { + let datum = { + [format]: i, + name + }; + + for (let k = 1; k < keyCount + 1; k++) { + let key = keys ? keys[k - 1] : 'some-key-' + k; + let value = keys ? keys[k - 1] + ' value ' + i : 'some value ' + i + '-' + k; + datum[key] = value; } - for (let i = 1; i < count + 1; i++) { - let datum = { - [format]: i, - name - }; + telemetry.push(datum); + } - for (let k = 1; k < keyCount + 1; k++) { - let key = keys ? keys[k - 1] : 'some-key-' + k; - let value = keys ? keys[k - 1] + ' value ' + i : 'some value ' + i + '-' + k; - datum[key] = value; - } - - telemetry.push(datum); - } - - return telemetry; + return telemetry; } // copy objects a bit more easily function copyObj(obj) { - return JSON.parse(JSON.stringify(obj)); + return JSON.parse(JSON.stringify(obj)); } // add any other necessary types to this mockObjects object function setMockObjects() { - return { - default: { - folder: { - identifier: { - namespace: "", - key: "folder-object" - }, - name: "Test Folder Object", - type: "folder", - composition: [], - location: "mine" - }, - ladTable: { - identifier: { - namespace: "", - key: "lad-object" - }, - type: 'LadTable', - composition: [] - }, - ladTableSet: { - identifier: { - namespace: "", - key: "lad-set-object" - }, - type: 'LadTableSet', - composition: [] - }, - telemetry: { - identifier: { - namespace: "", - key: "telemetry-object" - }, - type: "test-telemetry-object", - name: "Test Telemetry Object", - telemetry: { - values: [{ - key: "name", - name: "Name", - format: "string" - }, { - key: "utc", - name: "Time", - format: "utc", - hints: { - domain: 1 - } - }, { - name: "Some attribute 1", - key: "some-key-1", - hints: { - range: 1 - } - }, { - name: "Some attribute 2", - key: "some-key-2" - }] - } - } + return { + default: { + folder: { + identifier: { + namespace: '', + key: 'folder-object' }, - otherType: { - example: {} + name: 'Test Folder Object', + type: 'folder', + composition: [], + location: 'mine' + }, + ladTable: { + identifier: { + namespace: '', + key: 'lad-object' + }, + type: 'LadTable', + composition: [] + }, + ladTableSet: { + identifier: { + namespace: '', + key: 'lad-set-object' + }, + type: 'LadTableSet', + composition: [] + }, + telemetry: { + identifier: { + namespace: '', + key: 'telemetry-object' + }, + type: 'test-telemetry-object', + name: 'Test Telemetry Object', + telemetry: { + values: [ + { + key: 'name', + name: 'Name', + format: 'string' + }, + { + key: 'utc', + name: 'Time', + format: 'utc', + hints: { + domain: 1 + } + }, + { + name: 'Some attribute 1', + key: 'some-key-1', + hints: { + range: 1 + } + }, + { + name: 'Some attribute 2', + key: 'some-key-2' + } + ] } - }; + } + }, + otherType: { + example: {} + } + }; } diff --git a/src/utils/testing/mockLocalStorage.js b/src/utils/testing/mockLocalStorage.js index baf993e70d..9200eb6e24 100644 --- a/src/utils/testing/mockLocalStorage.js +++ b/src/utils/testing/mockLocalStorage.js @@ -1,33 +1,33 @@ export function mockLocalStorage() { - let store; + let store; - beforeEach(() => { - spyOn(Storage.prototype, 'getItem').and.callFake(getItem); - spyOn(Storage.prototype, 'setItem').and.callFake(setItem); - spyOn(Storage.prototype, 'removeItem').and.callFake(removeItem); - spyOn(Storage.prototype, 'clear').and.callFake(clear); + beforeEach(() => { + spyOn(Storage.prototype, 'getItem').and.callFake(getItem); + spyOn(Storage.prototype, 'setItem').and.callFake(setItem); + spyOn(Storage.prototype, 'removeItem').and.callFake(removeItem); + spyOn(Storage.prototype, 'clear').and.callFake(clear); - store = {}; + store = {}; - function getItem(key) { - return store[key]; - } + function getItem(key) { + return store[key]; + } - function setItem(key, value) { - store[key] = typeof value === 'string' ? value : JSON.stringify(value); - } + function setItem(key, value) { + store[key] = typeof value === 'string' ? value : JSON.stringify(value); + } - function removeItem(key) { - store[key] = undefined; - delete store[key]; - } + function removeItem(key) { + store[key] = undefined; + delete store[key]; + } - function clear() { - store = {}; - } - }); + function clear() { + store = {}; + } + }); - afterEach(() => { - store = undefined; - }); + afterEach(() => { + store = undefined; + }); } diff --git a/src/utils/textHighlight/TextHighlight.vue b/src/utils/textHighlight/TextHighlight.vue index 06e1b30a50..d22ad706f1 100644 --- a/src/utils/textHighlight/TextHighlight.vue +++ b/src/utils/textHighlight/TextHighlight.vue @@ -20,48 +20,44 @@ at runtime from the About dialog for additional information. --> diff --git a/tsconfig.json b/tsconfig.json index b618a6e789..545e4e4f10 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,33 +1,27 @@ /* Note: Open MCT does not intend to support the entire Typescript ecosystem at this time. * This file is intended to add Intellisense for IDEs like VSCode. For more information * about Typescript, please discuss in https://github.com/nasa/openmct/discussions/4693 -*/ + */ { - "compilerOptions": { - "baseUrl": "./", - "allowJs": true, - "checkJs": false, - "declaration": true, - "emitDeclarationOnly": true, - "declarationMap": true, - "strict": true, - "esModuleInterop": true, - "noImplicitOverride": true, - "module": "esnext", - "moduleResolution": "node", - "outDir": "dist", - "skipLibCheck": true, - "paths": { - // matches the alias in webpack config, so that types for those imports are visible. - "@/*": ["src/*"] - } - }, - "include": [ - "src/api/**/*.js" - ], - "exclude": [ - "node_modules", - "dist", - "**/*Spec.js" - ] + "compilerOptions": { + "baseUrl": "./", + "allowJs": true, + "checkJs": false, + "declaration": true, + "emitDeclarationOnly": true, + "declarationMap": true, + "strict": true, + "esModuleInterop": true, + "noImplicitOverride": true, + "module": "esnext", + "moduleResolution": "node", + "outDir": "dist", + "skipLibCheck": true, + "paths": { + // matches the alias in webpack config, so that types for those imports are visible. + "@/*": ["src/*"] + } + }, + "include": ["src/api/**/*.js"], + "exclude": ["node_modules", "dist", "**/*Spec.js"] } From 804dbf0caba9efd631d6b7b90e9645ddfe4e6b66 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 18 May 2023 15:08:13 -0700 Subject: [PATCH 300/594] chore: add `prettier` (3/3): update `.git-blame-ignore-revs` file (#6684) Update `.git-blame-ignore-revs` file --- .git-blame-ignore-revs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index ab9bcde005..6aa1fd0665 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -4,3 +4,9 @@ # Requires Git > 2.23 # See https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt +# Copyright year update 2022 +4a9744e916d24122a81092f6b7950054048ba860 +# Copyright year update 2023 +8040b275fcf2ba71b42cd72d4daa64bb25c19c2d +# Apply `prettier` formatting +caa7bc6faebc204f67aedae3e35fb0d0d3ce27a7 From 7e12a4596049d2281e8d820b20261be874ba6a91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 May 2023 14:34:42 -0700 Subject: [PATCH 301/594] chore(deps-dev): bump vue-eslint-parser from 9.2.1 to 9.3.0 (#6671) Bumps [vue-eslint-parser](https://github.com/vuejs/vue-eslint-parser) from 9.2.1 to 9.3.0. - [Release notes](https://github.com/vuejs/vue-eslint-parser/releases) - [Commits](https://github.com/vuejs/vue-eslint-parser/compare/v9.2.1...v9.3.0) --- updated-dependencies: - dependency-name: vue-eslint-parser dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c41900879b..faa7fbd8d8 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "typescript": "5.0.4", "uuid": "9.0.0", "vue": "2.6.14", - "vue-eslint-parser": "9.2.1", + "vue-eslint-parser": "9.3.0", "vue-loader": "15.9.8", "vue-template-compiler": "2.6.14", "webpack": "5.81.0", From 356c90ca45b9ed19a2593b03796156cfc2875f26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 May 2023 18:21:33 +0000 Subject: [PATCH 302/594] chore(deps-dev): bump @babel/eslint-parser from 7.19.1 to 7.21.8 (#6648) Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.19.1 to 7.21.8. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.21.8/eslint/babel-eslint-parser) --- updated-dependencies: - dependency-name: "@babel/eslint-parser" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index faa7fbd8d8..7b6fe1e365 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "2.2.4-SNAPSHOT", "description": "The Open MCT core platform", "devDependencies": { - "@babel/eslint-parser": "7.19.1", + "@babel/eslint-parser": "7.21.8", "@braintree/sanitize-url": "6.0.2", "@deploysentinel/playwright": "0.3.4", "@percy/cli": "1.24.0", From fea68381a75adae5b4b8954cf6f9d9e807b2505d Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Wed, 24 May 2023 02:29:19 -0700 Subject: [PATCH 303/594] fix: unlisten to annotation event beforeDestroy (#6690) * fix: unlisten to annotation event beforeDestroy * refactor: `npm run lint:fix` --------- Co-authored-by: Scott Bell --- .../inspectorViews/annotations/AnnotationsInspectorView.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue b/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue index 0726fa6b7b..fcf2f29653 100644 --- a/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue +++ b/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue @@ -128,6 +128,7 @@ export default { await this.updateSelection(this.openmct.selection.get()); }, beforeDestroy() { + this.openmct.annotation.off('targetDomainObjectAnnotated', this.loadAnnotationForTargetObject); this.openmct.selection.off('change', this.updateSelection); const unobserveEntryFunctions = Object.values(this.unobserveEntries); unobserveEntryFunctions.forEach((unobserveEntry) => { From 47b44cebbaaaa5292bccea60c5ce3294371a6bab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 16:22:50 +0000 Subject: [PATCH 304/594] chore(deps-dev): bump jasmine-core from 4.5.0 to 5.0.0 (#6666) Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 4.5.0 to 5.0.0. - [Release notes](https://github.com/jasmine/jasmine/releases) - [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md) - [Commits](https://github.com/jasmine/jasmine/compare/v4.5.0...v5.0.0) --- updated-dependencies: - dependency-name: jasmine-core dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b6fe1e365..c07c3523eb 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "git-rev-sync": "3.0.2", "html2canvas": "1.4.1", "imports-loader": "4.0.1", - "jasmine-core": "4.5.0", + "jasmine-core": "5.0.0", "karma": "6.4.2", "karma-chrome-launcher": "3.2.0", "karma-cli": "2.0.0", From 4d375ec765a6ae346112f306836938e74d3e2811 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 10:35:28 -0700 Subject: [PATCH 305/594] chore(deps-dev): bump webpack-cli from 5.0.2 to 5.1.1 (#6653) Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 5.0.2 to 5.1.1. - [Release notes](https://github.com/webpack/webpack-cli/releases) - [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@5.0.2...webpack-cli@5.1.1) --- updated-dependencies: - dependency-name: webpack-cli dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c07c3523eb..c83038625b 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "vue-loader": "15.9.8", "vue-template-compiler": "2.6.14", "webpack": "5.81.0", - "webpack-cli": "5.0.2", + "webpack-cli": "5.1.1", "webpack-dev-server": "4.13.3", "webpack-merge": "5.8.0" }, From 0bafdad605af2d34f24ece026fa8472d25323d40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 17:45:50 +0000 Subject: [PATCH 306/594] chore(deps-dev): bump webpack from 5.81.0 to 5.84.0 (#6692) Bumps [webpack](https://github.com/webpack/webpack) from 5.81.0 to 5.84.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.81.0...v5.84.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c83038625b..9aa54f4e9b 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "vue-eslint-parser": "9.3.0", "vue-loader": "15.9.8", "vue-template-compiler": "2.6.14", - "webpack": "5.81.0", + "webpack": "5.84.0", "webpack-cli": "5.1.1", "webpack-dev-server": "4.13.3", "webpack-merge": "5.8.0" From 4cab97cb4b90588966e5c322d4349cfe535667ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 17:57:44 +0000 Subject: [PATCH 307/594] chore(deps-dev): bump sinon from 15.0.1 to 15.1.0 (#6683) Bumps [sinon](https://github.com/sinonjs/sinon) from 15.0.1 to 15.1.0. - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md) - [Commits](https://github.com/sinonjs/sinon/compare/v15.0.1...v15.1.0) --- updated-dependencies: - dependency-name: sinon dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9aa54f4e9b..90a58fb58b 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "sanitize-html": "2.10.0", "sass": "1.62.1", "sass-loader": "13.2.2", - "sinon": "15.0.1", + "sinon": "15.1.0", "style-loader": "3.3.2", "typescript": "5.0.4", "uuid": "9.0.0", From 1c6214fe79f399af1dbc109579f8901f0ea8b349 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 15:54:37 -0700 Subject: [PATCH 308/594] chore(deps-dev): bump eslint from 8.40.0 to 8.41.0 (#6700) Bumps [eslint](https://github.com/eslint/eslint) from 8.40.0 to 8.41.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.40.0...v8.41.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90a58fb58b..cce92e0e48 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "d3-axis": "3.0.0", "d3-scale": "3.3.0", "d3-selection": "3.0.0", - "eslint": "8.40.0", + "eslint": "8.41.0", "eslint-plugin-compat": "4.1.4", "eslint-config-prettier": "8.8.0", "eslint-plugin-playwright": "0.12.0", From 295bfe92940e6a121ff0bba381d58f06d7301af8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 23:16:31 +0000 Subject: [PATCH 309/594] chore(deps-dev): bump eslint-plugin-vue from 9.13.0 to 9.14.1 (#6696) Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.13.0 to 9.14.1. - [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases) - [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.13.0...v9.14.1) --- updated-dependencies: - dependency-name: eslint-plugin-vue dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cce92e0e48..6956ccd2e0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "eslint-config-prettier": "8.8.0", "eslint-plugin-playwright": "0.12.0", "eslint-plugin-prettier": "4.2.1", - "eslint-plugin-vue": "9.13.0", + "eslint-plugin-vue": "9.14.1", "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0", "eventemitter3": "1.2.0", "file-saver": "2.0.5", From 47c5863edffbf59ef6d05dd6af9097bfe22a4c51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 23:32:56 +0000 Subject: [PATCH 310/594] chore(deps-dev): bump @percy/cli from 1.24.0 to 1.24.2 (#6699) Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.24.0 to 1.24.2. - [Release notes](https://github.com/percy/cli/releases) - [Commits](https://github.com/percy/cli/commits/v1.24.2/packages/cli) --- updated-dependencies: - dependency-name: "@percy/cli" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6956ccd2e0..a24f4b9132 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@babel/eslint-parser": "7.21.8", "@braintree/sanitize-url": "6.0.2", "@deploysentinel/playwright": "0.3.4", - "@percy/cli": "1.24.0", + "@percy/cli": "1.24.2", "@percy/playwright": "1.0.4", "@playwright/test": "1.32.3", "@types/eventemitter3": "1.2.0", From 9247951456799efed66c1cf77377b7476dbc9e49 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Wed, 31 May 2023 16:50:41 -0700 Subject: [PATCH 311/594] chore: bump version to `2.2.5-SNAPSHOT` (#6705) chore: bump snapshot version to 2.2.5-SNAPSHOT --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a24f4b9132..ec042facaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openmct", - "version": "2.2.4-SNAPSHOT", + "version": "2.2.5-SNAPSHOT", "description": "The Open MCT core platform", "devDependencies": { "@babel/eslint-parser": "7.21.8", From 07373817b0758eccbed557808b2e4ad99d73540d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 12:59:47 -0700 Subject: [PATCH 312/594] chore(deps-dev): bump webpack from 5.84.0 to 5.85.0 (#6704) Bumps [webpack](https://github.com/webpack/webpack) from 5.84.0 to 5.85.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.84.0...v5.85.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ec042facaa..71cdf9a041 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "vue-eslint-parser": "9.3.0", "vue-loader": "15.9.8", "vue-template-compiler": "2.6.14", - "webpack": "5.84.0", + "webpack": "5.85.0", "webpack-cli": "5.1.1", "webpack-dev-server": "4.13.3", "webpack-merge": "5.8.0" From a9158a90d5b61eeda90393e9da01516e80658b02 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Thu, 1 Jun 2023 23:26:14 +0200 Subject: [PATCH 313/594] Support filtering by severity for events tables (#6672) * hide tab if not editing and fix issue where configuration is null * show filters tab if editing * works with dropdown * add a none filter to remove 'filters applied' styling' * pass appropriate comparator * openmct side is ready * clear filter still not working * fix clearing of procedures * add filters * add some basic documentation * add some basic documentation * add some basic documentation * fix grammar issues and convert away from amd pattern * convert to permanent links * refactor: format with prettier * add aria labels for selects --- .../filters/FiltersInspectorViewProvider.js | 12 +- src/plugins/filters/README.md | 53 +++++++++ .../filters/components/FilterField.vue | 52 +++++++-- .../filters/components/FilterObject.vue | 15 ++- .../filters/components/GlobalFilters.vue | 11 ++ .../TableConfigurationViewProvider.js | 105 +++++++++--------- .../components/table-configuration.vue | 29 ++--- 7 files changed, 196 insertions(+), 81 deletions(-) create mode 100644 src/plugins/filters/README.md diff --git a/src/plugins/filters/FiltersInspectorViewProvider.js b/src/plugins/filters/FiltersInspectorViewProvider.js index d71fbd2fa1..16b5029fc1 100644 --- a/src/plugins/filters/FiltersInspectorViewProvider.js +++ b/src/plugins/filters/FiltersInspectorViewProvider.js @@ -49,10 +49,16 @@ define(['./components/FiltersView.vue', 'vue'], function (FiltersView, Vue) { }); }, showTab: function (isEditing) { - const hasPersistedFilters = Boolean(domainObject?.configuration?.filters); - const hasGlobalFilters = Boolean(domainObject?.configuration?.globalFilters); + if (isEditing) { + return true; + } - return hasPersistedFilters || hasGlobalFilters; + const metadata = openmct.telemetry.getMetadata(domainObject); + const metadataWithFilters = metadata + ? metadata.valueMetadatas.filter((value) => value.filters) + : []; + + return metadataWithFilters.length; }, priority: function () { return openmct.priority.DEFAULT; diff --git a/src/plugins/filters/README.md b/src/plugins/filters/README.md new file mode 100644 index 0000000000..321fa30432 --- /dev/null +++ b/src/plugins/filters/README.md @@ -0,0 +1,53 @@ + +# Server side filtering in Open MCT + +## Introduction + +In Open MCT, filters can be constructed to filter out telemetry data on the server side. This is useful for reducing the amount of data that needs to be sent to the client. For example, in [Open MCT for MCWS](https://github.com/NASA-AMMOS/openmct-mcws/blob/e8846d325cc3f659d8ad58d1d24efaafbe2b6bb7/src/constants.js#L115), they can be used to filter realtime data from recorded data. In the [Open MCT YAMCS plugin](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/src/providers/events.js#L44), we can use them to filter incoming event data by severity. + +## Installing the filter plugin + +You'll need to install the filter plugin first. For example: + +```js +openmct.install(openmct.plugins.Filters(['telemetry.plot.overlay', 'table'])); +``` + +will install the filters plugin and have it apply to overlay plots and tables. You can see an example of this in the [Open MCT YAMCS plugin](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/example/index.js#L58). + +## Defining a filter + +To define a filter, you'll need to add a new `filter` property to the domain object's `telemetry` metadata underneath the `values` array. For example, if you have a domain object with a `telemetry` metadata that looks like this: + +```js +{ + key: 'fruit', + name: 'Types of fruit', + filters: [{ + singleSelectionThreshold: true, + comparator: 'equals', + possibleValues: [ + { name: 'Apple', value: 'apple' }, + { name: 'Banana', value: 'banana' }, + { name: 'Orange', value: 'orange' } + ] + }] +} +``` + +This will define a filter that allows an operator to choose one (due to `singleSelectionThreshold` being `true`) of the three possible values. The `comparator` property defines how the filter will be applied to the telemetry data. +Setting `singleSelectionThreshold` to `false` will render the `possibleValues` as a series of checkboxes. Removing the `possibleValues` property will render the filter as a text box, allowing the operator to enter a value to filter on. + +Note that how the filter is interpreted is ultimately decided by the individual telemetry providers. + +## Implementing a filter in a telemetry provider + +Implementing a filter requires two parts: + +- First, one needs to add the filter implementation to the [subscribe](https://github.com/nasa/openmct/blob/5df7971438acb9e8b933edda2aed432b1b8bb27d/src/api/telemetry/TelemetryAPI.js#L366) method in your telemetry provider. The filter will be passed to you in the `options` argument. You can either add the filter to your telemetry subscription request, or filter manually as new messages appears. An example of the latter is [shown in the YAMCS plugin for Open MCT](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/src/providers/events.js#L95). + +- Second, one needs to add the filter implementation to the [request](https://github.com/nasa/openmct/blob/5df7971438acb9e8b933edda2aed432b1b8bb27d/src/api/telemetry/TelemetryAPI.js#L318) method in your telemetry provider. The filter again will be passed to you in the `options` argument. You can either add the filter to your telemetry request, or filter manually after the request is made. An example of the former is [shown in the YAMCS plugin for Open MCT](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/src/providers/historical-telemetry-provider.js#L171). + +## Using filters + +If you installed the plugin to have it apply to `table`, create a Telemetry Table in Open MCT and drag your telemetry object that contains the filter to it. Then click "Edit", and notice the "Filter" tab in the inspector. It allows operator to either select a "Global Filter", or a regular filter. The "Global Filter" will apply for all telemetry objects in the table, while the regular filter will only apply to the telemetry object that it is defined on. \ No newline at end of file diff --git a/src/plugins/filters/components/FilterField.vue b/src/plugins/filters/components/FilterField.vue index d8edc53fff..40daab0a71 100644 --- a/src/plugins/filters/components/FilterField.vue +++ b/src/plugins/filters/components/FilterField.vue @@ -37,14 +37,37 @@ :id="`${filter}filterControl`" class="c-input--flex" type="text" + :aria-label="label" :disabled="useGlobal" :value="persistedValue(filter)" - @change="updateFilterValue($event, filter)" + @change="updateFilterValueFromString($event, filter)" /> + + + - @@ -55,7 +55,7 @@ export default { this.$el.addEventListener('change', this.onChange); } }, - beforeDestroy() { + beforeUnmount() { if (this.options.type === 'number') { this.$el.removeEventListener('input', this.onInput); } else { diff --git a/src/utils/mount.js b/src/utils/mount.js new file mode 100644 index 0000000000..2c99da8dc1 --- /dev/null +++ b/src/utils/mount.js @@ -0,0 +1,26 @@ +import { h, render } from 'vue'; + +export default function mount(component, { props, children, element, app } = {}) { + let el = element; + + let vNode = h(component, props, children); + if (app && app._context) { + vNode.appContext = app._context; + } + if (el) { + render(vNode, el); + } else if (typeof document !== 'undefined') { + render(vNode, (el = document.createElement('div'))); + } + + // eslint-disable-next-line func-style + const destroy = () => { + if (el) { + render(null, el); + } + el = null; + vNode = null; + }; + + return { vNode, destroy, el }; +} From 16e1ac2529b28f6913a0b5d581cf36d46b17e2b9 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 27 Jul 2023 19:06:41 -0700 Subject: [PATCH 364/594] Fixes for e2e tests following the Vue 3 compat upgrade (#6837) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * clock, timeConductor and appActions fixes * Ensure realtime uses upstream context when available Eliminate ambiguity when looking for time conductor locator * Fix log plot e2e tests * Fix displayLayout e2e tests * Specify global time conductor to fix issues with duplicate selectors with independent time contexts * a11y: ARIA for conductor and independent time conductor * a11y: fix label collisions, specify 'Menu' in label * Add watch mode * fix(e2e): update appActions and tests to use a11y locators for ITC * Don't remove the itc popup from the DOM. Just show/hide it once it's added the first time. * test(e2e): disable one imagery test due to known bug * Add fixme to tagging tests, issue described in 6822 * Fix locator for time conductor popups * Improve how time bounds are set in independent time conductor. Fix tests for flexible layout and timestrip * Fix some tests for itc for display layouts * Fix Inspector tabs remounting on change * fix autoscale test and snapshot * Fix telemetry table test * Fix timestrip test * e2e: move test info annotations to within test * 6826: Fixes padStart error due to using it on a number rather than a string * fix(e2e): update snapshots * fix(e2e): fix restricted notebook locator * fix(restrictedNotebook): fix issue causing sections not to update on lock * fix(restrictedNotebook): fix issue causing snapshots to not be able to be deleted from a locked page - Using `this.$delete(arr, index)` does not update the `length` property on the underlying target object, so it can lead to bizarre issues where your array is of length 4 but it has 3 objects in it. * fix: replace all instances of `$delete` with `Array.splice()` or `delete` * fix(e2e): fix grand search test * fix(#3117): can remove item from displayLayout via tree context menu while viewing another item * fix: remove typo * Wait for background image to load * fix(#6832): timelist events can tick down * fix: ensure that menuitems have the raw objects so emits work * fix: assign new arrays instead of editing state in-place * refactor(timelist): use `getClock()` instead of `clock()` * Revert "refactor(timelist): use `getClock()` instead of `clock()`" This reverts commit d88855311289bcf9e0d94799cdeee25c8628f65d. * refactor(timelist): use new timeAPI * Stop ticking when the independent time context is disabled (#6833) * Turn off the clock ticket for independent time conductor when it is disabled * Fix linting issues --------- Co-authored-by: Khalid Adil * test: update couchdb notebook test * fix: codeQL warnings * fix(tree-item): infinite spinner issue - Using `indexOf()` with an object was failing due to some items in the tree being Proxy-wrapped and others not. So instead, use `findIndex()` with a predicate that compares the navigationPaths of both objects * [Timer] Remove "refresh" call, it is not needed (#6841) * removing an unneccessary refresh that waas causing many get requests * lets just pretend this never happened * fix(mct-tree): maintain reactivity of all tree items * Hide change role button in the indicator in cases where there is only… (#6840) Hide change role button in the indicator in cases where there is only a single role available for the current user --------- Co-authored-by: Shefali Co-authored-by: Khalid Adil Co-authored-by: John Hill Co-authored-by: David Tsay Co-authored-by: Jamie V --- e2e/appActions.js | 101 +++++-- e2e/tests/functional/notification.e2e.spec.js | 4 +- .../functional/planning/timelist.e2e.spec.js | 2 +- .../functional/planning/timestrip.e2e.spec.js | 30 +- .../plugins/clocks/clock.e2e.spec.js | 2 +- .../displayLayout/displayLayout.e2e.spec.js | 22 +- .../flexibleLayout/flexibleLayout.e2e.spec.js | 16 +- .../imagery/exampleImagery.e2e.spec.js | 67 +++-- .../notebook/notebookWithCouchDB.e2e.spec.js | 7 +- .../notebook/restrictedNotebook.e2e.spec.js | 2 +- .../plugins/plot/autoscale.e2e.spec.js | 11 +- .../autoscale-canvas-panned-chrome-darwin.png | Bin 19847 -> 19194 bytes .../autoscale-canvas-panned-chrome-linux.png | Bin 17867 -> 19282 bytes .../autoscale-canvas-prepan-chrome-darwin.png | Bin 19841 -> 19575 bytes .../autoscale-canvas-prepan-chrome-linux.png | Bin 19283 -> 20002 bytes .../plugins/plot/logPlot.e2e.spec.js | 10 +- .../plugins/plot/tagging.e2e.spec.js | 6 +- .../telemetryTable/telemetryTable.e2e.spec.js | 15 +- .../timeConductor/timeConductor.e2e.spec.js | 67 ++--- .../functional/recentObjects.e2e.spec.js | 97 +++--- e2e/tests/functional/search.e2e.spec.js | 4 +- package.json | 1 + src/api/menu/MenuAPISpec.js | 10 +- src/api/menu/components/Menu.vue | 2 - src/api/menu/components/SuperMenu.vue | 2 - src/api/menu/menu.js | 2 +- src/api/objects/InMemorySearchProvider.js | 2 +- src/api/objects/ObjectAPISpec.js | 3 + .../overlays/components/OverlayComponent.vue | 8 +- src/api/time/IndependentTimeContext.js | 19 ++ src/api/user/UserStatusAPISpec.js | 2 + .../components/LADTableConfiguration.vue | 4 +- .../LADTable/components/LadTableSet.vue | 2 +- src/plugins/LADTable/pluginSpec.js | 1 + .../URLTimeSettingsSynchronizer/pluginSpec.js | 2 +- .../autoflow/AutoflowTabularPluginSpec.js | 2 +- .../charts/bar/inspector/BarGraphOptions.vue | 2 +- src/plugins/charts/bar/pluginSpec.js | 38 +-- .../scatter/inspector/PlotOptionsBrowse.vue | 2 +- .../scatter/inspector/PlotOptionsEdit.vue | 2 +- src/plugins/charts/scatter/pluginSpec.js | 19 +- .../clock/components/ClockIndicator.vue | 2 +- src/plugins/clock/pluginSpec.js | 9 + src/plugins/conditionWidget/pluginSpec.js | 4 +- .../components/DisplayLayout.vue | 7 +- src/plugins/displayLayout/pluginSpec.js | 6 +- .../FaultManagementListView.vue | 2 +- .../filters/components/FiltersView.vue | 8 +- src/plugins/flexibleLayout/pluginSpec.js | 18 +- src/plugins/gauge/GaugePluginSpec.js | 12 +- src/plugins/hyperlink/pluginSpec.js | 2 +- .../imagery/components/ImageryView.vue | 2 +- src/plugins/imagery/pluginSpec.js | 56 ++-- src/plugins/notebook/components/Notebook.vue | 11 +- .../notebook/components/NotebookEmbed.vue | 9 +- src/plugins/notebook/pluginSpec.js | 20 +- .../notebook/utils/notebook-entriesSpec.js | 2 +- .../components/NotificationIndicator.vue | 4 + .../notificationIndicator/pluginSpec.js | 2 +- src/plugins/plot/configuration/PlotSeries.js | 2 +- src/plugins/plot/overlayPlot/pluginSpec.js | 2 +- src/plugins/plot/pluginSpec.js | 4 +- src/plugins/plot/stackedPlot/StackedPlot.vue | 2 +- src/plugins/plot/stackedPlot/pluginSpec.js | 2 +- src/plugins/timeConductor/Conductor.vue | 6 +- src/plugins/timeConductor/ConductorClock.vue | 37 ++- .../timeConductor/ConductorInputsFixed.vue | 2 + src/plugins/timeConductor/ConductorMode.vue | 8 +- .../timeConductor/ConductorTimeSystem.vue | 1 + src/plugins/timeConductor/DatePicker.vue | 21 +- src/plugins/timeConductor/conductor.scss | 4 + src/plugins/timeConductor/date-picker.scss | 154 +++++----- .../independent/IndependentClock.vue | 37 ++- .../independent/IndependentMode.vue | 1 + .../independent/IndependentTimeConductor.vue | 14 +- .../independentTimeConductorPopUpManager.js | 12 +- src/plugins/timeConductor/pluginSpec.js | 34 ++- src/plugins/timeConductor/timePopupFixed.vue | 157 +++++----- .../timeConductor/timePopupRealtime.vue | 283 +++++++++--------- src/plugins/timeline/TimelineViewLayout.vue | 8 +- src/plugins/timeline/pluginSpec.js | 17 +- src/plugins/timelist/Timelist.vue | 24 +- src/plugins/timer/components/Timer.vue | 2 - .../components/UserIndicator.vue | 12 +- src/plugins/webPage/pluginSpec.js | 2 +- src/styles/_constants.scss | 2 +- src/ui/components/ObjectFrame.vue | 8 +- src/ui/inspector/Inspector.vue | 14 +- src/ui/inspector/InspectorStylesSpec.js | 2 +- src/ui/inspector/InspectorTabs.vue | 25 +- src/ui/inspector/InspectorViews.vue | 21 +- src/ui/layout/BrowseBar.vue | 9 +- src/ui/layout/LayoutSpec.js | 2 +- src/ui/layout/mct-tree.vue | 36 ++- src/ui/mixins/context-menu-gesture.js | 2 +- src/utils/testing.js | 4 +- 96 files changed, 921 insertions(+), 816 deletions(-) diff --git a/e2e/appActions.js b/e2e/appActions.js index 56cc0e5a7f..230602334b 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -314,15 +314,13 @@ async function _isInEditMode(page, identifier) { */ async function setTimeConductorMode(page, isFixedTimespan = true) { // Click 'mode' button - const timeConductorMode = await page.locator('.c-compact-tc'); - await timeConductorMode.click(); - await timeConductorMode.locator('.js-mode-button').click(); - + await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); + await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click(); // Switch time conductor mode if (isFixedTimespan) { - await page.locator('data-testid=conductor-modeOption-fixed').click(); + await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click(); } else { - await page.locator('data-testid=conductor-modeOption-realtime').click(); + await page.getByRole('menuitem', { name: /Real-Time/ }).click(); } } @@ -344,9 +342,12 @@ async function setRealTimeMode(page) { /** * @typedef {Object} OffsetValues - * @property {string | undefined} hours - * @property {string | undefined} mins - * @property {string | undefined} secs + * @property {string | undefined} startHours + * @property {string | undefined} startMins + * @property {string | undefined} startSecs + * @property {string | undefined} endHours + * @property {string | undefined} endMins + * @property {string | undefined} endSecs */ /** @@ -355,19 +356,32 @@ async function setRealTimeMode(page) { * @param {OffsetValues} offset * @param {import('@playwright/test').Locator} offsetButton */ -async function setTimeConductorOffset(page, { hours, mins, secs }) { - // await offsetButton.click(); - - if (hours) { - await page.fill('.pr-time-input__hrs', hours); +async function setTimeConductorOffset( + page, + { startHours, startMins, startSecs, endHours, endMins, endSecs } +) { + if (startHours) { + await page.getByRole('spinbutton', { name: 'Start offset hours' }).fill(startHours); } - if (mins) { - await page.fill('.pr-time-input__mins', mins); + if (startMins) { + await page.getByRole('spinbutton', { name: 'Start offset minutes' }).fill(startMins); } - if (secs) { - await page.fill('.pr-time-input__secs', secs); + if (startSecs) { + await page.getByRole('spinbutton', { name: 'Start offset seconds' }).fill(startSecs); + } + + if (endHours) { + await page.getByRole('spinbutton', { name: 'End offset hours' }).fill(endHours); + } + + if (endMins) { + await page.getByRole('spinbutton', { name: 'End offset minutes' }).fill(endMins); + } + + if (endSecs) { + await page.getByRole('spinbutton', { name: 'End offset seconds' }).fill(endSecs); } // Click the check button @@ -381,8 +395,7 @@ async function setTimeConductorOffset(page, { hours, mins, secs }) { */ async function setStartOffset(page, offset) { // Click 'mode' button - const timeConductorMode = await page.locator('.c-compact-tc'); - await timeConductorMode.click(); + await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); await setTimeConductorOffset(page, offset); } @@ -393,11 +406,53 @@ async function setStartOffset(page, offset) { */ async function setEndOffset(page, offset) { // Click 'mode' button - const timeConductorMode = await page.locator('.c-compact-tc'); - await timeConductorMode.click(); + await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); await setTimeConductorOffset(page, offset); } +async function setTimeConductorBounds(page, startDate, endDate) { + // Bring up the time conductor popup + await page.click('.l-shell__time-conductor.c-compact-tc'); + + await setTimeBounds(page, startDate, endDate); + + await page.keyboard.press('Enter'); +} + +async function setIndependentTimeConductorBounds(page, startDate, endDate) { + // Activate Independent Time Conductor in Fixed Time Mode + await page.getByRole('switch').click(); + + // Bring up the time conductor popup + await page.click('.c-conductor-holder--compact .c-compact-tc'); + + await expect(page.locator('.itc-popout')).toBeVisible(); + + await setTimeBounds(page, startDate, endDate); + + await page.keyboard.press('Enter'); +} + +async function setTimeBounds(page, startDate, endDate) { + if (startDate) { + // Fill start time + await page + .getByRole('textbox', { name: 'Start date' }) + .fill(startDate.toString().substring(0, 10)); + await page + .getByRole('textbox', { name: 'Start time' }) + .fill(startDate.toString().substring(11, 19)); + } + + if (endDate) { + // Fill end time + await page.getByRole('textbox', { name: 'End date' }).fill(endDate.toString().substring(0, 10)); + await page + .getByRole('textbox', { name: 'End time' }) + .fill(endDate.toString().substring(11, 19)); + } +} + /** * Selects an inspector tab based on the provided tab name * @@ -509,6 +564,8 @@ module.exports = { setRealTimeMode, setStartOffset, setEndOffset, + setTimeConductorBounds, + setIndependentTimeConductorBounds, selectInspectorTab, waitForPlotsToRender }; diff --git a/e2e/tests/functional/notification.e2e.spec.js b/e2e/tests/functional/notification.e2e.spec.js index 69719ea0b3..040fbc666d 100644 --- a/e2e/tests/functional/notification.e2e.spec.js +++ b/e2e/tests/functional/notification.e2e.spec.js @@ -28,10 +28,10 @@ const { createDomainObjectWithDefaults, createNotification } = require('../../ap const { test, expect } = require('../../pluginFixtures'); test.describe('Notifications List', () => { - test('Notifications can be dismissed individually', async ({ page }) => { + test.fixme('Notifications can be dismissed individually', async ({ page }) => { test.info().annotations.push({ type: 'issue', - description: 'https://github.com/nasa/openmct/issues/6122' + description: 'https://github.com/nasa/openmct/issues/6820' }); // Go to baseURL diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index 54f65019f6..b5208e909c 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -110,7 +110,7 @@ test.describe('Time List', () => { await test.step('Does not show milliseconds in times', async () => { // Get the first activity - const row = await page.locator('.js-list-item').first(); + const row = page.locator('.js-list-item').first(); // Verify that none fo the times have milliseconds displayed. // Example: 2024-11-17T16:00:00Z is correct and 2024-11-17T16:00:00.000Z is wrong diff --git a/e2e/tests/functional/planning/timestrip.e2e.spec.js b/e2e/tests/functional/planning/timestrip.e2e.spec.js index 0bff22ffd4..5b882df74d 100644 --- a/e2e/tests/functional/planning/timestrip.e2e.spec.js +++ b/e2e/tests/functional/planning/timestrip.e2e.spec.js @@ -21,7 +21,11 @@ *****************************************************************************/ const { test, expect } = require('../../../pluginFixtures'); -const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions'); +const { + createDomainObjectWithDefaults, + createPlanFromJSON, + setIndependentTimeConductorBounds +} = require('../../../appActions'); const testPlan = { TEST_GROUP: [ @@ -78,9 +82,6 @@ test.describe('Time Strip', () => { }); // Constant locators - const independentTimeConductorInputs = page.locator( - '.l-shell__main-independent-time-conductor .c-input--datetime' - ); const activityBounds = page.locator('.activity-bounds'); // Goto baseURL @@ -122,9 +123,7 @@ test.describe('Time Strip', () => { }); await test.step('TimeStrip can use the Independent Time Conductor', async () => { - // Activate Independent Time Conductor in Fixed Time Mode - await page.click('.c-toggle-switch__slider'); - expect(await activityBounds.count()).toEqual(0); + expect(await activityBounds.count()).toEqual(5); // Set the independent time bounds so that only one event is shown const startBound = testPlan.TEST_GROUP[0].start; @@ -132,12 +131,7 @@ test.describe('Time Strip', () => { const startBoundString = new Date(startBound).toISOString().replace('T', ' '); const endBoundString = new Date(endBound).toISOString().replace('T', ' '); - await independentTimeConductorInputs.nth(0).fill(''); - await independentTimeConductorInputs.nth(0).fill(startBoundString); - await page.keyboard.press('Enter'); - await independentTimeConductorInputs.nth(1).fill(''); - await independentTimeConductorInputs.nth(1).fill(endBoundString); - await page.keyboard.press('Enter'); + await setIndependentTimeConductorBounds(page, startBoundString, endBoundString); expect(await activityBounds.count()).toEqual(1); }); @@ -156,9 +150,6 @@ test.describe('Time Strip', () => { await page.click("button[title='Save']"); await page.click("li[title='Save and Finish Editing']"); - // Activate Independent Time Conductor in Fixed Time Mode - await page.click('.c-toggle-switch__slider'); - // All events should be displayed at this point because the // initial independent context bounds will match the global bounds expect(await activityBounds.count()).toEqual(5); @@ -169,12 +160,7 @@ test.describe('Time Strip', () => { const startBoundString = new Date(startBound).toISOString().replace('T', ' '); const endBoundString = new Date(endBound).toISOString().replace('T', ' '); - await independentTimeConductorInputs.nth(0).fill(''); - await independentTimeConductorInputs.nth(0).fill(startBoundString); - await page.keyboard.press('Enter'); - await independentTimeConductorInputs.nth(1).fill(''); - await independentTimeConductorInputs.nth(1).fill(endBoundString); - await page.keyboard.press('Enter'); + await setIndependentTimeConductorBounds(page, startBoundString, endBoundString); // Verify that two events are displayed expect(await activityBounds.count()).toEqual(2); diff --git a/e2e/tests/functional/plugins/clocks/clock.e2e.spec.js b/e2e/tests/functional/plugins/clocks/clock.e2e.spec.js index 565da1e116..b0afc5167f 100644 --- a/e2e/tests/functional/plugins/clocks/clock.e2e.spec.js +++ b/e2e/tests/functional/plugins/clocks/clock.e2e.spec.js @@ -41,7 +41,7 @@ test.describe('Clock Generator CRUD Operations', () => { await page.click('button:has-text("Create")'); // Click Clock - await page.click('text=Clock'); + await page.getByRole('menuitem').first().click(); // Click .icon-arrow-down await page.locator('.icon-arrow-down').click(); diff --git a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js index 37840b6b62..e231074076 100644 --- a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js @@ -25,7 +25,8 @@ const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, - setRealTimeMode + setRealTimeMode, + setIndependentTimeConductorBounds } = require('../../../../appActions'); test.describe('Display Layout', () => { @@ -231,20 +232,27 @@ test.describe('Display Layout', () => { let layoutGridHolder = page.locator('.l-layout__grid-holder'); await exampleImageryTreeItem.dragTo(layoutGridHolder); + //adjust so that we can see the independent time conductor toggle + // Adjust object height + await page.locator('div[title="Resize object height"] > input').click(); + await page.locator('div[title="Resize object height"] > input').fill('70'); + + // Adjust object width + await page.locator('div[title="Resize object width"] > input').click(); + await page.locator('div[title="Resize object width"] > input').fill('70'); + await page.locator('button[title="Save"]').click(); await page.locator('text=Save and Finish Editing').click(); - // flip on independent time conductor - await page.getByTitle('Enable independent Time Conductor').first().locator('label').click(); - await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z'); - await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z'); - await page.getByRole('textbox').nth(1).click(); + const startDate = '2021-12-30 01:01:00.000Z'; + const endDate = '2021-12-30 01:11:00.000Z'; + await setIndependentTimeConductorBounds(page, startDate, endDate); // check image date await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); // flip it off - await page.getByTitle('Disable independent Time Conductor').first().locator('label').click(); + await page.getByRole('switch').click(); // timestamp shouldn't be in the past anymore await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden(); }); diff --git a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js index ce16227ad0..f81b5298a0 100644 --- a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js @@ -21,7 +21,10 @@ *****************************************************************************/ const { test, expect } = require('../../../../pluginFixtures'); -const { createDomainObjectWithDefaults } = require('../../../../appActions'); +const { + createDomainObjectWithDefaults, + setIndependentTimeConductorBounds +} = require('../../../../appActions'); test.describe('Flexible Layout', () => { let sineWaveObject; @@ -187,16 +190,17 @@ test.describe('Flexible Layout', () => { await page.locator('text=Save and Finish Editing').click(); // flip on independent time conductor - await page.getByTitle('Enable independent Time Conductor').first().locator('label').click(); - await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z'); - await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z'); - await page.getByRole('textbox').nth(1).click(); + await setIndependentTimeConductorBounds( + page, + '2021-12-30 01:01:00.000Z', + '2021-12-30 01:11:00.000Z' + ); // check image date await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); // flip it off - await page.getByTitle('Disable independent Time Conductor').first().locator('label').click(); + await page.getByRole('switch').click(); // timestamp shouldn't be in the past anymore await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden(); }); diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index 5ef4a6a06b..d64688e044 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -27,7 +27,7 @@ but only assume that example imagery is present. /* globals process */ const { waitForAnimations } = require('../../../../baseFixtures'); const { test, expect } = require('../../../../pluginFixtures'); -const { createDomainObjectWithDefaults } = require('../../../../appActions'); +const { createDomainObjectWithDefaults, setRealTimeMode } = require('../../../../appActions'); const backgroundImageSelector = '.c-imagery__main-image__background-image'; const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt']; const tagHotkey = ['Shift', 'Alt']; @@ -46,6 +46,7 @@ test.describe('Example Imagery Object', () => { // Verify that the created object is focused await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name); await page.locator('.c-imagery__main-image__bg').hover({ trial: true }); + await page.locator(backgroundImageSelector).waitFor(); }); test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => { @@ -71,46 +72,60 @@ test.describe('Example Imagery Object', () => { }); test('Can use independent time conductor to change time', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6821' + }); // Test independent fixed time with global fixed time // flip on independent time conductor - await page.getByTitle('Enable independent Time Conductor').locator('label').click(); - await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z'); - await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z'); - await page.getByRole('textbox').nth(1).click(); + await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click(); + await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click(); + await page.getByRole('textbox', { name: 'Start date' }).click(); + await page.getByRole('textbox', { name: 'Start date' }).fill(''); + await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30'); + await page.getByRole('textbox', { name: 'Start time' }).click(); + await page.getByRole('textbox', { name: 'Start time' }).fill(''); + await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00'); + await page.getByRole('textbox', { name: 'End date' }).click(); + await page.getByRole('textbox', { name: 'End date' }).fill(''); + await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30'); + await page.getByRole('textbox', { name: 'End time' }).click(); + await page.getByRole('textbox', { name: 'End time' }).fill(''); + await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00'); + await page.getByRole('button', { name: 'Submit time bounds' }).click(); // check image date await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); // flip it off - await page.getByTitle('Disable independent Time Conductor').locator('label').click(); + await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click(); // timestamp shouldn't be in the past anymore await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden(); // Test independent fixed time with global realtime - await page.getByRole('button', { name: /Fixed Timespan/ }).click(); - await page.getByTestId('conductor-modeOption-realtime').click(); - await page.getByTitle('Enable independent Time Conductor').locator('label').click(); + await setRealTimeMode(page); + await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click(); // check image date to be in the past await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); // flip it off - await page.getByTitle('Disable independent Time Conductor').locator('label').click(); + await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click(); // timestamp shouldn't be in the past anymore await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden(); // Test independent realtime with global realtime - await page.getByTitle('Enable independent Time Conductor').locator('label').click(); + await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click(); // check image date await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); // change independent time to realtime - await page.getByRole('button', { name: /Fixed Timespan/ }).click(); - await page.getByRole('menuitem', { name: /Local Clock/ }).click(); + await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click(); + await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click(); + await page.getByRole('menuitem', { name: /Real-Time/ }).click(); // timestamp shouldn't be in the past anymore await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden(); // back to the past - await page - .getByRole('button', { name: /Local Clock/ }) - .first() - .click(); + await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click(); + await page.getByRole('menuitem', { name: /Real-Time/ }).click(); + await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click(); await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click(); // check image date to be in the past await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); @@ -247,7 +262,7 @@ test.describe('Example Imagery Object', () => { test('Uses low fetch priority', async ({ page }) => { const priority = await page.locator('.js-imageryView-image').getAttribute('fetchpriority'); - await expect(priority).toBe('low'); + expect(priority).toBe('low'); }); }); @@ -281,7 +296,7 @@ test.describe('Example Imagery in Display Layout', () => { await setRealTimeMode(page); // pause/play button - const pausePlayButton = await page.locator('.c-button.pause-play'); + const pausePlayButton = page.locator('.c-button.pause-play'); await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); @@ -304,7 +319,7 @@ test.describe('Example Imagery in Display Layout', () => { await setRealTimeMode(page); // pause/play button - const pausePlayButton = await page.locator('.c-button.pause-play'); + const pausePlayButton = page.locator('.c-button.pause-play'); await pausePlayButton.click(); await expect.soft(pausePlayButton).toHaveClass(/is-paused/); @@ -928,15 +943,3 @@ async function createImageryView(page) { page.waitForSelector('.c-message-banner__message') ]); } - -/** - * @param {import('@playwright/test').Page} page - */ -async function setRealTimeMode(page) { - await page.locator('.c-compact-tc').click(); - await page.waitForSelector('.c-tc-input-popup', { state: 'visible' }); - // Click mode dropdown - await page.getByRole('button', { name: ' Fixed Timespan ' }).click(); - // Click realtime - await page.getByTestId('conductor-modeOption-realtime').click(); -} diff --git a/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js index 377e6de07f..646e08d0e9 100644 --- a/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js @@ -51,10 +51,9 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { page.on('request', (request) => notebookElementsRequests.push(request)); //Clicking Add Page generates - let [notebookUrlRequest, allDocsRequest] = await Promise.all([ + let [notebookUrlRequest] = await Promise.all([ // Waits for the next request with the specified url page.waitForRequest(`**/openmct/${testNotebook.uuid}`), - page.waitForRequest('**/openmct/_all_docs?include_docs=true'), // Triggers the request page.click('[aria-label="Add Page"]') ]); @@ -64,15 +63,13 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { // Assert that only two requests are made // Network Requests are: // 1) The actual POST to create the page - // 2) The shared worker event from 👆 request - expect(notebookElementsRequests.length).toBe(2); + expect(notebookElementsRequests.length).toBe(1); // Assert on request object expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name); expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual( notebookUrlRequest.postDataJSON().model.modified ); - expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid); // Add an entry // Network Requests are: diff --git a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js index 584a1c0401..5f14593937 100644 --- a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js @@ -134,7 +134,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc // Click the context menu button for the new page await page.getByTitle('Open context menu').click(); // Delete the page - await page.getByRole('listitem', { name: 'Delete Page' }).click(); + await page.getByRole('menuitem', { name: 'Delete Page' }).click(); // Click OK button await page.getByRole('button', { name: 'Ok' }).click(); diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js index 821015e283..3c1237a71c 100644 --- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js @@ -24,7 +24,7 @@ Testsuite for plot autoscale. */ -const { selectInspectorTab } = require('../../../../appActions'); +const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions'); const { test, expect } = require('../../../../pluginFixtures'); test.use({ viewport: { @@ -107,7 +107,7 @@ test.describe('Autoscale', () => { await page.keyboard.up('Alt'); // Ensure the drag worked. - await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00', '3.50']); + await testYTicks(page, ['-0.50', '0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00']); //Wait for canvas to stablize. await canvas.hover({ trial: true }); @@ -131,12 +131,7 @@ async function setTimeRange( // Set a specific time range for consistency, otherwise it will change // on every test to a range based on the current time. - const timeInputs = page.locator('input.c-input--datetime'); - await timeInputs.first().click(); - await timeInputs.first().fill(start); - - await timeInputs.nth(1).click(); - await timeInputs.nth(1).fill(end); + await setTimeConductorBounds(page, start, end); } /** diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin.png b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin.png index e9e82fd14e65f3ee6fe7b5e91f47fe1a0ab1a679..abdb37b8dcd7b19079280beab2ba11f29ad08200 100644 GIT binary patch literal 19194 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 literal 19847 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 z zrsI(q;Xl0?G{Hf?)%3d`YH_U8OZotSJ0bLkhOiJ2#^9Er9DADq@B8{5l1)6xnz+*= z9WPO9HGC~tG0@#^{+{s38d{3~KLHF@mw=iug1UJEm5#>yu-l z?p*~~5_`>~Py}PZTugSmT$#1a4tov-tUm5$X9L{{dk$W3UWv#Mrc$>0tevG2e7~+= zenvyfZQ2$4@$r}{)Q$Q+Z4BuR+9D?tdtC8&*Fm&SiuL$4LaA)*zXdB zFDX@IpB2?X!*9Mj@ii^AMlYn^X}df(C(vJ}(be0EmoLO`B$yT&FFKq&ebEaP9I1ju zdygEWOVbiAdUV2;=DnlFSGc-;D<@NI=0$uY+smv2@0f%)1N(d$(nIXhNnw85V8ovF zE_Ai|sFyLl9Q6-2OHqwZWoD@Y_x1`v!?-S>`C*3DAtKS%l4s6DUwqicsD1c(RN)Kt zymO39+&k6wAo{zTvg3K3S*ySYiotFd%T_0!6ASJ}iF8OVwpCaM+Pt>UQb()Ogz*_~ zTI7&+ru}8EJ2(TRcEw}bf9}w<*A8#abR2Jkv~QY35X8)WZ30bubeZ*tVl(iqZ7$95 zJL7Nyr(N_8vRPtJ9kAekBBwUPr@Or%s_>g4fkyb4L=u6iaL$c8#c8F84gKYe>lcYo z3r3(Tv>chFD~u8?`5s8jHQ>H!<6Ec9{Q9%}vGny;oej5k&49`kaZKZo9Gj*3&OHO* z^t+hU9smij^3@8*a#h6RZG4YVUuc^~CzyVZmA%75Qv!hDaEVn%NW8WV96{QT3NUyA zG9(CGn=G88ujb8@H25d2K`LfHE_Fr2uo%7osT86PD=idu{JC z+RjBUJfsEO`BTc@hNdE(f8V+II8EV8+r)e%Yi8as2D)QU`7ewxstEOY=p#R8&UZ5ul9?R+2=u8{js^ga>T>?`m;O zU}M{o4I!_K$IV@bDn0I)P$-Oox3L3fdXSzv)eA+(z3)z^LQ0ywQ~8l@M2|~?n_-;F z`ur|Y*HZAihKN|2Z+5%Crujx&FNNAeP(Oh>i69M2r$x2DOH$#yeVA=94rv`2RkoVl z+7WTM;)!nF;th0XA8g1EpIzkICy7VMq_z6YucLT$i?5it)64-HmtC|#@Zdn3`Ca|| z?Yo7i+?{MYh0IeyPSijegWVLT>f!LIjS-pH=sL9hh}(iC#;vnFA=W`8JQP~djl9TW z=GSTMjHU0d&AuM8Z(zb%)`ha_8`(Ja;s#Svx(w7pA^jQB`)&nM0R!)28b}k{Ay$3r z%#q4*F|q22TkWAIyNk6VYgK0t(@ehQchycIq^HfF}Lr$6HfzNT1rqwMHt1tS0TN#*- z*f+QA&Yh$%_nHN=E-D6o)W&{pTfto-K`~U$a9mNUz~HWZ`&5@>BOJlYAkbp^BzqJ$ z;5I5Y5BIS*rfk&bjZW(Xyd!pvzLI}k3-IU(e_vGxH+yMpC75l*B=Pu(5p3V?Q8Kp%$0v%Dt&ZAr1YHI7 zK1TEaIQ}wl^PKJ_oAw$KB8?{+Tme3wzw9z6=-HT6l$TI)f;UGZj4$|ouwXdZ=hU|x4%{b4%g9Xs_Ll}UcC9>{{ZQ>p>zNM diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux.png b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux.png index dde9cbf0440edbba94b1082557c63e69e26d013c..1ff030d9580323f77d4bc98ee3cddae20c6ee29b 100644 GIT binary patch literal 19282 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 ZN*TO-ap|Ta&|SU2K>y_L*@rIP`ah{2mHPky literal 17867 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 diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin.png b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin.png index 0c0e7c6e70e00291f64e97266288100adde85ff7..148089b4bd1708838ee3cb9fe67a7242847927b7 100644 GIT binary patch literal 19575 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 JNB8=j{|Dz=e&PTC literal 19841 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 diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux.png b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux.png index e97b59129188677de56e8b9293bd1b2b0de5b1b5..7b60adcdb23e7d2deeb0f04abd1d5aa37eaf6019 100644 GIT binary patch literal 20002 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 literal 19283 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 SSpX_m8R%a)m3QLW-Tw#aO2iZZ diff --git a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js index ab498741b3..e7c3a7b081 100644 --- a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js @@ -26,7 +26,7 @@ necessarily be used for reference when writing new tests in this area. */ const { test, expect } = require('../../../../pluginFixtures'); -const { selectInspectorTab } = require('../../../../appActions'); +const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions'); test.describe('Log plot tests', () => { test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ @@ -87,12 +87,10 @@ async function makeOverlayPlot(page, myItemsFolderName) { // Set a specific time range for consistency, otherwise it will change // on every test to a range based on the current time. - const timeInputs = page.locator('input.c-input--datetime'); - await timeInputs.first().click(); - await timeInputs.first().fill('2022-03-29 22:00:00.000Z'); + const start = '2022-03-29 22:00:00.000Z'; + const end = '2022-03-29 22:00:30.000Z'; - await timeInputs.nth(1).click(); - await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z'); + await setTimeConductorBounds(page, start, end); // create overlay plot diff --git a/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js b/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js index bf49c1d407..aa3b6e5279 100644 --- a/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js @@ -32,7 +32,7 @@ const { waitForPlotsToRender } = require('../../../../appActions'); -test.describe('Plot Tagging', () => { +test.describe.fixme('Plot Tagging', () => { /** * Given a canvas and a set of points, tags the points on the canvas. * @param {import('@playwright/test').Page} page @@ -167,6 +167,10 @@ test.describe('Plot Tagging', () => { }); test('Tags work with Overlay Plots', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6822' + }); //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 test.slow(); diff --git a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js index 089c99c421..295e63dacd 100644 --- a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js +++ b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js @@ -20,7 +20,10 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -const { createDomainObjectWithDefaults } = require('../../../../appActions'); +const { + createDomainObjectWithDefaults, + setTimeConductorBounds +} = require('../../../../appActions'); const { test, expect } = require('../../../../pluginFixtures'); test.describe('Telemetry Table', () => { @@ -51,18 +54,14 @@ test.describe('Telemetry Table', () => { await expect(tableWrapper).toHaveClass(/is-paused/); // Subtract 5 minutes from the current end bound datetime and set it - const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1); - await endTimeInput.click(); - - let endDate = await endTimeInput.inputValue(); + // Bring up the time conductor popup + let endDate = await page.locator('[aria-label="End bounds"]').textContent(); endDate = new Date(endDate); endDate.setUTCMinutes(endDate.getUTCMinutes() - 5); endDate = endDate.toISOString().replace(/T/, ' '); - await endTimeInput.fill(''); - await endTimeInput.fill(endDate); - await page.keyboard.press('Enter'); + await setTimeConductorBounds(page, undefined, endDate); await expect(tableWrapper).not.toHaveClass(/is-paused/); diff --git a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js index 89fa1346d1..5dba7ef998 100644 --- a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js +++ b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js @@ -25,7 +25,8 @@ const { setFixedTimeMode, setRealTimeMode, setStartOffset, - setEndOffset + setEndOffset, + setTimeConductorBounds } = require('../../../../appActions'); test.describe('Time conductor operations', () => { @@ -40,38 +41,36 @@ test.describe('Time conductor operations', () => { let endDate = 'xxxx-01-01 02:00:00.000Z'; endDate = year + endDate.substring(4); - const startTimeLocator = page.locator('input[type="text"]').first(); - const endTimeLocator = page.locator('input[type="text"]').nth(1); - - // Click start time - await startTimeLocator.click(); - - // Click end time - await endTimeLocator.click(); - - await endTimeLocator.fill(endDate.toString()); - await startTimeLocator.fill(startDate.toString()); + await setTimeConductorBounds(page, startDate, endDate); // invalid start date startDate = year + 1 + startDate.substring(4); - await startTimeLocator.fill(startDate.toString()); - await endTimeLocator.click(); + await setTimeConductorBounds(page, startDate); - const startDateValidityStatus = await startTimeLocator.evaluate((element) => + // Bring up the time conductor popup + const timeConductorMode = await page.locator('.c-compact-tc'); + await timeConductorMode.click(); + const startDateLocator = page.locator('input[type="text"]').first(); + const endDateLocator = page.locator('input[type="text"]').nth(2); + + await endDateLocator.click(); + + const startDateValidityStatus = await startDateLocator.evaluate((element) => element.checkValidity() ); expect(startDateValidityStatus).not.toBeTruthy(); // fix to valid start date startDate = year - 1 + startDate.substring(4); - await startTimeLocator.fill(startDate.toString()); + await setTimeConductorBounds(page, startDate); // invalid end date endDate = year - 2 + endDate.substring(4); - await endTimeLocator.fill(endDate.toString()); - await startTimeLocator.click(); + await setTimeConductorBounds(page, undefined, endDate); - const endDateValidityStatus = await endTimeLocator.evaluate((element) => + await startDateLocator.click(); + + const endDateValidityStatus = await endDateLocator.evaluate((element) => element.checkValidity() ); expect(endDateValidityStatus).not.toBeTruthy(); @@ -83,11 +82,11 @@ test.describe('Time conductor operations', () => { test.describe('Time conductor input fields real-time mode', () => { test('validate input fields in real-time mode', async ({ page }) => { const startOffset = { - secs: '23' + startSecs: '23' }; const endOffset = { - secs: '31' + endSecs: '31' }; // Go to baseURL @@ -100,15 +99,13 @@ test.describe('Time conductor input fields real-time mode', () => { await setStartOffset(page, startOffset); // Verify time was updated on time offset button - await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText( - '00:30:23' - ); + await expect(page.locator('.c-compact-tc__setting-value.icon-minus')).toContainText('00:30:23'); // Set end time offset await setEndOffset(page, endOffset); // Verify time was updated on preceding time offset button - await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31'); + await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:31'); }); /** @@ -119,12 +116,12 @@ test.describe('Time conductor input fields real-time mode', () => { page }) => { const startOffset = { - mins: '30', - secs: '23' + startMins: '30', + startSecs: '23' }; const endOffset = { - secs: '01' + endSecs: '01' }; // Convert offsets to milliseconds @@ -150,12 +147,10 @@ test.describe('Time conductor input fields real-time mode', () => { await setRealTimeMode(page); // Verify updated start time offset persists after mode switch - await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText( - '00:30:23' - ); + await expect(page.locator('.c-compact-tc__setting-value.icon-minus')).toContainText('00:30:23'); // Verify updated end time offset persists after mode switch - await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01'); + await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:01'); // Verify url parameters persist after mode switch await page.waitForNavigation({ waitUntil: 'networkidle' }); @@ -203,11 +198,11 @@ test.describe('Time Conductor History', () => { // with startBound at 2022-01-01 00:00:00.000Z // and endBound at 2022-01-01 00:00:00.200Z await page.goto( - './#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true', - { waitUntil: 'networkidle' } + './#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true' ); - await page.locator("[aria-label='Time Conductor History']").hover({ trial: true }); - await page.locator("[aria-label='Time Conductor History']").click(); + await page.getByRole('button', { name: 'Time Conductor Settings' }).click(); + await page.getByRole('button', { name: 'Time Conductor History' }).hover({ trial: true }); + await page.getByRole('button', { name: 'Time Conductor History' }).click(); // Validate history item format const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"'); diff --git a/e2e/tests/functional/recentObjects.e2e.spec.js b/e2e/tests/functional/recentObjects.e2e.spec.js index a97c32d88c..da7a2dcb3f 100644 --- a/e2e/tests/functional/recentObjects.e2e.spec.js +++ b/e2e/tests/functional/recentObjects.e2e.spec.js @@ -59,53 +59,60 @@ test.describe('Recent Objects', () => { await page.mouse.move(0, 100); await page.mouse.up(); }); - test('Navigated objects show up in recents, object renames and deletions are reflected', async ({ - page - }) => { - // Verify that both created objects appear in the list and are in the correct order - await assertInitialRecentObjectsListState(); - - // Navigate to the folder by clicking on the main object name in the recent objects list item - await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click(); - await page.waitForURL(`**/${folderA.uuid}?*`); - expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy(); - - // Rename - folderA.name = `${folderA.name}-NEW!`; - await page.locator('.l-browse-bar__object-name').fill(''); - await page.locator('.l-browse-bar__object-name').fill(folderA.name); - await page.keyboard.press('Enter'); - - // Verify rename has been applied in recent objects list item and objects paths - expect( - await page - .getByRole('navigation', { - name: clock.name - }) - .locator('a') - .filter({ - hasText: folderA.name - }) - .count() - ).toBeGreaterThan(0); - expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); - - // Delete - await page.click('button[title="Show selected item in tree"]'); - // Delete the folder via the left tree pane treeitem context menu - await page - .getByRole('treeitem', { name: new RegExp(folderA.name) }) - .locator('a') - .click({ - button: 'right' + test.fixme( + 'Navigated objects show up in recents, object renames and deletions are reflected', + async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6818' }); - await page.getByRole('menuitem', { name: /Remove/ }).click(); - await page.getByRole('button', { name: 'OK' }).click(); - // Verify that the folder and clock are no longer in the recent objects list - await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden(); - await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden(); - }); + // Verify that both created objects appear in the list and are in the correct order + await assertInitialRecentObjectsListState(); + + // Navigate to the folder by clicking on the main object name in the recent objects list item + await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click(); + await page.waitForURL(`**/${folderA.uuid}?*`); + expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy(); + + // Rename + folderA.name = `${folderA.name}-NEW!`; + await page.locator('.l-browse-bar__object-name').fill(''); + await page.locator('.l-browse-bar__object-name').fill(folderA.name); + await page.keyboard.press('Enter'); + + // Verify rename has been applied in recent objects list item and objects paths + expect( + await page + .getByRole('navigation', { + name: clock.name + }) + .locator('a') + .filter({ + hasText: folderA.name + }) + .count() + ).toBeGreaterThan(0); + expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); + + // Delete + await page.click('button[title="Show selected item in tree"]'); + // Delete the folder via the left tree pane treeitem context menu + await page + .getByRole('treeitem', { name: new RegExp(folderA.name) }) + .locator('a') + .click({ + button: 'right' + }); + await page.getByRole('menuitem', { name: /Remove/ }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // Verify that the folder and clock are no longer in the recent objects list + await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden(); + await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden(); + } + ); + test('Clicking on an object in the path of a recent object navigates to the object', async ({ page, openmctConfig diff --git a/e2e/tests/functional/search.e2e.spec.js b/e2e/tests/functional/search.e2e.spec.js index 846f8afda9..3f93814923 100644 --- a/e2e/tests/functional/search.e2e.spec.js +++ b/e2e/tests/functional/search.e2e.spec.js @@ -77,11 +77,11 @@ test.describe('Grand Search', () => { // Click [aria-label="OpenMCT Search"] a >> nth=0 await page.locator('[aria-label="Search Result"] >> nth=0').click(); - await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden(); + await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeInViewport(); // Fill [aria-label="OpenMCT Search"] input[type="search"] await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo'); - await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden(); + await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeInViewport(); // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 await page diff --git a/package.json b/package.json index a9dcf6ff17..41131d921e 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots", "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable", "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb", + "test:e2e:watch": "npx playwright test --ui --config=e2e/playwright-ci.config.js", "test:perf": "npx playwright test --config=e2e/playwright-performance.config.js", "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue", "update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2023/gm'", diff --git a/src/api/menu/MenuAPISpec.js b/src/api/menu/MenuAPISpec.js index 68f6f3b04a..5ff8cb4a01 100644 --- a/src/api/menu/MenuAPISpec.js +++ b/src/api/menu/MenuAPISpec.js @@ -23,6 +23,7 @@ import MenuAPI from './MenuAPI'; import Menu from './menu'; import { createOpenMct, createMouseEvent, resetApplicationState } from '../../utils/testing'; +import Vue from 'vue'; describe('The Menu API', () => { let openmct; @@ -137,14 +138,13 @@ describe('The Menu API', () => { it('invokes the destroy method when menu is dismissed', (done) => { menuOptions.onDestroy = done; - menuAPI.showMenu(x, y, actionsArray, menuOptions); + spyOn(menuAPI, '_clearMenuComponent').and.callThrough(); - const vueComponent = menuAPI.menuComponent.component; - spyOn(vueComponent, '$destroy'); + menuAPI.showMenu(x, y, actionsArray, menuOptions); document.body.click(); - expect(vueComponent.$destroy).toHaveBeenCalled(); + expect(menuAPI._clearMenuComponent).toHaveBeenCalled(); }); it('invokes the onDestroy callback if passed in', (done) => { @@ -185,7 +185,7 @@ describe('The Menu API', () => { superMenuItem.dispatchEvent(mouseOverEvent); const itemDescription = document.querySelector('.l-item-description__description'); - menuAPI.menuComponent.component.$nextTick(() => { + Vue.nextTick(() => { expect(menuElement).not.toBeNull(); expect(itemDescription.innerText).toEqual(actionsArray[0].description); diff --git a/src/api/menu/components/Menu.vue b/src/api/menu/components/Menu.vue index 6c86bd56b2..68b1136b3d 100644 --- a/src/api/menu/components/Menu.vue +++ b/src/api/menu/components/Menu.vue @@ -30,7 +30,6 @@ role="menuitem" :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" :title="action.description" - :data-testid="action.testId || null" @click="action.onItemClicked" > {{ action.name }} @@ -53,7 +52,6 @@ role="menuitem" :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" :title="action.description" - :data-testid="action.testId || null" @click="action.onItemClicked" > {{ action.name }} diff --git a/src/api/menu/components/SuperMenu.vue b/src/api/menu/components/SuperMenu.vue index 6d3b2451a0..808750d8ca 100644 --- a/src/api/menu/components/SuperMenu.vue +++ b/src/api/menu/components/SuperMenu.vue @@ -34,7 +34,6 @@ role="menuitem" :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" :title="action.description" - :data-testid="action.testId || null" @click="action.onItemClicked" @mouseover="toggleItemDescription(action)" @mouseleave="toggleItemDescription()" @@ -59,7 +58,6 @@ role="menuitem" :class="action.cssClass" :title="action.description" - :data-testid="action.testId || null" @click="action.onItemClicked" @mouseover="toggleItemDescription(action)" @mouseleave="toggleItemDescription()" diff --git a/src/api/menu/menu.js b/src/api/menu/menu.js index 18f259cff3..1a829fc4d4 100644 --- a/src/api/menu/menu.js +++ b/src/api/menu/menu.js @@ -52,12 +52,12 @@ class Menu extends EventEmitter { } dismiss() { - this.emit('destroy'); if (this.destroy) { this.destroy(); this.destroy = null; } document.removeEventListener('click', this.dismiss); + this.emit('destroy'); } showMenu() { diff --git a/src/api/objects/InMemorySearchProvider.js b/src/api/objects/InMemorySearchProvider.js index 364e4c7605..15e66a2e79 100644 --- a/src/api/objects/InMemorySearchProvider.js +++ b/src/api/objects/InMemorySearchProvider.js @@ -374,7 +374,7 @@ class InMemorySearchProvider { delete provider.pendingIndex[keyString]; try { - if (domainObject) { + if (domainObject && domainObject.identifier) { await provider.index(domainObject); } } catch (error) { diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js index 1d910ae6b5..b7c56121ae 100644 --- a/src/api/objects/ObjectAPISpec.js +++ b/src/api/objects/ObjectAPISpec.js @@ -23,6 +23,9 @@ describe('The Object API', () => { return USERNAME; } }); + }, + getPossibleRoles() { + return Promise.resolve([]); } }; openmct = createOpenMct(); diff --git a/src/api/overlays/components/OverlayComponent.vue b/src/api/overlays/components/OverlayComponent.vue index e61ae7b0e0..2cae0807d4 100644 --- a/src/api/overlays/components/OverlayComponent.vue +++ b/src/api/overlays/components/OverlayComponent.vue @@ -27,7 +27,7 @@ v-if="dismissable" aria-label="Close" class="c-click-icon c-overlay__close-button icon-x" - @click="destroy" + @click.stop="destroy" > -
+
{{ selectedMode.name }}
diff --git a/src/plugins/timeConductor/ConductorTimeSystem.vue b/src/plugins/timeConductor/ConductorTimeSystem.vue index 4b1876421d..08f66dab86 100644 --- a/src/plugins/timeConductor/ConductorTimeSystem.vue +++ b/src/plugins/timeConductor/ConductorTimeSystem.vue @@ -28,6 +28,7 @@
-
    -
  • +
    +
    {{ day }} -
  • -
-
    -
  • +
+
+
@@ -63,8 +68,8 @@
{{ cell.dayOfYear }}
- - +
+
diff --git a/src/plugins/timeConductor/conductor.scss b/src/plugins/timeConductor/conductor.scss index bfe77f19a3..cb6f4a8b07 100644 --- a/src/plugins/timeConductor/conductor.scss +++ b/src/plugins/timeConductor/conductor.scss @@ -563,6 +563,10 @@ } } } + + .pr-time-input input { + width: 3.5em; // Needed for Firefox + } } .c-compact-tc { diff --git a/src/plugins/timeConductor/date-picker.scss b/src/plugins/timeConductor/date-picker.scss index cd36818e63..b0abb8d270 100644 --- a/src/plugins/timeConductor/date-picker.scss +++ b/src/plugins/timeConductor/date-picker.scss @@ -1,101 +1,107 @@ /******************************************************** PICKER */ .c-datetime-picker { - @include userSelectNone(); - padding: $interiorMarginLg !important; - display: flex !important; // Override .c-menu display: block; - flex-direction: column; - > * + * { - margin-top: $interiorMargin; - } + @include userSelectNone(); + padding: $interiorMarginLg !important; + display: flex !important; // Override .c-menu display: block; + flex-direction: column; - &__close-button { - display: none; // Only show when body.phone, see below. - } + > * + * { + margin-top: $interiorMargin; + } - &__pager { - flex: 0 0 auto; - } + &__close-button { + display: none; // Only show when body.phone, see below. + } - &__calendar { - border-top: 1px solid $colorInteriorBorder; - flex: 1 1 auto; - } + &__pager { + flex: 0 0 auto; + } + + &__calendar { + border-top: 1px solid $colorInteriorBorder; + flex: 1 1 auto; + } } .c-pager { - display: grid; - grid-column-gap: $interiorMargin; - grid-template-rows: 1fr; - grid-template-columns: auto 1fr auto; - align-items: center; + display: grid; + grid-column-gap: $interiorMargin; + grid-template-rows: 1fr; + grid-template-columns: auto 1fr auto; + align-items: center; - .c-icon-button { - font-size: 0.8em; - } + .c-icon-button { + font-size: 0.8em; + } - &__month-year { - text-align: center; - } + &__month-year { + text-align: center; + } } /******************************************************** CALENDAR */ .c-calendar { - display: grid; - grid-template-columns: repeat(7, min-content); - grid-template-rows: auto; - grid-gap: 1px; - height: 100%; + $mutedOpacity: 0.5; + display: grid; + grid-template-columns: repeat(7, min-content); + grid-template-rows: auto; + grid-gap: 1px; - $mutedOpacity: 0.5; - - ul { - display: contents; - &[class*='--header'] { - pointer-events: none; - li { - opacity: $mutedOpacity; - } - } - } - - li { - display: flex; - flex-direction: column; - justify-content: center !important; - padding: $interiorMargin; - - &.is-in-month { - background: $colorMenuElementHilite; + [class*="__row"] { + display: contents; } - &.selected { - background: $colorKey; - color: $colorKeyFg; - } - } + .c-calendar__row--header { + pointer-events: none; - &__day { - &--sub { - opacity: $mutedOpacity; - font-size: 0.8em; + .c-calendar-cell { + opacity: $mutedOpacity; + } + } + + .c-calendar-cell { + display: flex; + flex-direction: column; + align-items: center; + padding: $interiorMargin; + cursor: pointer; + + @include hover { + background: $colorMenuHovBg; + } + + &.is-in-month { + background: $colorMenuElementHilite; + } + + &.selected { + background: $colorKey; + color: $colorKeyFg; + } + } + + &__day { + &--sub { + opacity: $mutedOpacity; + font-size: 0.8em; + } } - } } /******************************************************** MOBILE */ body.phone { - .c-datetime-picker { - &.c-menu { - @include modalFullScreen(); + .c-datetime-picker { + &.c-menu { + @include modalFullScreen(); + } + + &__close-button { + display: flex; + justify-content: flex-end; + } } - &__close-button { - display: flex; - justify-content: flex-end; + .c-calendar { + grid-template-columns: repeat(7, auto); } - } - - .c-calendar { - grid-template-columns: repeat(7, auto); - } } diff --git a/src/plugins/timeConductor/independent/IndependentClock.vue b/src/plugins/timeConductor/independent/IndependentClock.vue index 35e8cd540a..58dd5e71f9 100644 --- a/src/plugins/timeConductor/independent/IndependentClock.vue +++ b/src/plugins/timeConductor/independent/IndependentClock.vue @@ -1,16 +1,24 @@ -/***************************************************************************** * Open MCT Web, -Copyright (c) 2014-2023, United States Government * as represented by the Administrator of the -National Aeronautics and Space * Administration. All rights reserved. * * Open MCT Web 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 Web 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. -*****************************************************************************/ + @@ -37,6 +37,7 @@ export default { return { userName: undefined, role: undefined, + availableRoles: [], loggedIn: false, inputRoleSelection: undefined, roleSelectionDialog: undefined @@ -57,6 +58,7 @@ export default { const user = await this.openmct.user.getCurrentUser(); this.userName = user.getName(); this.role = this.openmct.user.getActiveRole(); + this.availableRoles = await this.openmct.user.getPossibleRoles(); this.loggedIn = this.openmct.user.isLoggedIn(); }, async fetchOrPromptForRole() { @@ -67,15 +69,15 @@ export default { this.promptForRoleSelection(); } else { // only notify the user if they have more than one role available - const allRoles = await this.openmct.user.getPossibleRoles(); - if (allRoles.length > 1) { + this.availableRoles = await this.openmct.user.getPossibleRoles(); + if (this.availableRoles.length > 1) { this.openmct.notifications.info(`You're logged in as role ${activeRole}`); } } }, async promptForRoleSelection() { - const allRoles = await this.openmct.user.getPossibleRoles(); - const selectionOptions = allRoles.map((role) => ({ + this.availableRoles = await this.openmct.user.getPossibleRoles(); + const selectionOptions = this.availableRoles.map((role) => ({ key: role, name: role })); diff --git a/src/plugins/webPage/pluginSpec.js b/src/plugins/webPage/pluginSpec.js index e77fabbe1e..370c3b677c 100644 --- a/src/plugins/webPage/pluginSpec.js +++ b/src/plugins/webPage/pluginSpec.js @@ -27,7 +27,7 @@ function getView(openmct, domainObj, objectPath) { const applicableViews = openmct.objectViews.get(domainObj, objectPath); const webpageView = applicableViews.find((viewProvider) => viewProvider.key === 'webPage'); - return webpageView.view(domainObj); + return webpageView.view(domainObj, [domainObj]); } function destroyView(view) { diff --git a/src/styles/_constants.scss b/src/styles/_constants.scss index 5193256ca2..4530cf3bb8 100755 --- a/src/styles/_constants.scss +++ b/src/styles/_constants.scss @@ -48,7 +48,7 @@ $overlayInnerMargin: 25px; $mainViewPad: 0px; $treeNavArrowD: 20px; $shellMainBrowseBarH: 22px; -$shellTimeConductorH: 55px; +$shellTimeConductorH: 25px; $shellToolBarH: 29px; $fadeTruncateW: 7px; /*************** Items */ diff --git a/src/ui/components/ObjectFrame.vue b/src/ui/components/ObjectFrame.vue index 12485cc460..673326ad00 100644 --- a/src/ui/components/ObjectFrame.vue +++ b/src/ui/components/ObjectFrame.vue @@ -251,13 +251,7 @@ export default { this.widthClass = wClass.trimStart(); }, getViewKey() { - let viewKey = this.$refs.objectView?.viewKey; - - if (this.objectViewKey) { - viewKey = this.objectViewKey; - } - - return viewKey; + return this.$refs.objectView?.viewKey; }, async showToolTip() { const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS; diff --git a/src/ui/inspector/Inspector.vue b/src/ui/inspector/Inspector.vue index 9b9c194388..94c35295da 100644 --- a/src/ui/inspector/Inspector.vue +++ b/src/ui/inspector/Inspector.vue @@ -23,8 +23,8 @@ @@ -48,20 +48,10 @@ export default { }, data() { return { - selection: this.openmct.selection.get(), selectedTab: undefined }; }, - mounted() { - this.openmct.selection.on('change', this.setSelection); - }, - unmounted() { - this.openmct.selection.off('change', this.setSelection); - }, methods: { - setSelection(selection) { - this.selection = selection; - }, selectTab(tab) { this.selectedTab = tab; } diff --git a/src/ui/inspector/InspectorStylesSpec.js b/src/ui/inspector/InspectorStylesSpec.js index 930ac88a54..0bf1513321 100644 --- a/src/ui/inspector/InspectorStylesSpec.js +++ b/src/ui/inspector/InspectorStylesSpec.js @@ -34,7 +34,7 @@ import StylesView from '@/plugins/condition/components/inspector/StylesView.vue' import SavedStylesView from '../../plugins/inspectorViews/styles/SavedStylesView.vue'; import stylesManager from '../../plugins/inspectorViews/styles/StylesManager'; -describe('the inspector', () => { +xdescribe('the inspector', () => { let openmct; let selection; let stylesViewComponent; diff --git a/src/ui/inspector/InspectorTabs.vue b/src/ui/inspector/InspectorTabs.vue index 1fd34e36fb..6f90a538d4 100644 --- a/src/ui/inspector/InspectorTabs.vue +++ b/src/ui/inspector/InspectorTabs.vue @@ -40,21 +40,11 @@ export default { inject: ['openmct'], props: { - selection: { - type: Array, - default: () => { - return []; - } - }, isEditing: { type: Boolean, required: true } }, - selection: { - type: Array, - default: [] - }, data() { return { tabs: [], @@ -69,12 +59,6 @@ export default { } }, watch: { - selection: { - handler() { - this.updateSelection(); - }, - deep: true - }, visibleTabs: { handler() { this.selectDefaultTabIfSelectedNotVisible(); @@ -82,9 +66,16 @@ export default { deep: true } }, + mounted() { + this.updateSelection(); + this.openmct.selection.on('change', this.updateSelection); + }, + unmounted() { + this.openmct.selection.off('change', this.updateSelection); + }, methods: { updateSelection() { - const inspectorViews = this.openmct.inspectorViews.get(this.selection); + const inspectorViews = this.openmct.inspectorViews.get(this.openmct.selection.get()); this.tabs = inspectorViews.map((view) => { return { diff --git a/src/ui/inspector/InspectorViews.vue b/src/ui/inspector/InspectorViews.vue index 9883439f23..f9c39ce784 100644 --- a/src/ui/inspector/InspectorViews.vue +++ b/src/ui/inspector/InspectorViews.vue @@ -31,29 +31,24 @@ export default { selectedTab: { type: Object, default: undefined - }, - selection: { - type: Array, - default: () => { - return []; - } } }, watch: { - selection: { - handler() { - this.updateSelectionViews(); - }, - deep: true - }, selectedTab() { this.clearAndShowViewsForTab(); } }, + mounted() { + this.updateSelectionViews(); + this.openmct.selection.on('change', this.updateSelectionViews); + }, + unmounted() { + this.openmct.selection.off('change', this.updateSelectionViews); + }, methods: { updateSelectionViews(selection) { this.clearViews(); - this.selectedViews = this.openmct.inspectorViews.get(this.selection); + this.selectedViews = this.openmct.inspectorViews.get(this.openmct.selection.get()); this.showViewsForTab(); }, clearViews() { diff --git a/src/ui/layout/BrowseBar.vue b/src/ui/layout/BrowseBar.vue index 38f2294418..1d6ae4e9c3 100644 --- a/src/ui/layout/BrowseBar.vue +++ b/src/ui/layout/BrowseBar.vue @@ -164,7 +164,7 @@ export default { actionCollection: { type: Object, default: () => { - return {}; + return undefined; } } }, @@ -324,12 +324,7 @@ export default { this.openmct.editor.edit(); }, getViewKey() { - let viewKey = this.viewKey; - if (this.objectViewKey) { - viewKey = this.objectViewKey; - } - - return viewKey; + return this.viewKey; }, promptUserandCancelEditing() { let dialog = this.openmct.overlays.dialog({ diff --git a/src/ui/layout/LayoutSpec.js b/src/ui/layout/LayoutSpec.js index c2804d64b6..3470184408 100644 --- a/src/ui/layout/LayoutSpec.js +++ b/src/ui/layout/LayoutSpec.js @@ -24,7 +24,7 @@ import { createOpenMct, resetApplicationState } from 'utils/testing'; import Vue from 'vue'; import Layout from './Layout.vue'; -describe('Open MCT Layout:', () => { +xdescribe('Open MCT Layout:', () => { let openmct; let element; let components; diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue index 50ca03255e..4cbcd7c972 100644 --- a/src/ui/layout/mct-tree.vue +++ b/src/ui/layout/mct-tree.vue @@ -119,7 +119,7 @@ import _ from 'lodash'; import treeItem from './tree-item.vue'; import search from '../components/search.vue'; -import { markRaw } from 'vue'; +import { markRaw, reactive } from 'vue'; const ITEM_BUFFER = 25; const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded'; @@ -263,7 +263,7 @@ export default { } }, async mounted() { - await this.initialize(); + this.initialize(); await this.loadRoot(); this.isLoading = false; @@ -342,7 +342,7 @@ export default { parentItem.objectPath, abortSignal ); - const parentIndex = this.treeItems.indexOf(parentItem); + const parentIndex = this.treeItems.findIndex((item) => item.navigationPath === parentPath); // if it's not loading, it was aborted if (!this.isItemLoading(parentPath) || parentIndex === -1) { @@ -351,7 +351,9 @@ export default { this.endItemLoad(parentPath); - this.treeItems.splice(parentIndex + 1, 0, ...childrenItems); + const newTreeItems = [...this.treeItems]; + newTreeItems.splice(parentIndex + 1, 0, ...childrenItems); + this.treeItems = [...newTreeItems]; if (!this.isTreeItemOpen(parentItem)) { this.openTreeItems.push(parentPath); @@ -377,7 +379,7 @@ export default { return; } - this.treeItems = this.treeItems.filter((item) => { + const newTreeItems = this.treeItems.filter((item) => { const otherPath = item.navigationPath; if (otherPath !== path && this.isTreeItemAChildOf(otherPath, path)) { this.destroyObserverByPath(otherPath); @@ -388,7 +390,10 @@ export default { return true; }); - this.openTreeItems.splice(pathIndex, 1); + this.treeItems = [...newTreeItems]; + const newOpenTreeItems = [...this.openTreeItems]; + newOpenTreeItems.splice(pathIndex, 1); + this.openTreeItems = [...newOpenTreeItems]; this.removeCompositionListenerFor(path); }, closeTreeItem(item) { @@ -632,14 +637,15 @@ export default { let objectPath = [domainObject].concat(parentObjectPath); let navigationPath = this.buildNavigationPath(objectPath); - return { + // Ensure that we create reactive objects for the tree + return reactive({ id: this.openmct.objects.makeKeyString(domainObject.identifier), object: domainObject, leftOffset: (objectPath.length - 1) * TREE_ITEM_INDENT_PX + 'px', isNew, objectPath, navigationPath - }; + }); }, addMutable(mutableDomainObject, parentObjectPath) { const objectPath = [mutableDomainObject].concat(parentObjectPath); @@ -703,11 +709,13 @@ export default { }); // Splice in all of the sorted descendants - this.treeItems.splice( - this.treeItems.indexOf(parentItem) + 1, + const newTreeItems = [...this.treeItems]; + newTreeItems.splice( + newTreeItems.indexOf(parentItem) + 1, sortedTreeItems.length, ...sortedTreeItems ); + this.treeItems = [...newTreeItems]; }, buildNavigationPath(objectPath) { return ( @@ -792,7 +800,9 @@ export default { } const removeIndex = this.getTreeItemIndex(item.navigationPath); - this.treeItems.splice(removeIndex, 1); + const newTreeItems = [...this.treeItems]; + newTreeItems.splice(removeIndex, 1); + this.treeItems = [...newTreeItems]; }, addItemToTreeBefore(addItem, beforeItem) { const addIndex = this.getTreeItemIndex(beforeItem.navigationPath); @@ -805,7 +815,9 @@ export default { this.addItemToTree(addItem, addIndex + 1); }, addItemToTree(addItem, index) { - this.treeItems.splice(index, 0, addItem); + const newTreeItems = [...this.treeItems]; + newTreeItems.splice(index, 0, addItem); + this.treeItems = [...newTreeItems]; if (this.isTreeItemOpen(addItem)) { this.openTreeItem(addItem); diff --git a/src/ui/mixins/context-menu-gesture.js b/src/ui/mixins/context-menu-gesture.js index ef6fadcebb..34b9477035 100644 --- a/src/ui/mixins/context-menu-gesture.js +++ b/src/ui/mixins/context-menu-gesture.js @@ -50,7 +50,7 @@ export default { event.preventDefault(); event.stopPropagation(); - let actionsCollection = this.openmct.actions.getActionsCollection(this.objectPath); + let actionsCollection = this.openmct.actions.getActionsCollection(toRaw(this.objectPath)); let actions = actionsCollection.getVisibleActions(); let sortedActions = this.openmct.actions._groupAndSortActions(actions); diff --git a/src/utils/testing.js b/src/utils/testing.js index ba15ead9d4..2d89acfbff 100644 --- a/src/utils/testing.js +++ b/src/utils/testing.js @@ -21,6 +21,7 @@ *****************************************************************************/ import MCT from 'MCT'; +import { markRaw } from 'vue'; let nativeFunctions = []; let mockObjects = setMockObjects(); @@ -35,7 +36,8 @@ const DEFAULT_TIME_OPTIONS = { }; export function createOpenMct(timeSystemOptions = DEFAULT_TIME_OPTIONS) { - const openmct = new MCT(); + let openmct = new MCT(); + openmct = markRaw(openmct); openmct.install(openmct.plugins.LocalStorage()); openmct.install(openmct.plugins.UTCTimeSystem()); openmct.setAssetPath('/base'); From 7c58b19c3ec007dd1217697d12d5e77c63674cd4 Mon Sep 17 00:00:00 2001 From: Khalid Adil Date: Fri, 28 Jul 2023 00:04:42 -0500 Subject: [PATCH 365/594] Switch staleness provider for SWG to use modeChanged instead of clock (#6845) * Switch staleness provider for SWG to use modeChanged instead of clock --- example/generator/SinewaveStalenessProvider.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/example/generator/SinewaveStalenessProvider.js b/example/generator/SinewaveStalenessProvider.js index 9eda006fe1..8819ae28ca 100644 --- a/example/generator/SinewaveStalenessProvider.js +++ b/example/generator/SinewaveStalenessProvider.js @@ -62,7 +62,7 @@ export default class SinewaveLimitProvider extends EventEmitter { const id = this.#getObjectKeyString(domainObject); if (this.#isRealTime === undefined) { - this.#updateRealTime(this.#openmct.time.clock()); + this.#updateRealTime(this.#openmct.time.getMode()); } this.#handleClockUpdate(); @@ -92,15 +92,15 @@ export default class SinewaveLimitProvider extends EventEmitter { if (observers && !this.#watchingTheClock) { this.#watchingTheClock = true; - this.#openmct.time.on('clock', this.#updateRealTime, this); + this.#openmct.time.on('modeChanged', this.#updateRealTime, this); } else if (!observers && this.#watchingTheClock) { this.#watchingTheClock = false; - this.#openmct.time.off('clock', this.#updateRealTime, this); + this.#openmct.time.off('modeChanged', this.#updateRealTime, this); } } - #updateRealTime(clock) { - this.#isRealTime = clock !== undefined; + #updateRealTime(mode) { + this.#isRealTime = mode !== 'fixed'; if (!this.#isRealTime) { Object.keys(this.#observingStaleness).forEach((id) => { From d4e51cbaf104662bcc3595be5d2fa0c7f0d12be6 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Fri, 28 Jul 2023 09:37:11 -0700 Subject: [PATCH 366/594] Use the current timestamp from the global clock (#6851) * Use the current timestamp from the global clock. Use mode changes to set if the view is fixed time or real time * Reload the page after adding a plan and then change the url params. --- .../functional/planning/timelist.e2e.spec.js | 2 ++ src/plugins/timelist/Timelist.vue | 30 +++++++------------ 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index b5208e909c..65802cf92f 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -98,6 +98,8 @@ test.describe('Time List', () => { const startBound = testPlan.TEST_GROUP[0].start; const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end; + await page.goto(timelist.url); + // Switch to fixed time mode with all plan events within the bounds await page.goto( `${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view` diff --git a/src/plugins/timelist/Timelist.vue b/src/plugins/timelist/Timelist.vue index 5f6c17f014..a87290baff 100644 --- a/src/plugins/timelist/Timelist.vue +++ b/src/plugins/timelist/Timelist.vue @@ -38,6 +38,7 @@ import { getPreciseDuration } from '../../utils/duration'; import { SORT_ORDER_OPTIONS } from './constants'; import _ from 'lodash'; import { v4 as uuid } from 'uuid'; +import { TIME_CONTEXT_EVENTS } from '../../api/time/constants'; const SCROLL_TIMEOUT = 10000; @@ -114,10 +115,8 @@ export default { }, mounted() { this.isEditing = this.openmct.editor.isEditing(); - this.timestamp = this.openmct.time.isRealTime() - ? this.openmct.time.now() - : this.openmct.time.bounds().start; - this.openmct.time.on('clock', this.setViewFromClock); + this.timestamp = this.openmct.time.now(); + this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime); this.getPlanDataAndSetConfig(this.domainObject); @@ -138,7 +137,7 @@ export default { this.status = this.openmct.status.get(this.domainObject.identifier); this.updateTimestamp = _.throttle(this.updateTimestamp, 1000); - this.openmct.time.on('bounds', this.updateTimestamp); + this.openmct.time.on('tick', this.updateTimestamp); this.openmct.editor.on('isEditing', this.setEditState); this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500); @@ -150,7 +149,7 @@ export default { this.composition.load(); } - this.setViewFromClock(this.openmct.time.getClock()); + this.setFixedTime(this.openmct.time.getMode()); }, beforeUnmount() { if (this.unlisten) { @@ -166,8 +165,8 @@ export default { } this.openmct.editor.off('isEditing', this.setEditState); - this.openmct.time.off('bounds', this.updateTimestamp); - this.openmct.time.off('clock', this.setViewFromClock); + this.openmct.time.off('tick', this.updateTimestamp); + this.openmct.time.off(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime); this.$el.parentElement?.removeEventListener('scroll', this.deferAutoScroll, true); if (this.clearAutoScrollDisabledTimer) { @@ -202,22 +201,15 @@ export default { this.listActivities(); } }, - updateTimestamp(bounds, isTick) { - if (isTick === true && this.openmct.time.isRealTime()) { - this.updateTimeStampAndListActivities(this.openmct.time.now()); - } else if (isTick === false && !this.openmct.time.isRealTime()) { - // set the start time for fixed time using the selected bounds start - this.updateTimeStampAndListActivities(bounds.start); - } + updateTimestamp(timestamp) { + //The clock never stops ticking + this.updateTimeStampAndListActivities(timestamp); }, - setViewFromClock(newClock) { + setFixedTime() { this.filterValue = this.domainObject.configuration.filter; this.isFixedTime = !this.openmct.time.isRealTime(); if (this.isFixedTime) { this.hideAll = false; - this.updateTimeStampAndListActivities(this.openmct.time.bounds()?.start); - } else { - this.updateTimeStampAndListActivities(this.openmct.time.now()); } }, addItem(domainObject) { From 3c2b032526638dc9026462f6cecfeaf80867cc42 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Fri, 28 Jul 2023 10:24:48 -0700 Subject: [PATCH 367/594] Plan rendering inside a timestrip (#6852) * Use the width and height of the container of the plan to set the activity widths and now markers * Use the right parent to determine height and width --------- Co-authored-by: Jesse Mazzella --- src/plugins/plan/components/Plan.vue | 9 +++++--- src/plugins/timeline/TimelineViewLayout.vue | 13 +++++++++-- src/ui/components/TimeSystemAxis.vue | 24 +++++++++------------ 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/plugins/plan/components/Plan.vue b/src/plugins/plan/components/Plan.vue index 5a13547012..cf0c0cf617 100644 --- a/src/plugins/plan/components/Plan.vue +++ b/src/plugins/plan/components/Plan.vue @@ -281,7 +281,7 @@ export default { if (!clientWidth) { //this is a hack - need a better way to find the parent of this component - let parent = this.openmct.layout.$refs.browseObject.$el; + let parent = this.getParent(); if (parent) { clientWidth = parent.getBoundingClientRect().width; } @@ -289,12 +289,15 @@ export default { return clientWidth - 200; }, + getParent() { + //this is a hack - need a better way to find the parent of this component + return this.$el.closest('.is-object-type-time-strip'); + }, getClientHeight() { let clientHeight = this.$refs.plan.clientHeight; if (!clientHeight) { - //this is a hack - need a better way to find the parent of this component - let parent = this.openmct.layout.$refs.browseObject.$el; + let parent = this.getParent(); if (parent) { clientHeight = parent.getBoundingClientRect().height; } diff --git a/src/plugins/timeline/TimelineViewLayout.vue b/src/plugins/timeline/TimelineViewLayout.vue index 7183d44333..b22f342d45 100644 --- a/src/plugins/timeline/TimelineViewLayout.vue +++ b/src/plugins/timeline/TimelineViewLayout.vue @@ -52,6 +52,7 @@ import TimelineObjectView from './TimelineObjectView.vue'; import TimelineAxis from '../../ui/components/TimeSystemAxis.vue'; import SwimLane from '@/ui/components/swim-lane/SwimLane.vue'; import { getValidatedData } from '../plan/util'; +import _ from 'lodash'; const unknownObjectType = { definition: { @@ -81,6 +82,7 @@ export default { this.composition.off('remove', this.removeItem); this.composition.off('reorder', this.reorder); this.stopFollowingTimeContext(); + this.contentResizeObserver.disconnect(); }, mounted() { this.items = []; @@ -92,6 +94,10 @@ export default { this.composition.on('reorder', this.reorder); this.composition.load(); } + + this.handleContentResize = _.debounce(this.handleContentResize, 500); + this.contentResizeObserver = new ResizeObserver(this.handleContentResize); + this.contentResizeObserver.observe(this.$refs.timelineHolder); }, methods: { addItem(domainObject) { @@ -132,6 +138,9 @@ export default { this.items[reorderEvent.newIndex] = oldItems[reorderEvent.oldIndex]; }); }, + handleContentResize() { + this.updateContentHeight(); + }, updateContentHeight() { const clientHeight = this.getClientHeight(); if (this.height !== clientHeight) { @@ -139,11 +148,11 @@ export default { } }, getClientHeight() { - let clientHeight = this.$refs.contentHolder.getBoundingClientRect().height; + let clientHeight = this.$refs.timelineHolder.getBoundingClientRect().height; if (!clientHeight) { //this is a hack - need a better way to find the parent of this component - let parent = this.openmct.layout.$refs.browseObject.$el; + let parent = this.$el.closest('.c-object-view'); if (parent) { clientHeight = parent.getBoundingClientRect().height; } diff --git a/src/ui/components/TimeSystemAxis.vue b/src/ui/components/TimeSystemAxis.vue index d0db97ce7f..c3751268a6 100644 --- a/src/ui/components/TimeSystemAxis.vue +++ b/src/ui/components/TimeSystemAxis.vue @@ -78,6 +78,9 @@ export default { }, timeSystem(newTimeSystem) { this.drawAxis(this.bounds, newTimeSystem); + }, + contentHeight() { + this.updateNowMarker(); } }, mounted() { @@ -110,20 +113,13 @@ export default { } }, updateNowMarker() { - if (this.openmct.time.getClock() === undefined) { - let nowMarker = document.querySelector('.nowMarker'); - if (nowMarker) { - nowMarker.classList.add('hidden'); - } - } else { - let nowMarker = document.querySelector('.nowMarker'); - if (nowMarker) { - nowMarker.classList.remove('hidden'); - nowMarker.style.height = this.contentHeight + 'px'; - const nowTimeStamp = this.openmct.time.getClock().currentValue(); - const now = this.xScale(nowTimeStamp); - nowMarker.style.left = now + this.offset + 'px'; - } + let nowMarker = this.$el.querySelector('.nowMarker'); + if (nowMarker) { + nowMarker.classList.remove('hidden'); + nowMarker.style.height = this.contentHeight + 'px'; + const nowTimeStamp = this.openmct.time.now(); + const now = this.xScale(nowTimeStamp); + nowMarker.style.left = now + this.offset + 'px'; } }, setDimensions() { From 194eb4360777dfc9da2fcf06b9abc34d38b069f2 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Fri, 28 Jul 2023 12:35:11 -0700 Subject: [PATCH 368/594] fix(#6854): [LADTableSet] prevent compositions from becoming reactive (#6855) * fix: prevent compositions from becoming reactive --- src/plugins/LADTable/components/LadTableSet.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/LADTable/components/LadTableSet.vue b/src/plugins/LADTable/components/LadTableSet.vue index 4f4fcbf243..a104fbe87a 100644 --- a/src/plugins/LADTable/components/LadTableSet.vue +++ b/src/plugins/LADTable/components/LadTableSet.vue @@ -74,7 +74,6 @@ export default { return { ladTableObjects: [], ladTelemetryObjects: {}, - compositions: [], viewContext: {}, staleObjects: [], configuration: this.ladTableConfiguration.getConfiguration() @@ -115,6 +114,9 @@ export default { return ''; } }, + created() { + this.compositions = []; + }, mounted() { this.ladTableConfiguration.on('change', this.handleConfigurationChange); this.composition = this.openmct.composition.get(this.domainObject); From 3ae14cf78697e90665d440055608554dd32facbd Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Fri, 28 Jul 2023 17:20:06 -0700 Subject: [PATCH 369/594] Revert "[CI] Temporarily disable some tests" (#6853) * Revert "[CI] Temporarily disable some tests (#6806)" This reverts commit 85974fc5f14179dc0b009ed24e28dd3380e88917. * fix(e2e): fix visual tests * refactor: lint:fix * fix: revert localStorage data changes --------- Co-authored-by: Shefali Joshi --- .circleci/config.yml | 4 ++++ e2e/test-data/VisualTestData_storage.json | 12 ++++++------ e2e/tests/visual/addInit.visual.spec.js | 3 ++- e2e/tests/visual/controlledClock.visual.spec.js | 3 ++- e2e/tests/visual/default.visual.spec.js | 5 ++++- e2e/tests/visual/search.visual.spec.js | 3 ++- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9aa1075fd6..16110bae2f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -242,6 +242,10 @@ workflows: name: e2e-stable node-version: lts/hydrogen suite: stable + - perf-test: + node-version: lts/hydrogen + - visual-test: + node-version: lts/hydrogen the-nightly: #These jobs do not run on PRs, but against master at night jobs: diff --git a/e2e/test-data/VisualTestData_storage.json b/e2e/test-data/VisualTestData_storage.json index 017415dce4..02fe3cd82b 100644 --- a/e2e/test-data/VisualTestData_storage.json +++ b/e2e/test-data/VisualTestData_storage.json @@ -5,18 +5,18 @@ "origin": "http://localhost:8080", "localStorage": [ { - "name": "mct", - "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},\"58f55f3a-46d9-4c37-a726-27b5d38b895a\":{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400878,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400878},\"19f2e461-190e-4662-8d62-251e90bb7aac\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433}}" + "name": "tcHistory", + "value": "{\"utc\":[{\"start\":1658617611983,\"end\":1658619411983}]}" }, { - "name": "mct-recent-objects", - "value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"domainObject\":{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436}},{\"objectPath\":[{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433},{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/58f55f3a-46d9-4c37-a726-27b5d38b895a/19f2e461-190e-4662-8d62-251e90bb7aac\",\"domainObject\":{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654}}]" + "name": "mct", + "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619412848,\"modified\":1658619412848},\"7fa5749b-8969-494c-9d85-c272516d333c\":{\"identifier\":{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}}]},\"modified\":1658619413566,\"location\":\"mine\",\"persisted\":1658619413567},\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1658619413552,\"location\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"persisted\":1658619413552}}" }, { "name": "mct-tree-expanded", - "value": "[]" + "value": "[\"/browse/mine\"]" } ] } ] -} \ No newline at end of file +} diff --git a/e2e/tests/visual/addInit.visual.spec.js b/e2e/tests/visual/addInit.visual.spec.js index 007ce9904f..8e8b1e543c 100644 --- a/e2e/tests/visual/addInit.visual.spec.js +++ b/e2e/tests/visual/addInit.visual.spec.js @@ -52,7 +52,8 @@ test.describe('Visual - addInit', () => { path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js') }); //Go to baseURL - await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); + await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); + await page.getByTitle('Collapse Browse Pane').click(); await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME }); diff --git a/e2e/tests/visual/controlledClock.visual.spec.js b/e2e/tests/visual/controlledClock.visual.spec.js index 5a12fdf390..8ecc55bf47 100644 --- a/e2e/tests/visual/controlledClock.visual.spec.js +++ b/e2e/tests/visual/controlledClock.visual.spec.js @@ -41,7 +41,8 @@ test.describe('Visual - Controlled Clock @localStorage', () => { test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => { // Go to baseURL - await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); + await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); + await page.getByTitle('Collapse Browse Pane').click(); await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click(); //Ensure that we're on the Unnamed Overlay Plot object diff --git a/e2e/tests/visual/default.visual.spec.js b/e2e/tests/visual/default.visual.spec.js index e733415567..d0375bc476 100644 --- a/e2e/tests/visual/default.visual.spec.js +++ b/e2e/tests/visual/default.visual.spec.js @@ -39,7 +39,8 @@ const { createDomainObjectWithDefaults } = require('../../appActions'); test.describe('Visual - Default', () => { test.beforeEach(async ({ page }) => { //Go to baseURL and Hide Tree - await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); + await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); + await page.getByTitle('Collapse Browse Pane').click(); }); test.use({ clockOptions: { @@ -99,6 +100,8 @@ test.describe('Visual - Default', () => { let endDate = 'xxxx-01-01 02:00:00.000Z'; endDate = year + endDate.substring(4); + await page.getByRole('button', { name: 'Time Conductor Settings' }).click(); + await page.locator('input[type="text"]').nth(1).fill(endDate.toString()); await page.locator('input[type="text"]').first().fill(startDate.toString()); diff --git a/e2e/tests/visual/search.visual.spec.js b/e2e/tests/visual/search.visual.spec.js index 9d07fe99ca..449e60afdb 100644 --- a/e2e/tests/visual/search.visual.spec.js +++ b/e2e/tests/visual/search.visual.spec.js @@ -32,7 +32,8 @@ const percySnapshot = require('@percy/playwright'); test.describe('Grand Search', () => { test.beforeEach(async ({ page, theme }) => { //Go to baseURL and Hide Tree - await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); + await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); + await page.getByTitle('Collapse Browse Pane').click(); }); test.use({ clockOptions: { From f0ef93dd3f3dcfcd2d90fa57ff1f29109b05aef2 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Mon, 31 Jul 2023 09:57:11 -0700 Subject: [PATCH 370/594] fix: remove `tree-item-destroyed` event (#6856) * fix: remove `tree-item-destroyed` event - Composition listeners should only be removed if the item has been deleted or its parent has been collapsed. Both are handled by `mct-tree` already, so doing this additionally when tree-items unmount is redundant. - In addition to that, any time the `visibleTreeItems` array changes, all tree-items will unmount, so this just doesn't work as intended-- it will unregister all composition listeners whenever the tree changes! * test: stabilize imagery test - Use keyboard gestures to navigate * fix: lint:fix --- .../plugins/imagery/exampleImagery.e2e.spec.js | 18 ++++++++++-------- src/ui/layout/mct-tree.vue | 1 - src/ui/layout/tree-item.vue | 1 - 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index d64688e044..8a9756f3df 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -80,19 +80,21 @@ test.describe('Example Imagery Object', () => { // flip on independent time conductor await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click(); await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click(); - await page.getByRole('textbox', { name: 'Start date' }).click(); await page.getByRole('textbox', { name: 'Start date' }).fill(''); await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30'); - await page.getByRole('textbox', { name: 'Start time' }).click(); + await page.keyboard.press('Tab'); await page.getByRole('textbox', { name: 'Start time' }).fill(''); - await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00'); - await page.getByRole('textbox', { name: 'End date' }).click(); + await page.getByRole('textbox', { name: 'Start time' }).type('01:01:00'); + await page.keyboard.press('Tab'); await page.getByRole('textbox', { name: 'End date' }).fill(''); - await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30'); - await page.getByRole('textbox', { name: 'End time' }).click(); + await page.getByRole('textbox', { name: 'End date' }).type('2021-12-30'); + await page.keyboard.press('Tab'); await page.getByRole('textbox', { name: 'End time' }).fill(''); - await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00'); - await page.getByRole('button', { name: 'Submit time bounds' }).click(); + await page.getByRole('textbox', { name: 'End time' }).type('01:11:00'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + // expect(await page.getByRole('button', { name: 'Submit time bounds' }).isEnabled()).toBe(true); + // await page.getByRole('button', { name: 'Submit time bounds' }).click(); // check image date await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue index 4cbcd7c972..14822fb138 100644 --- a/src/ui/layout/mct-tree.vue +++ b/src/ui/layout/mct-tree.vue @@ -97,7 +97,6 @@ :loading-items="treeItemLoading" :targeted-path="targetedPath" @tree-item-mounted="scrollToCheck($event)" - @tree-item-destroyed="removeCompositionListenerFor($event)" @tree-item-action="treeItemAction(treeItem, $event)" @tree-item-selection="treeItemSelection(treeItem)" @targeted-path-animation-end="targetedPathAnimationEnd()" diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue index 3ed262b55d..3be89f3c4c 100644 --- a/src/ui/layout/tree-item.vue +++ b/src/ui/layout/tree-item.vue @@ -191,7 +191,6 @@ export default { }, unmounted() { this.openmct.router.off('change:path', this.highlightIfNavigated); - this.$emit('tree-item-destroyed', this.navigationPath); }, methods: { targetedPathAnimationEnd($event) { From 50559ac502a753d63ed3793f35415f2b1a60a35b Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Mon, 31 Jul 2023 10:16:52 -0700 Subject: [PATCH 371/594] Don't allow editing line more when not editing display layout (#6858) --- src/plugins/displayLayout/components/LineView.vue | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/displayLayout/components/LineView.vue b/src/plugins/displayLayout/components/LineView.vue index 82377adc79..bed46f7447 100644 --- a/src/plugins/displayLayout/components/LineView.vue +++ b/src/plugins/displayLayout/components/LineView.vue @@ -101,7 +101,11 @@ export default { type: Number, required: true }, - multiSelect: Boolean + multiSelect: Boolean, + isEditing: { + type: Boolean, + required: true + } }, data() { return { @@ -114,7 +118,7 @@ export default { showFrameEdit() { let layoutItem = this.selection.length > 0 && this.selection[0][0].context.layoutItem; - return !this.multiSelect && layoutItem && layoutItem.id === this.item.id; + return this.isEditing && !this.multiSelect && layoutItem && layoutItem.id === this.item.id; }, position() { let { x, y, x2, y2 } = this.item; From f705bf9a618fa20eaaa57fbcd5b11c573616696e Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Mon, 31 Jul 2023 11:15:08 -0700 Subject: [PATCH 372/594] Wait for bounds change to reset telemetry collection data (#6857) * Reset and re-request telemetry only after receiving bounds following a mode change * Don't check for tick - just in case the mode is set without bounds * Use the imagery view timeContext to get related telemetry. --------- Co-authored-by: Khalid Adil --- src/api/telemetry/TelemetryAPI.js | 6 ++- src/api/telemetry/TelemetryCollection.js | 10 ++++- .../imagery/components/ImageryView.vue | 44 ++++++++++--------- .../RelatedTelemetry/RelatedTelemetry.js | 11 ++--- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/api/telemetry/TelemetryAPI.js b/src/api/telemetry/TelemetryAPI.js index 2cb27c6a09..85cf416cd1 100644 --- a/src/api/telemetry/TelemetryAPI.js +++ b/src/api/telemetry/TelemetryAPI.js @@ -204,7 +204,8 @@ export default class TelemetryAPI { */ standardizeRequestOptions(options = {}) { if (!Object.hasOwn(options, 'start')) { - if (options.timeContext?.getBounds()) { + const bounds = options.timeContext?.getBounds(); + if (bounds?.start) { options.start = options.timeContext.getBounds().start; } else { options.start = this.openmct.time.getBounds().start; @@ -212,7 +213,8 @@ export default class TelemetryAPI { } if (!Object.hasOwn(options, 'end')) { - if (options.timeContext?.getBounds()) { + const bounds = options.timeContext?.getBounds(); + if (bounds?.end) { options.end = options.timeContext.getBounds().end; } else { options.end = this.openmct.time.getBounds().end; diff --git a/src/api/telemetry/TelemetryCollection.js b/src/api/telemetry/TelemetryCollection.js index 3b1fd370ce..f7d131edd7 100644 --- a/src/api/telemetry/TelemetryCollection.js +++ b/src/api/telemetry/TelemetryCollection.js @@ -71,6 +71,7 @@ export default class TelemetryCollection extends EventEmitter { this.requestAbort = undefined; this.isStrategyLatest = this.options.strategy === 'latest'; this.dataOutsideTimeBounds = false; + this.modeChanged = false; } /** @@ -306,6 +307,12 @@ export default class TelemetryCollection extends EventEmitter { * @private */ _bounds(bounds, isTick) { + if (this.modeChanged) { + this.modeChanged = false; + this._reset(); + return; + } + let startChanged = this.lastBounds.start !== bounds.start; let endChanged = this.lastBounds.end !== bounds.end; @@ -439,7 +446,8 @@ export default class TelemetryCollection extends EventEmitter { } _timeModeChanged() { - this._reset(); + //We're need this so that when the bounds change comes in after this mode change, we can reset and request historic telemetry + this.modeChanged = true; } /** diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index 5a6657b8fe..2ce2c9026c 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -209,6 +209,7 @@ import ImageControls from './ImageControls.vue'; import ImageThumbnail from './ImageThumbnail.vue'; import imageryData from '../../imagery/mixins/imageryData'; import AnnotationsCanvas from './AnnotationsCanvas.vue'; +import { TIME_CONTEXT_EVENTS } from '../../../api/time/constants'; const REFRESH_CSS_MS = 500; const DURATION_TRACK_MS = 1000; @@ -754,32 +755,28 @@ export default { this.stopFollowingTimeContext(); this.timeContext = this.openmct.time.getContextForView(this.objectPath); //listen - this.timeContext.on('timeSystem', this.timeContextChanged); - this.timeContext.on('clock', this.timeContextChanged); - this.timeContextChanged(); + this.timeContext.on('timeSystem', this.setModeAndTrackDuration); + this.timeContext.on(TIME_CONTEXT_EVENTS.clockChanged, this.setModeAndTrackDuration); + this.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this.setModeAndTrackDuration); + this.setModeAndTrackDuration(); }, stopFollowingTimeContext() { if (this.timeContext) { - this.timeContext.off('timeSystem', this.timeContextChanged); - this.timeContext.off('clock', this.timeContextChanged); + this.timeContext.off('timeSystem', this.setModeAndTrackDuration); + this.timeContext.off(TIME_CONTEXT_EVENTS.clockChanged, this.setModeAndTrackDuration); + this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setModeAndTrackDuration); } }, - timeContextChanged() { + setModeAndTrackDuration() { this.setIsFixed(); this.setCanTrackDuration(); this.trackDuration(); }, setIsFixed() { - this.isFixed = this.timeContext ? this.timeContext.isFixed() : this.openmct.time.isFixed(); + this.isFixed = this.timeContext.isRealTime() === false; }, setCanTrackDuration() { - let isRealTime; - if (this.timeContext) { - isRealTime = this.timeContext.isRealTime(); - } else { - isRealTime = this.openmct.time.isRealTime(); - } - + let isRealTime = this.timeContext.isRealTime(); this.canTrackDuration = isRealTime && this.timeSystem.isUTCBased; }, updateSelection(selection) { @@ -809,13 +806,18 @@ export default { } }, async initializeRelatedTelemetry() { - this.relatedTelemetry = new RelatedTelemetry(this.openmct, this.domainObject, [ - ...this.spacecraftPositionKeys, - ...this.spacecraftOrientationKeys, - ...this.cameraKeys, - ...this.sunKeys, - ...this.transformationsKeys - ]); + this.relatedTelemetry = new RelatedTelemetry( + this.openmct, + this.domainObject, + [ + ...this.spacecraftPositionKeys, + ...this.spacecraftOrientationKeys, + ...this.cameraKeys, + ...this.sunKeys, + ...this.transformationsKeys + ], + this.timeContext + ); if (this.relatedTelemetry.hasRelatedTelemetry) { await this.relatedTelemetry.load(); diff --git a/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js b/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js index 57b0bff4f8..8431c55427 100644 --- a/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js +++ b/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js @@ -30,9 +30,10 @@ function copyRelatedMetadata(metadata) { import IndependentTimeContext from '@/api/time/IndependentTimeContext'; export default class RelatedTelemetry { - constructor(openmct, domainObject, telemetryKeys) { + constructor(openmct, domainObject, telemetryKeys, timeContext) { this._openmct = openmct; this._domainObject = domainObject; + this.timeContext = timeContext; let metadata = this._openmct.telemetry.getMetadata(this._domainObject); let imageHints = metadata.valuesForHints(['image'])[0]; @@ -43,7 +44,7 @@ export default class RelatedTelemetry { this.keys = telemetryKeys; this._timeFormatter = undefined; - this._timeSystemChange(this._openmct.time.timeSystem()); + this._timeSystemChange(this.timeContext.timeSystem()); // grab related telemetry metadata for (let key of this.keys) { @@ -57,7 +58,7 @@ export default class RelatedTelemetry { this._timeSystemChange = this._timeSystemChange.bind(this); this.destroy = this.destroy.bind(this); - this._openmct.time.on('timeSystem', this._timeSystemChange); + this.timeContext.on('timeSystem', this._timeSystemChange); } } @@ -109,7 +110,7 @@ export default class RelatedTelemetry { // and set bounds. ephemeralContext.resetContext(); const newBounds = { - start: this._openmct.time.bounds().start, + start: this.timeContext.bounds().start, end: this._parseTime(datum) }; ephemeralContext.bounds(newBounds); @@ -183,7 +184,7 @@ export default class RelatedTelemetry { } destroy() { - this._openmct.time.off('timeSystem', this._timeSystemChange); + this.timeContext.off('timeSystem', this._timeSystemChange); for (let key of this.keys) { if (this[key] && this[key].unsubscribe) { this[key].unsubscribe(); From 95e686038de836b8f9dd69923f0c0f57956e6f35 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Tue, 1 Aug 2023 14:07:59 -0700 Subject: [PATCH 373/594] fix: toggling markers, alarm markers, marker style + update `Vue.extend()` usage to Vue 3 (#6868) * fix: update to `defineComponent` from `Vue.extend()` * fix: unwrap Proxy arg before WeakMap.get() * refactor: `defineComponent` not needed here --- src/plugins/plot/chart/MctChart.vue | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/plugins/plot/chart/MctChart.vue b/src/plugins/plot/chart/MctChart.vue index b5eec1e203..b40b2b9f1a 100644 --- a/src/plugins/plot/chart/MctChart.vue +++ b/src/plugins/plot/chart/MctChart.vue @@ -42,7 +42,8 @@ import configStore from '../configuration/ConfigStore'; import PlotConfigurationModel from '../configuration/PlotConfigurationModel'; import LimitLine from './LimitLine.vue'; import LimitLabel from './LimitLabel.vue'; -import Vue from 'vue'; +import mount from 'utils/mount'; +import { toRaw } from 'vue'; const MARKER_SIZE = 6.0; const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0; @@ -315,7 +316,7 @@ export default { return; } - const elements = this.seriesElements.get(series); + const elements = this.seriesElements.get(toRaw(series)); elements.lines.forEach(function (line) { this.lines.splice(this.lines.indexOf(line), 1); line.destroy(); @@ -333,7 +334,7 @@ export default { return; } - const elements = this.seriesElements.get(series); + const elements = this.seriesElements.get(toRaw(series)); if (elements.alarmSet) { elements.alarmSet.destroy(); this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1); @@ -349,7 +350,7 @@ export default { return; } - const elements = this.seriesElements.get(series); + const elements = this.seriesElements.get(toRaw(series)); elements.pointSets.forEach(function (pointSet) { this.pointSets.splice(this.pointSets.indexOf(pointSet), 1); pointSet.destroy(); @@ -473,7 +474,7 @@ export default { this.$emit('plotReinitializeCanvas'); }, removeChartElement(series) { - const elements = this.seriesElements.get(series); + const elements = this.seriesElements.get(toRaw(series)); elements.lines.forEach(function (line) { this.lines.splice(this.lines.indexOf(line), 1); @@ -576,7 +577,7 @@ export default { this.seriesLimits.set(series, limitElements); }, clearLimitLines(series) { - const seriesLimits = this.seriesLimits.get(series); + const seriesLimits = this.seriesLimits.get(toRaw(series)); if (seriesLimits) { seriesLimits.limitLines.forEach(function (line) { @@ -747,16 +748,14 @@ export default { left: 0, top: this.drawAPI.y(limit.point.y) }; - let LimitLineClass = Vue.extend(LimitLine); - const component = new LimitLineClass({ - propsData: { + const { vNode } = mount(LimitLine, { + props: { point, limit } }); - component.$mount(); - return component.$el; + return vNode.el; }, getLimitOverlap(limit, overlapMap) { //calculate if limit lines are too close to each other @@ -792,16 +791,14 @@ export default { left: 0, top: this.drawAPI.y(limit.point.y) }; - let LimitLabelClass = Vue.extend(LimitLabel); - const component = new LimitLabelClass({ - propsData: { + const { vNode } = mount(LimitLabel, { + props: { limit: Object.assign({}, overlap, limit), point } }); - component.$mount(); - return component.$el; + return vNode.el; }, drawAlarmPoints(alarmSet) { this.drawAPI.drawLimitPoints( From 04219368747d388f6dc8c4454f1878668f9dcd8a Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Wed, 2 Aug 2023 09:11:41 -0700 Subject: [PATCH 374/594] fix: suppress deprecation warnings to once per unique args (#6875) --- src/api/time/TimeContext.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/api/time/TimeContext.js b/src/api/time/TimeContext.js index 8ff1657696..e18530331f 100644 --- a/src/api/time/TimeContext.js +++ b/src/api/time/TimeContext.js @@ -42,6 +42,7 @@ class TimeContext extends EventEmitter { this.activeClock = undefined; this.offsets = undefined; this.mode = undefined; + this.warnCounts = {}; this.tick = this.tick.bind(this); } @@ -648,6 +649,17 @@ class TimeContext extends EventEmitter { } #warnMethodDeprecated(method, newMethod) { + const MAX_CALLS = 1; // Only warn once per unique method and newMethod combination + + const key = `${method}.${newMethod}`; + const currentWarnCount = this.warnCounts[key] || 0; + + if (currentWarnCount >= MAX_CALLS) { + return; // Don't warn if already warned once + } + + this.warnCounts[key] = currentWarnCount + 1; + let message = `[DEPRECATION WARNING]: The ${method} API method is deprecated and will be removed in a future version of Open MCT.`; if (newMethod) { From c6305697c06acb4521fe6df2dfc8b49d5cc4b103 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Wed, 2 Aug 2023 09:50:03 -0700 Subject: [PATCH 375/594] Set the raw series limits so that we can get the raw series limits (#6877) * Set the raw series limits so that we can get the raw series limits * fix: `toRaw()` the other gets/sets/deletes --------- Co-authored-by: Jesse Mazzella --- src/plugins/plot/chart/MctChart.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/plot/chart/MctChart.vue b/src/plugins/plot/chart/MctChart.vue index b40b2b9f1a..1cc3438aa2 100644 --- a/src/plugins/plot/chart/MctChart.vue +++ b/src/plugins/plot/chart/MctChart.vue @@ -489,7 +489,7 @@ export default { this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1); } - this.seriesElements.delete(series); + this.seriesElements.delete(toRaw(series)); this.clearLimitLines(series); }, @@ -555,7 +555,7 @@ export default { this.alarmSets.push(elements.alarmSet); } - this.seriesElements.set(series, elements); + this.seriesElements.set(toRaw(series), elements); }, makeLimitLines(series) { this.clearLimitLines(series); @@ -574,7 +574,7 @@ export default { this.limitLines.push(limitLine); } - this.seriesLimits.set(series, limitElements); + this.seriesLimits.set(toRaw(series), limitElements); }, clearLimitLines(series) { const seriesLimits = this.seriesLimits.get(toRaw(series)); @@ -585,7 +585,7 @@ export default { line.destroy(); }, this); - this.seriesLimits.delete(series); + this.seriesLimits.delete(toRaw(series)); } }, canDraw(yAxisId) { From 676bb81eab5bd92256852f885c44fe9a23d0e4a5 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Wed, 2 Aug 2023 16:30:51 -0700 Subject: [PATCH 376/594] Synchronize timers between multiple users (#6885) * created a throttle util and using it in timer plugin to throttle refreshing the timer domain object * Simplify timer logic * Clarify code a little * refactor: lint:fix * Fix linting issue --------- Co-authored-by: Jamie V Co-authored-by: Jesse Mazzella Co-authored-by: Jesse Mazzella --- src/plugins/timer/components/Timer.vue | 60 ++++++++------------------ src/utils/throttle.js | 34 +++++++++++++++ 2 files changed, 53 insertions(+), 41 deletions(-) create mode 100644 src/utils/throttle.js diff --git a/src/plugins/timer/components/Timer.vue b/src/plugins/timer/components/Timer.vue index 8a28d89548..ec74cc1a2d 100644 --- a/src/plugins/timer/components/Timer.vue +++ b/src/plugins/timer/components/Timer.vue @@ -43,9 +43,11 @@ - diff --git a/example/simpleVuePlugin/plugin.js b/example/simpleVuePlugin/plugin.js deleted file mode 100644 index 9ff73043f8..0000000000 --- a/example/simpleVuePlugin/plugin.js +++ /dev/null @@ -1,36 +0,0 @@ -import Vue from 'vue'; - -import HelloWorld from './HelloWorld.vue'; - -function SimpleVuePlugin() { - return function install(openmct) { - openmct.types.addType('hello-world', { - name: 'Hello World', - description: 'An introduction object', - creatable: true - }); - openmct.objectViews.addProvider({ - name: 'demo-provider', - key: 'hello-world', - cssClass: 'icon-packet', - canView: function (d) { - return d.type === 'hello-world'; - }, - view: function (domainObject) { - var vm; - - return { - show: function (container) { - vm = new Vue(HelloWorld); - container.appendChild(vm.$mount().$el); - }, - destroy: function (container) { - //vm.$destroy(); - } - }; - } - }); - }; -} - -export default SimpleVuePlugin; diff --git a/package.json b/package.json index 0f18f327b0..7584141d5b 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "sass-loader": "13.3.2", "sinon": "15.1.0", "style-loader": "3.3.3", + "tiny-emitter": "2.1.0", "typescript": "5.2.2", "uuid": "9.0.0", "vue": "3.3.4", @@ -85,8 +86,8 @@ "start": "npx webpack serve --config ./.webpack/webpack.dev.js", "start:prod": "npx webpack serve --config ./.webpack/webpack.prod.js", "start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js", - "lint:js": "eslint example src e2e --ext .js openmct.js --max-warnings=0", - "lint:vue": "eslint example src --ext .vue", + "lint:js": "eslint \"example/**/*.js\" \"src/**/*.js\" \"e2e/**/*.js\" \"openmct.js\" --max-warnings=0", + "lint:vue": "eslint \"src/**/*.vue\"", "lint:spelling": "cspell \"**/*.{js,md,vue}\" --show-context --gitignore --quiet", "lint": "run-p \"lint:js -- {1}\" \"lint:vue -- {1}\" \"lint:spelling -- {1}\" --", "lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix", diff --git a/src/MCT.js b/src/MCT.js index 199e2f61fc..9fa755d7a3 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -33,7 +33,7 @@ define([ './ui/registries/ToolbarRegistry', './ui/router/ApplicationRouter', './ui/router/Browse', - './ui/layout/Layout.vue', + './ui/layout/AppLayout.vue', './ui/preview/plugin', './api/Branding', './plugins/licenses/plugin', diff --git a/src/api/forms/FormController.js b/src/api/forms/FormController.js index eb608e0f7e..4c946573c9 100644 --- a/src/api/forms/FormController.js +++ b/src/api/forms/FormController.js @@ -3,9 +3,9 @@ import mount from 'utils/mount'; import AutoCompleteField from './components/controls/AutoCompleteField.vue'; import CheckBoxField from './components/controls/CheckBoxField.vue'; import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue'; -import Datetime from './components/controls/Datetime.vue'; +import Datetime from './components/controls/DatetimeField.vue'; import FileInput from './components/controls/FileInput.vue'; -import Locator from './components/controls/Locator.vue'; +import Locator from './components/controls/LocatorField.vue'; import NumberField from './components/controls/NumberField.vue'; import SelectField from './components/controls/SelectField.vue'; import TextAreaField from './components/controls/TextAreaField.vue'; @@ -87,7 +87,7 @@ export default class FormControl { onChange }; }, - template: `` + template: `` }, { element, diff --git a/src/api/forms/FormsAPI.js b/src/api/forms/FormsAPI.js index 367aca18a1..0564a2cc1c 100644 --- a/src/api/forms/FormsAPI.js +++ b/src/api/forms/FormsAPI.js @@ -171,7 +171,7 @@ export default class FormsAPI { }; }, template: - '' + '' }, { element, diff --git a/src/api/forms/components/FormProperties.vue b/src/api/forms/components/FormProperties.vue index b4c0676b45..71fc0cb33f 100644 --- a/src/api/forms/components/FormProperties.vue +++ b/src/api/forms/components/FormProperties.vue @@ -44,7 +44,7 @@ :css-class="row.cssClass" :first="index < 1" :row="row" - @onChange="onChange" + @on-change="onChange" /> @@ -94,6 +94,7 @@ export default { } } }, + emits: ['on-change', 'on-save', 'on-cancel'], data() { return { invalidProperties: {}, @@ -144,13 +145,13 @@ export default { onChange(data) { this.invalidProperties[data.model.key] = data.invalid; - this.$emit('onChange', data); + this.$emit('on-change', data); }, onCancel() { - this.$emit('onCancel'); + this.$emit('on-cancel'); }, onSave() { - this.$emit('onSave'); + this.$emit('on-save'); } } }; diff --git a/src/api/forms/components/FormRow.vue b/src/api/forms/components/FormRow.vue index ccff26a7fb..1cf2496825 100644 --- a/src/api/forms/components/FormRow.vue +++ b/src/api/forms/components/FormRow.vue @@ -21,7 +21,7 @@ --> @@ -46,12 +46,13 @@ export default { required: true } }, + emits: ['on-change'], mounted() { this.model.items.forEach((item, index) => (item.key = `${this.model.key}.${index}`)); }, methods: { onChange(data) { - this.$emit('onChange', data); + this.$emit('on-change', data); } } }; diff --git a/src/api/forms/components/controls/CompositeItem.vue b/src/api/forms/components/controls/CompositeItem.vue index 42ab07d025..c94b26d07e 100644 --- a/src/api/forms/components/controls/CompositeItem.vue +++ b/src/api/forms/components/controls/CompositeItem.vue @@ -22,7 +22,7 @@
{ 'setPollQuestion', 'getPollQuestion', 'getCurrentUser', + 'getPossibleRoles', 'getPossibleStatuses', 'getAllStatusRoles', 'canSetPollQuestion', @@ -42,6 +43,7 @@ describe('The User Status API', () => { mockUser = new openmct.user.User('test-user', 'A test user'); userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser)); userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([])); + userProvider.getPossibleRoles.and.returnValue(Promise.resolve([])); userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([])); userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false)); userProvider.isLoggedIn.and.returnValue(true); diff --git a/src/plugins/LADTable/components/LADTableConfiguration.vue b/src/plugins/LADTable/components/LADTableConfiguration.vue index 699e99c9be..e13d9d5ecf 100644 --- a/src/plugins/LADTable/components/LADTableConfiguration.vue +++ b/src/plugins/LADTable/components/LADTableConfiguration.vue @@ -151,7 +151,7 @@ export default { ); const ladTable = this.ladTableObjects[index]; - this.$delete(this.ladTelemetryObjects, ladTable.key); + delete this.ladTelemetryObjects[ladTable.key]; this.ladTableObjects.splice(index, 1); this.shouldShowUnitsCheckbox(); @@ -224,7 +224,7 @@ export default { } if (!showUnitsCheckbox && this.headers?.units) { - this.$delete(this.headers, 'units'); + delete this.headers.units; } }, metadataHasUnits(domainObject) { diff --git a/src/plugins/LADTable/components/LadTableSet.vue b/src/plugins/LADTable/components/LadTableSet.vue index b12f48e9e6..4f4fcbf243 100644 --- a/src/plugins/LADTable/components/LadTableSet.vue +++ b/src/plugins/LADTable/components/LadTableSet.vue @@ -178,7 +178,7 @@ export default { this.unwatchStaleness(combinedKey); }); - this.$delete(this.ladTelemetryObjects, ladTable.key); + delete this.ladTelemetryObjects[ladTable.key]; this.ladTableObjects.splice(index, 1); }, reorderLadTables(reorderPlan) { diff --git a/src/plugins/LADTable/pluginSpec.js b/src/plugins/LADTable/pluginSpec.js index 3d8e674cbe..fa71e9289b 100644 --- a/src/plugins/LADTable/pluginSpec.js +++ b/src/plugins/LADTable/pluginSpec.js @@ -75,6 +75,7 @@ describe('The LAD Table', () => { child = document.createElement('div'); parent.appendChild(child); + openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false); spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); ladPlugin = new LadPlugin(); diff --git a/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js index 67ce2077f0..8fd8440ff5 100644 --- a/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js +++ b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js @@ -21,7 +21,7 @@ *****************************************************************************/ import { createOpenMct, resetApplicationState } from 'utils/testing'; -describe('The URLTimeSettingsSynchronizer', () => { +xdescribe('The URLTimeSettingsSynchronizer', () => { let appHolder; let openmct; let resolveFunction; diff --git a/src/plugins/autoflow/AutoflowTabularPluginSpec.js b/src/plugins/autoflow/AutoflowTabularPluginSpec.js index 693702b713..0c0bfd3004 100644 --- a/src/plugins/autoflow/AutoflowTabularPluginSpec.js +++ b/src/plugins/autoflow/AutoflowTabularPluginSpec.js @@ -171,7 +171,7 @@ xdescribe('AutoflowTabularPlugin', () => { return [{ hint: hints[0] }]; }); - view = provider.view(testObject); + view = provider.view(testObject, [testObject]); view.show(testContainer); return Vue.nextTick(); diff --git a/src/plugins/charts/bar/inspector/BarGraphOptions.vue b/src/plugins/charts/bar/inspector/BarGraphOptions.vue index 91e941fbcd..c620b02709 100644 --- a/src/plugins/charts/bar/inspector/BarGraphOptions.vue +++ b/src/plugins/charts/bar/inspector/BarGraphOptions.vue @@ -200,7 +200,7 @@ export default { this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier) ); if (index >= 0) { - this.$delete(this.plotSeries, index); + this.plotSeries.splice(index, 1); this.setupOptions(); } }, diff --git a/src/plugins/charts/bar/pluginSpec.js b/src/plugins/charts/bar/pluginSpec.js index b496b8bce3..74b956285c 100644 --- a/src/plugins/charts/bar/pluginSpec.js +++ b/src/plugins/charts/bar/pluginSpec.js @@ -23,7 +23,7 @@ import { createOpenMct, resetApplicationState } from 'utils/testing'; import Vue from 'vue'; import BarGraphPlugin from './plugin'; -import BarGraph from './BarGraphPlot.vue'; +// import BarGraph from './BarGraphPlot.vue'; import EventEmitter from 'EventEmitter'; import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY } from './BarGraphConstants'; @@ -125,7 +125,6 @@ describe('the plugin', function () { describe('The bar graph view', () => { let barGraphObject; // eslint-disable-next-line no-unused-vars - let component; let mockComposition; beforeEach(async () => { @@ -153,21 +152,6 @@ describe('the plugin', function () { 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: '' - }); - await Vue.nextTick(); }); @@ -179,7 +163,7 @@ describe('the plugin', function () { expect(plotViewProvider).toBeDefined(); }); - it('Renders plotly bar graph', () => { + xit('Renders plotly bar graph', () => { let barChartElement = element.querySelectorAll('.plotly'); expect(barChartElement.length).toBe(1); }); @@ -236,10 +220,9 @@ describe('the plugin', function () { }); }); - describe('The spectral plot view for telemetry objects with array values', () => { + xdescribe('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 () => { @@ -270,21 +253,6 @@ describe('the plugin', function () { 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: '' - }); - await Vue.nextTick(); }); diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue index b57d590022..b92e3b6eee 100644 --- a/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue +++ b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue @@ -112,7 +112,7 @@ export default { const foundSeries = seriesIndex > -1; if (foundSeries) { - this.$delete(this.plotSeries, seriesIndex); + this.plotSeries.splice(seriesIndex, 1); this.setAxesLabels(); } }, diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue b/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue index cd21603c07..1fb7d7cd02 100644 --- a/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue +++ b/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue @@ -143,7 +143,7 @@ export default { this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier) ); if (index >= 0) { - this.$delete(this.plotSeries, index); + this.plotSeries.splice(index, 1); this.setupOptions(); } }, diff --git a/src/plugins/charts/scatter/pluginSpec.js b/src/plugins/charts/scatter/pluginSpec.js index 7cd3b5f0df..20b5f5c6c8 100644 --- a/src/plugins/charts/scatter/pluginSpec.js +++ b/src/plugins/charts/scatter/pluginSpec.js @@ -23,7 +23,6 @@ import { createOpenMct, resetApplicationState } from 'utils/testing'; import Vue from 'vue'; import ScatterPlotPlugin from './plugin'; -import ScatterPlot from './ScatterPlotView.vue'; import EventEmitter from 'EventEmitter'; import { SCATTER_PLOT_VIEW, SCATTER_PLOT_KEY } from './scatterPlotConstants'; @@ -118,7 +117,6 @@ describe('the plugin', function () { let testDomainObject; let scatterPlotObject; // eslint-disable-next-line no-unused-vars - let component; let mockComposition; beforeEach(async () => { @@ -179,21 +177,6 @@ describe('the plugin', function () { spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - ScatterPlot - }, - provide: { - openmct: openmct, - domainObject: scatterPlotObject, - composition: openmct.composition.get(scatterPlotObject) - }, - template: '' - }); - await Vue.nextTick(); }); @@ -205,7 +188,7 @@ describe('the plugin', function () { expect(plotViewProvider).toBeDefined(); }); - it('Renders plotly scatter plot', () => { + xit('Renders plotly scatter plot', () => { let scatterPlotElement = element.querySelectorAll('.plotly'); expect(scatterPlotElement.length).toBe(1); }); diff --git a/src/plugins/clock/components/ClockIndicator.vue b/src/plugins/clock/components/ClockIndicator.vue index ff4f2cd9f9..2a79763356 100644 --- a/src/plugins/clock/components/ClockIndicator.vue +++ b/src/plugins/clock/components/ClockIndicator.vue @@ -42,7 +42,7 @@ export default { }, data() { return { - timeTextValue: this.openmct.time.now() + timeTextValue: this.openmct.time.getClock() ? this.openmct.time.now() : undefined }; }, mounted() { diff --git a/src/plugins/clock/pluginSpec.js b/src/plugins/clock/pluginSpec.js index 3c3cdfbdf2..a0302719fa 100644 --- a/src/plugins/clock/pluginSpec.js +++ b/src/plugins/clock/pluginSpec.js @@ -22,6 +22,7 @@ import { createOpenMct, resetApplicationState } from 'utils/testing'; import clockPlugin from './plugin'; +import EventEmitter from 'EventEmitter'; import Vue from 'vue'; @@ -70,6 +71,7 @@ describe('Clock plugin:', () => { let clockView; let clockViewObject; let mutableClockObject; + let mockComposition; beforeEach(async () => { await setupClock(true); @@ -85,6 +87,13 @@ describe('Clock plugin:', () => { } }; + mockComposition = new EventEmitter(); + // eslint-disable-next-line require-await + mockComposition.load = async () => { + return []; + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject)); spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); spyOn(openmct.objects, 'supportsMutation').and.returnValue(true); diff --git a/src/plugins/conditionWidget/pluginSpec.js b/src/plugins/conditionWidget/pluginSpec.js index d1291cf22d..39fe88f8b3 100644 --- a/src/plugins/conditionWidget/pluginSpec.js +++ b/src/plugins/conditionWidget/pluginSpec.js @@ -186,7 +186,9 @@ describe('the plugin', function () { await Vue.nextTick(); const domainUrl = mockConditionObject[CONDITION_WIDGET_KEY].url; - expect(urlParent.innerHTML).toContain(` viewProvider.key === 'layout.view' ); - let view = displayLayoutViewProvider.view(testViewObject); + let view = displayLayoutViewProvider.view(testViewObject, [testViewObject]); let error; try { @@ -159,7 +159,7 @@ describe('the plugin', function () { const displayLayoutViewProvider = applicableViews.find( (viewProvider) => viewProvider.key === 'layout.view' ); - const view = displayLayoutViewProvider.view(displayLayoutItem); + const view = displayLayoutViewProvider.view(displayLayoutItem, displayLayoutItem); view.show(child, false); Vue.nextTick(done); diff --git a/src/plugins/faultManagement/FaultManagementListView.vue b/src/plugins/faultManagement/FaultManagementListView.vue index ed5b45106c..f3b0b2b1f7 100644 --- a/src/plugins/faultManagement/FaultManagementListView.vue +++ b/src/plugins/faultManagement/FaultManagementListView.vue @@ -169,7 +169,7 @@ export default { if (selected) { this.selectedFaults[fault.id] = fault; } else { - this.$delete(this.selectedFaults, fault.id); + delete this.selectedFaults[fault.id]; } const selectedFaults = Object.values(this.selectedFaults); diff --git a/src/plugins/filters/components/FiltersView.vue b/src/plugins/filters/components/FiltersView.vue index e7dfd4c8a3..8732129041 100644 --- a/src/plugins/filters/components/FiltersView.vue +++ b/src/plugins/filters/components/FiltersView.vue @@ -173,14 +173,14 @@ export default { if (globalFiltersToRemove.length > 0) { globalFiltersToRemove.forEach((key) => { - this.$delete(this.globalFilters, key); - this.$delete(this.globalMetadata, key); + delete this.globalFilters[key]; + delete this.globalMetadata[key]; }); this.mutateConfigurationGlobalFilters(); } - this.$delete(this.children, keyString); - this.$delete(this.persistedFilters, keyString); + delete this.children[keyString]; + delete this.persistedFilters[keyString]; this.mutateConfigurationFilters(); }, getGlobalFiltersToRemove(keyString) { diff --git a/src/plugins/flexibleLayout/pluginSpec.js b/src/plugins/flexibleLayout/pluginSpec.js index 1b659b664e..c6db935bc4 100644 --- a/src/plugins/flexibleLayout/pluginSpec.js +++ b/src/plugins/flexibleLayout/pluginSpec.js @@ -23,12 +23,15 @@ import { createOpenMct, resetApplicationState } from 'utils/testing'; import FlexibleLayout from './plugin'; import Vue from 'vue'; +import EventEmitter from 'EventEmitter'; describe('the plugin', function () { let element; let child; let openmct; let flexibleLayoutDefinition; + let mockComposition; + const testViewObject = { id: 'test-object', type: 'flexible-layout', @@ -75,7 +78,15 @@ describe('the plugin', function () { let flexibleLayoutViewProvider; beforeEach(() => { - const applicableViews = openmct.objectViews.get(testViewObject, []); + mockComposition = new EventEmitter(); + // eslint-disable-next-line require-await + mockComposition.load = async () => { + return []; + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); flexibleLayoutViewProvider = applicableViews.find( (viewProvider) => viewProvider.key === 'flexible-layout' ); @@ -86,11 +97,12 @@ describe('the plugin', function () { }); it('renders a view', async () => { - const flexibleView = flexibleLayoutViewProvider.view(testViewObject, []); + const flexibleView = flexibleLayoutViewProvider.view(testViewObject, [testViewObject]); flexibleView.show(child, false); await Vue.nextTick(); - const flexTitle = child.querySelector('.l-browse-bar .c-object-label__name'); + console.log(child); + const flexTitle = child.querySelector('.c-fl'); expect(flexTitle).not.toBeNull(); }); diff --git a/src/plugins/gauge/GaugePluginSpec.js b/src/plugins/gauge/GaugePluginSpec.js index 36c4a72e10..e34df1d68c 100644 --- a/src/plugins/gauge/GaugePluginSpec.js +++ b/src/plugins/gauge/GaugePluginSpec.js @@ -172,7 +172,7 @@ describe('Gauge plugin', () => { return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { mutablegaugeObject = mutableObject; - gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView.show(child); return Vue.nextTick(); @@ -314,7 +314,7 @@ describe('Gauge plugin', () => { return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { mutablegaugeObject = mutableObject; - gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView.show(child); return Vue.nextTick(); @@ -456,7 +456,7 @@ describe('Gauge plugin', () => { return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { mutablegaugeObject = mutableObject; - gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView.show(child); return Vue.nextTick(); @@ -560,7 +560,7 @@ describe('Gauge plugin', () => { return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { mutablegaugeObject = mutableObject; - gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView.show(child); return Vue.nextTick(); @@ -643,7 +643,7 @@ describe('Gauge plugin', () => { return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { mutablegaugeObject = mutableObject; - gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView.show(child); return Vue.nextTick(); @@ -771,7 +771,7 @@ describe('Gauge plugin', () => { return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { mutablegaugeObject = mutableObject; - gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView.show(child); return Vue.nextTick(); diff --git a/src/plugins/hyperlink/pluginSpec.js b/src/plugins/hyperlink/pluginSpec.js index 4e1b26f076..1b149fc285 100644 --- a/src/plugins/hyperlink/pluginSpec.js +++ b/src/plugins/hyperlink/pluginSpec.js @@ -29,7 +29,7 @@ function getView(openmct, domainObj, objectPath) { (viewProvider) => viewProvider.key === 'hyperlink.view' ); - return hyperLinkView.view(domainObj); + return hyperLinkView.view(domainObj, [domainObj]); } function destroyView(view) { diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index 2ca9edb343..5a6657b8fe 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -1109,7 +1109,7 @@ export default { window.clearInterval(this.durationTracker); }, updateDuration() { - let currentTime = this.timeContext.getClock().currentValue(); + let currentTime = this.timeContext.isRealTime() ? this.timeContext.now() : undefined; if (currentTime === undefined) { this.numericDuration = currentTime; } else if (Number.isInteger(this.parsedSelectedTime)) { diff --git a/src/plugins/imagery/pluginSpec.js b/src/plugins/imagery/pluginSpec.js index 1415d92fd6..5748b04a17 100644 --- a/src/plugins/imagery/pluginSpec.js +++ b/src/plugins/imagery/pluginSpec.js @@ -60,7 +60,6 @@ function isNew(doc) { function generateTelemetry(start, count) { let telemetry = []; - for (let i = 1, l = count + 1; i < l; i++) { let stringRep = i + 'minute'; let logo = 'images/logo-openmct.svg'; @@ -211,7 +210,6 @@ describe('The Imagery View Layouts', () => { disconnect() {} }); - //spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(imageryObject)); originalRouterPath = openmct.router.path; @@ -401,18 +399,22 @@ describe('The Imagery View Layouts', () => { it('on mount should show the the most recent image', async () => { //Looks like we need Vue.nextTick here so that computed properties settle down await Vue.nextTick(); + await Vue.nextTick(); + await Vue.nextTick(); const imageInfo = getImageInfo(parent); expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); }); - it('on mount should show the any image layers', async () => { + it('on mount should show any image layers', async () => { //Looks like we need Vue.nextTick here so that computed properties settle down await Vue.nextTick(); + await Vue.nextTick(); const layerEls = parent.querySelectorAll('.js-layer-image'); expect(layerEls.length).toEqual(1); }); it('should use the image thumbnailUrl for thumbnails', async () => { + await Vue.nextTick(); await Vue.nextTick(); const fullSizeImageUrl = imageTelemetry[5].url; const thumbnailUrl = formatThumbnail(imageTelemetry[5].url); @@ -433,6 +435,7 @@ describe('The Imagery View Layouts', () => { it('should show the clicked thumbnail as the main image', async () => { //Looks like we need Vue.nextTick here so that computed properties settle down await Vue.nextTick(); + await Vue.nextTick(); const thumbnailUrl = formatThumbnail(imageTelemetry[5].url); parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click(); await Vue.nextTick(); @@ -458,6 +461,7 @@ describe('The Imagery View Layouts', () => { }); it('should show that an image is not new', async () => { + await Vue.nextTick(); await Vue.nextTick(); const target = formatThumbnail(imageTelemetry[4].url); parent.querySelectorAll(`img[src='${target}']`)[0].click(); @@ -469,6 +473,7 @@ describe('The Imagery View Layouts', () => { }); it('should navigate via arrow keys', async () => { + await Vue.nextTick(); await Vue.nextTick(); const keyOpts = { element: parent.querySelector('.c-imagery'), @@ -485,6 +490,7 @@ describe('The Imagery View Layouts', () => { }); it('should navigate via numerous arrow keys', async () => { + await Vue.nextTick(); await Vue.nextTick(); const element = parent.querySelector('.c-imagery'); const type = 'keyup'; @@ -580,6 +586,7 @@ describe('The Imagery View Layouts', () => { }); it('should display the viewable area when zoom factor is greater than 1', async () => { + await Vue.nextTick(); await Vue.nextTick(); expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0); @@ -688,31 +695,28 @@ describe('The Imagery View Layouts', () => { openmct.time.setClock('local'); }); - it('on mount should show imagery within the given bounds', (done) => { - Vue.nextTick(() => { - const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper'); - expect(imageElements.length).toEqual(5); - done(); - }); + it('on mount should show imagery within the given bounds', async () => { + await Vue.nextTick(); + await Vue.nextTick(); + const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper'); + expect(imageElements.length).toEqual(5); }); - it('should show the clicked thumbnail as the preview image', (done) => { - Vue.nextTick(() => { - const mouseDownEvent = createMouseEvent('mousedown'); - let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`); - imageWrapper[2].dispatchEvent(mouseDownEvent); - Vue.nextTick(() => { - const timestamp = imageWrapper[2].id.replace('wrapper-', ''); - expect(componentView.previewAction.invoke).toHaveBeenCalledWith( - [componentView.objectPath[0]], - { - timestamp: Number(timestamp), - objectPath: componentView.objectPath - } - ); - done(); - }); - }); + it('should show the clicked thumbnail as the preview image', async () => { + await Vue.nextTick(); + await Vue.nextTick(); + const mouseDownEvent = createMouseEvent('mousedown'); + let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`); + imageWrapper[2].dispatchEvent(mouseDownEvent); + await Vue.nextTick(); + const timestamp = imageWrapper[2].id.replace('wrapper-', ''); + expect(componentView.previewAction.invoke).toHaveBeenCalledWith( + [componentView.objectPath[0]], + { + timestamp: Number(timestamp), + objectPath: componentView.objectPath + } + ); }); it('should remove images when clock advances', async () => { diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index 37a038ebf5..30a21ed3fa 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -476,7 +476,6 @@ export default { { label: 'Lock Page', callback: () => { - let sections = this.getSections(); this.selectedPage.isLocked = true; // cant be default if it's locked @@ -488,7 +487,12 @@ export default { this.selectedSection.isLocked = true; } - mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections); + mutateObject( + this.openmct, + this.domainObject, + 'configuration.sections', + this.sections + ); if (!this.domainObject.locked) { mutateObject(this.openmct, this.domainObject, 'locked', true); @@ -708,9 +712,6 @@ export default { getSection(id) { return this.sections.find((s) => s.id === id); }, - getSections() { - return this.domainObject.configuration.sections || []; - }, getSearchResults() { if (!this.search.length) { return []; diff --git a/src/plugins/notebook/components/NotebookEmbed.vue b/src/plugins/notebook/components/NotebookEmbed.vue index 1a69ac9df0..a11d56487d 100644 --- a/src/plugins/notebook/components/NotebookEmbed.vue +++ b/src/plugins/notebook/components/NotebookEmbed.vue @@ -106,9 +106,8 @@ export default { watch: { isLocked(value) { if (value === true) { - let index = this.menuActions.findIndex((item) => item.id === 'removeEmbed'); - - this.$delete(this.menuActions, index); + const index = this.menuActions.findIndex((item) => item.id === 'removeEmbed'); + this.menuActions.splice(index, 1); } } }, @@ -140,7 +139,7 @@ export default { onItemClicked: () => this.openSnapshot() }; - this.menuActions = [viewSnapshot]; + this.menuActions.splice(0, this.menuActions.length, viewSnapshot); } const navigateToItem = { @@ -167,7 +166,7 @@ export default { onItemClicked: () => this.previewEmbed() }; - this.menuActions = this.menuActions.concat([quickView, navigateToItem, navigateToItemInTime]); + this.menuActions.push(...[quickView, navigateToItem, navigateToItemInTime]); if (!this.isLocked) { const removeEmbed = { diff --git a/src/plugins/notebook/pluginSpec.js b/src/plugins/notebook/pluginSpec.js index 5bcc5cf1f1..0c63b5c0b5 100644 --- a/src/plugins/notebook/pluginSpec.js +++ b/src/plugins/notebook/pluginSpec.js @@ -185,7 +185,7 @@ describe('Notebook plugin:', () => { mutableNotebookObject = mutableObject; objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1]; - notebookView = notebookViewProvider.view(mutableNotebookObject); + notebookView = notebookViewProvider.view(mutableNotebookObject, [mutableNotebookObject]); notebookView.show(child); await Vue.nextTick(); @@ -267,7 +267,7 @@ describe('Notebook plugin:', () => { }); }); - it('updates the notebook when a user adds a page', () => { + xit('updates the notebook when a user adds a page', async () => { const newPage = { id: 'test-page-4', isDefault: false, @@ -280,22 +280,20 @@ describe('Notebook plugin:', () => { objectCloneToSyncFrom.configuration.sections[0].pages.push(newPage); objectProviderObserver(objectCloneToSyncFrom); - return Vue.nextTick().then(() => { - expect(allNotebookPageElements().length).toBe(3); - }); + await Vue.nextTick(); + expect(allNotebookPageElements().length).toBe(3); }); - it('updates the notebook when a user removes a page', () => { + xit('updates the notebook when a user removes a page', async () => { expect(allNotebookPageElements().length).toBe(2); objectCloneToSyncFrom.configuration.sections[0].pages.splice(0, 1); objectProviderObserver(objectCloneToSyncFrom); - return Vue.nextTick().then(() => { - expect(allNotebookPageElements().length).toBe(1); - }); + await Vue.nextTick(); + expect(allNotebookPageElements().length).toBe(1); }); - it('updates the notebook when a user adds a section', () => { + xit('updates the notebook when a user adds a section', () => { const newSection = { id: 'test-section-3', isDefault: false, @@ -321,7 +319,7 @@ describe('Notebook plugin:', () => { }); }); - it('updates the notebook when a user removes a section', () => { + xit('updates the notebook when a user removes a section', () => { expect(allNotebookSectionElements().length).toBe(2); objectCloneToSyncFrom.configuration.sections.splice(0, 1); objectProviderObserver(objectCloneToSyncFrom); diff --git a/src/plugins/notebook/utils/notebook-entriesSpec.js b/src/plugins/notebook/utils/notebook-entriesSpec.js index b0a23f085b..1d33cf24da 100644 --- a/src/plugins/notebook/utils/notebook-entriesSpec.js +++ b/src/plugins/notebook/utils/notebook-entriesSpec.js @@ -99,6 +99,7 @@ let openmct; describe('Notebook Entries:', () => { beforeEach(() => { openmct = createOpenMct(); + openmct.time.setClock('local'); openmct.types.addType('notebook', { creatable: true }); @@ -216,7 +217,6 @@ describe('Notebook Entries:', () => { it('deleteNotebookEntries deletes correct page entries', async () => { await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); - NotebookEntries.deleteNotebookEntries( openmct, notebookDomainObject, diff --git a/src/plugins/notificationIndicator/components/NotificationIndicator.vue b/src/plugins/notificationIndicator/components/NotificationIndicator.vue index a3fa63af56..6814917f29 100644 --- a/src/plugins/notificationIndicator/components/NotificationIndicator.vue +++ b/src/plugins/notificationIndicator/components/NotificationIndicator.vue @@ -71,6 +71,10 @@ export default { this.openmct.notifications.on('notification', this.updateNotifications); this.openmct.notifications.on('dismiss-all', this.updateNotifications); }, + unmounted() { + this.openmct.notifications.of('notification', this.updateNotifications); + this.openmct.notifications.of('dismiss-all', this.updateNotifications); + }, methods: { dismissAllNotifications() { this.openmct.notifications.dismissAllNotifications(); diff --git a/src/plugins/notificationIndicator/pluginSpec.js b/src/plugins/notificationIndicator/pluginSpec.js index b04b361959..d0ac938a9e 100644 --- a/src/plugins/notificationIndicator/pluginSpec.js +++ b/src/plugins/notificationIndicator/pluginSpec.js @@ -63,7 +63,7 @@ describe('the plugin', () => { it('notifies the user of the number of notifications', () => { let notificationCountElement = document.querySelector('.c-indicator__count'); - expect(notificationCountElement.innerText).toEqual(mockMessages.length.toString()); + expect(notificationCountElement.innerText).toEqual('1'); }); }); }); diff --git a/src/plugins/plot/configuration/PlotSeries.js b/src/plugins/plot/configuration/PlotSeries.js index 13a916b100..19c5453038 100644 --- a/src/plugins/plot/configuration/PlotSeries.js +++ b/src/plugins/plot/configuration/PlotSeries.js @@ -324,7 +324,7 @@ export default class PlotSeries extends Model { async load(options) { await this.fetch(options); this.emit('load'); - this.loadLimits(); + await this.loadLimits(); } async loadLimits() { diff --git a/src/plugins/plot/overlayPlot/pluginSpec.js b/src/plugins/plot/overlayPlot/pluginSpec.js index 27b215f121..4c2d081c77 100644 --- a/src/plugins/plot/overlayPlot/pluginSpec.js +++ b/src/plugins/plot/overlayPlot/pluginSpec.js @@ -33,7 +33,7 @@ import configStore from '../configuration/ConfigStore'; import EventEmitter from 'EventEmitter'; import PlotOptions from '../inspector/PlotOptions.vue'; -describe('the plugin', function () { +xdescribe('the plugin', function () { let element; let child; let openmct; diff --git a/src/plugins/plot/pluginSpec.js b/src/plugins/plot/pluginSpec.js index f6524b4f5f..dd4eaa23c1 100644 --- a/src/plugins/plot/pluginSpec.js +++ b/src/plugins/plot/pluginSpec.js @@ -35,7 +35,7 @@ import PlotConfigurationModel from './configuration/PlotConfigurationModel'; const TEST_KEY_ID = 'some-other-key'; -describe('the plugin', function () { +xdescribe('the plugin', function () { let element; let child; let openmct; @@ -697,7 +697,7 @@ describe('the plugin', function () { }); }); - describe('the inspector view', () => { + xdescribe('the inspector view', () => { let component; let viewComponentObject; let mockComposition; diff --git a/src/plugins/plot/stackedPlot/StackedPlot.vue b/src/plugins/plot/stackedPlot/StackedPlot.vue index 74e892fe9a..90b4069224 100644 --- a/src/plugins/plot/stackedPlot/StackedPlot.vue +++ b/src/plugins/plot/stackedPlot/StackedPlot.vue @@ -232,7 +232,7 @@ export default { removeChild(childIdentifier) { const id = this.openmct.objects.makeKeyString(childIdentifier); - this.$delete(this.tickWidthMap, id); + delete this.tickWidthMap[id]; const childObj = this.compositionObjects.filter((c) => { const identifier = c.keyString; diff --git a/src/plugins/plot/stackedPlot/pluginSpec.js b/src/plugins/plot/stackedPlot/pluginSpec.js index 7689508783..2e3e2f7e79 100644 --- a/src/plugins/plot/stackedPlot/pluginSpec.js +++ b/src/plugins/plot/stackedPlot/pluginSpec.js @@ -34,7 +34,7 @@ import EventEmitter from 'EventEmitter'; import PlotConfigurationModel from '../configuration/PlotConfigurationModel'; import PlotOptions from '../inspector/PlotOptions.vue'; -describe('the plugin', function () { +xdescribe('the plugin', function () { let element; let child; let openmct; diff --git a/src/plugins/timeConductor/Conductor.vue b/src/plugins/timeConductor/Conductor.vue index f62dfb556f..29fd89bd5f 100644 --- a/src/plugins/timeConductor/Conductor.vue +++ b/src/plugins/timeConductor/Conductor.vue @@ -49,7 +49,11 @@ @panAxis="pan" @zoomAxis="zoom" /> -
+
diff --git a/src/plugins/timeConductor/ConductorInputsFixed.vue b/src/plugins/timeConductor/ConductorInputsFixed.vue index 0a23c22dda..70ebf9b185 100644 --- a/src/plugins/timeConductor/ConductorInputsFixed.vue +++ b/src/plugins/timeConductor/ConductorInputsFixed.vue @@ -32,6 +32,7 @@
{{ formattedBounds.start }}
@@ -39,6 +40,7 @@
{{ formattedBounds.end }}
diff --git a/src/plugins/timeConductor/ConductorMode.vue b/src/plugins/timeConductor/ConductorMode.vue index ef49245ca4..20dfd41911 100644 --- a/src/plugins/timeConductor/ConductorMode.vue +++ b/src/plugins/timeConductor/ConductorMode.vue @@ -25,13 +25,19 @@