diff --git a/.circleci/config.yml b/.circleci/config.yml index ffc8573372..e17e4f8a18 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: build: docker: - - image: circleci/node:8-browsers + - image: circleci/node:13-browsers environment: CHROME_BIN: "/usr/bin/google-chrome" steps: @@ -11,12 +11,12 @@ jobs: name: Update npm command: 'sudo npm install -g npm@latest' - restore_cache: - key: dependency-cache-{{ checksum "package.json" }} + key: dependency-cache-13-{{ checksum "package.json" }} - run: name: Installing dependencies (npm install) command: npm install - save_cache: - key: dependency-cache-{{ checksum "package.json" }} + key: dependency-cache-13-{{ checksum "package.json" }} paths: - node_modules - run: diff --git a/.eslintrc.js b/.eslintrc.js index 0cb8f3c4f7..fbd822507c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,4 @@ +const LEGACY_FILES = ["platform/**", "example/**"]; module.exports = { "env": { "browser": true, @@ -70,6 +71,56 @@ module.exports = { ], "dot-notation": "error", "indent": ["error", 4], + + // https://eslint.org/docs/rules/no-case-declarations + "no-case-declarations": "error", + // https://eslint.org/docs/rules/max-classes-per-file + "max-classes-per-file": ["error", 1], + // https://eslint.org/docs/rules/no-eq-null + "no-eq-null": "error", + // https://eslint.org/docs/rules/no-eval + "no-eval": "error", + // https://eslint.org/docs/rules/no-floating-decimal + "no-floating-decimal": "error", + // https://eslint.org/docs/rules/no-implicit-globals + "no-implicit-globals": "error", + // https://eslint.org/docs/rules/no-implied-eval + "no-implied-eval": "error", + // https://eslint.org/docs/rules/no-lone-blocks + "no-lone-blocks": "error", + // https://eslint.org/docs/rules/no-loop-func + "no-loop-func": "error", + // https://eslint.org/docs/rules/no-new-func + "no-new-func": "error", + // https://eslint.org/docs/rules/no-new-wrappers + "no-new-wrappers": "error", + // https://eslint.org/docs/rules/no-octal-escape + "no-octal-escape": "error", + // https://eslint.org/docs/rules/no-proto + "no-proto": "error", + // https://eslint.org/docs/rules/no-return-await + "no-return-await": "error", + // https://eslint.org/docs/rules/no-script-url + "no-script-url": "error", + // https://eslint.org/docs/rules/no-self-compare + "no-self-compare": "error", + // https://eslint.org/docs/rules/no-sequences + "no-sequences": "error", + // https://eslint.org/docs/rules/no-unmodified-loop-condition + "no-unmodified-loop-condition": "error", + // https://eslint.org/docs/rules/no-useless-call + "no-useless-call": "error", + // https://eslint.org/docs/rules/wrap-iife + "wrap-iife": "error", + // https://eslint.org/docs/rules/no-nested-ternary + "no-nested-ternary": "error", + // https://eslint.org/docs/rules/switch-colon-spacing + "switch-colon-spacing": "error", + // https://eslint.org/docs/rules/no-useless-computed-key + "no-useless-computed-key": "error", + // https://eslint.org/docs/rules/rest-spread-spacing + "rest-spread-spacing": ["error"], + "vue/html-indent": [ "error", 4, @@ -116,6 +167,13 @@ module.exports = { } ] } + }, { + "files": LEGACY_FILES, + "rules": { + // https://eslint.org/docs/rules/no-nested-ternary + "no-nested-ternary": "off", + "no-var": "off" + } } ] }; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8878f36ec..77506207c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,89 +131,96 @@ changes. ### Code Standards -JavaScript sources in Open MCT must satisfy JSLint under its default -settings. This is verified by the command line build. +JavaScript sources in Open MCT must satisfy the ESLint rules defined in +this repository. This is verified by the command line build. #### Code Guidelines -JavaScript sources in Open MCT should: - -* Use four spaces for indentation. Tabs should not be used. -* Include JSDoc for any exposed API (e.g. public methods, constructors). -* Include non-JSDoc comments as-needed for explaining private variables, - methods, or algorithms when they are non-obvious. -* Define one public class per script, expressed as a constructor function - returned from an AMD-style module. -* Follow “Java-like” naming conventions. These includes: - * Classes should use camel case, first letter capitalized - (e.g. SomeClassName). - * Methods, variables, fields, and function names should use camel case, - first letter lower-case (e.g. someVariableName). - * Constants (variables or fields which are meant to be declared and - initialized statically, and never changed) should use only capital - letters, with underscores between words (e.g. SOME_CONSTANT). - * File names should be the name of the exported class, plus a .js extension - (e.g. SomeClassName.js). -* Avoid anonymous functions, except when functions are short (a few lines) - and/or their inclusion makes sense within the flow of the code - (e.g. as arguments to a forEach call). -* Avoid deep nesting (especially of functions), except where necessary - (e.g. due to closure scope). -* End with a single new-line character. -* Expose public methods by declaring them on the class's prototype. -* Within a given function's scope, do not mix declarations and imperative - code, and present these in the following order: - * First, variable declarations and initialization. - * Second, function declarations. - * Third, imperative statements. - * Finally, the returned value. +The following guidelines are provided for anyone contributing source code to the Open MCT project: +1. Write clean code. Here’s a good summary - https://github.com/ryanmcdermott/clean-code-javascript. +1. Include JSDoc for any exposed API (e.g. public methods, classes). +1. Include non-JSDoc comments as-needed for explaining private variables, + methods, or algorithms when they are non-obvious. Otherwise code + should be self-documenting. +1. Classes and Vue components should use camel case, first letter capitalized + (e.g. SomeClassName). +1. Methods, variables, fields, events, and function names should use camelCase, + first letter lower-case (e.g. someVariableName). +1. Source files that export functions should use camelCase, first letter lower-case (eg. testTools.js) +1. Constants (variables or fields which are meant to be declared and + initialized statically, and never changed) should use only capital + letters, with underscores between words (e.g. SOME_CONSTANT). They should always be declared as `const`s +1. File names should be the name of the exported class, plus a .js extension + (e.g. SomeClassName.js). +1. Avoid anonymous functions, except when functions are short (one or two lines) + and their inclusion makes sense within the flow of the code + (e.g. as arguments to a forEach call). Anonymous functions should always be arrow functions. +1. Named functions are preferred over functions assigned to variables. + eg. + ```JavaScript + function renameObject(object, newName) { + Object.name = newName; + } + ``` + is preferable to + ```JavaScript + const rename = (object, newName) => { + Object.name = newName; + } + ``` +1. Avoid deep nesting (especially of functions), except where necessary + (e.g. due to closure scope). +1. End with a single new-line character. +1. Always use ES6 `Class`es and inheritence rather than the pre-ES6 prototypal + pattern. +1. Within a given function's scope, do not mix declarations and imperative + code, and present these in the following order: + * First, variable declarations and initialization. + * Secondly, imperative statements. + * Finally, the returned value. A single return statement at the end of the function should be used, except where an early return would improve code clarity. +1. Avoid the use of "magic" values. + eg. + ```JavaScript + Const UNAUTHORIZED = 401 + if (responseCode === UNAUTHORIZED) + ``` + is preferable to + ```JavaScript + if (responseCode === 401) + ``` +1. Use the ternary operator only for simple cases such as variable assignment. Nested ternaries should be avoided in all cases. +1. Test specs should reside alongside the source code they test, not in a separate directory. +1. Organize code by feature, not by type. + eg. + ``` + - telemetryTable + - row + TableRow.js + TableRowCollection.js + TableRow.vue + - column + TableColumn.js + TableColumn.vue + plugin.js + pluginSpec.js + ``` + is preferable to + ``` + - telemetryTable + - components + TableRow.vue + TableColumn.vue + - collections + TableRowCollection.js + TableColumn.js + TableRow.js + plugin.js + pluginSpec.js + ``` Deviations from Open MCT code style guidelines require two-party agreement, typically from the author of the change and its reviewer. -#### Code Example - -```js -/*global define*/ - -/** - * Bundles should declare themselves as namespaces in whichever source - * file is most like the "main point of entry" to the bundle. - * @namespace some/bundle - */ -define( - ['./OtherClass'], - function (OtherClass) { - "use strict"; - - /** - * A summary of how to use this class goes here. - * - * @constructor - * @memberof some/bundle - */ - function ExampleClass() { - } - - // Methods which are not intended for external use should - // not have JSDoc (or should be marked @private) - ExampleClass.prototype.privateMethod = function () { - }; - - /** - * A summary of this method goes here. - * @param {number} n a parameter - * @returns {number} a return value - */ - ExampleClass.prototype.publicMethod = function (n) { - return n * 2; - } - - return ExampleClass; - } -); -``` - ### Test Standards Automated testing shall occur whenever changes are merged into the main diff --git a/docs/src/process/testing/plan.md b/docs/src/process/testing/plan.md index 34e75855c9..e0fd00482c 100644 --- a/docs/src/process/testing/plan.md +++ b/docs/src/process/testing/plan.md @@ -125,3 +125,22 @@ A release is not closed until both categories have been performed on the latest snapshot of the software, _and_ no issues labelled as ["blocker" or "critical"](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting) remain open. + +### Testathons +Testathons can be used as a means of performing per-sprint and per-release testing. + +#### Timing +For per-sprint testing, a testathon is typically performed at the beginning of the third week of a sprint, and again later that week to verify any fixes. For per-release testing, a testathon is typically performed prior to any formal testing processes that are applicable to that release. + +#### Process + +1. Prior to the scheduled testathon, a list will be compiled of all issues that are closed and unverified. +2. For each issue, testers should review the associated PR for testing instructions. See the contributing guide for instructions on [pull requests](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md#merging). +3. As each issue is verified via testing, any team members testing it should leave a comment on that issue indicating that it has been verified fixed. +4. If a bug is found that relates to an issue being tested, notes should be included on the associated issue, and the issue should be reopened. Bug notes should include reproduction steps. +5. For any bugs that are not obviously related to any of the issues under test, a new issue should be created with details about the bug, including reproduction steps. If unsure about whether a bug relates to an issue being tested, just create a new issue. +6. At the end of the testathon, triage will take place, where all tested issues will be reviewed. +7. If verified fixed, an issue will remain closed, and will have the “unverified” label removed. +8. For any bugs found, a severity will be assigned. +9. A second testathon will be scheduled for later in the week that will aim to address all issues identified as blockers, as well as any other issues scoped by the team during triage. +10. Any issues that were not tested will remain "unverified" and will be picked up in the next testathon. diff --git a/example/imagery/plugin.js b/example/imagery/plugin.js index 8d3d4211ed..63a351c985 100644 --- a/example/imagery/plugin.js +++ b/example/imagery/plugin.js @@ -52,6 +52,7 @@ define([ return { name: name, utc: Math.floor(timestamp / 5000) * 5000, + local: Math.floor(timestamp / 5000) * 5000, url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length] }; } @@ -78,7 +79,7 @@ define([ }, request: function (domainObject, options) { var start = options.start; - var end = options.end; + var end = Math.min(options.end, Date.now()); var data = []; while (start <= end && data.length < 5000) { data.push(pointForTimestamp(start, domainObject.name)); @@ -118,6 +119,14 @@ define([ name: 'Time', key: 'utc', format: 'utc', + hints: { + domain: 2 + } + }, + { + name: 'Local Time', + key: 'local', + format: 'local-format', hints: { domain: 1 } diff --git a/package.json b/package.json index 8b6fab4940..53e032e169 100644 --- a/package.json +++ b/package.json @@ -84,10 +84,10 @@ "build:prod": "cross-env NODE_ENV=production webpack", "build:dev": "webpack", "build:watch": "webpack --watch", - "test": "karma start --single-run", + "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", - "test:coverage": "./scripts/test-coverage.sh", - "test:watch": "karma start --no-single-run", + "test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run", + "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", "verify": "concurrently 'npm:test' 'npm:lint'", "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", diff --git a/platform/commonUI/dialog/res/templates/overlay-message-list.html b/platform/commonUI/dialog/res/templates/overlay-message-list.html index 39aa0ca228..caabd7d4bf 100644 --- a/platform/commonUI/dialog/res/templates/overlay-message-list.html +++ b/platform/commonUI/dialog/res/templates/overlay-message-list.html @@ -6,6 +6,12 @@ ng-show="ngModel.dialog.messages.length > 1 || ngModel.dialog.messages.length == 0">s +
- {{dialogAction.label}} + {{ dialogAction.label }}
diff --git a/platform/commonUI/notification/bundle.js b/platform/commonUI/notification/bundle.js index 5410172d84..f8a2a3a566 100644 --- a/platform/commonUI/notification/bundle.js +++ b/platform/commonUI/notification/bundle.js @@ -21,44 +21,15 @@ *****************************************************************************/ define([ - "./src/NotificationIndicatorController", - "./src/NotificationIndicator", - "./src/NotificationService", - "./res/notification-indicator.html" + "./src/NotificationService" ], function ( - NotificationIndicatorController, - NotificationIndicator, - NotificationService, - notificationIndicatorTemplate + NotificationService ) { return { name:"platform/commonUI/notification", definition: { "extensions": { - "templates": [ - { - "key": "notificationIndicatorTemplate", - "template": notificationIndicatorTemplate - } - ], - "controllers": [ - { - "key": "NotificationIndicatorController", - "implementation": NotificationIndicatorController, - "depends": [ - "$scope", - "openmct", - "dialogService" - ] - } - ], - "indicators": [ - { - "implementation": NotificationIndicator, - "priority": "fallback" - } - ], "services": [ { "key": "notificationService", diff --git a/platform/commonUI/notification/res/notification-indicator.html b/platform/commonUI/notification/res/notification-indicator.html deleted file mode 100644 index 75ec9be553..0000000000 --- a/platform/commonUI/notification/res/notification-indicator.html +++ /dev/null @@ -1,8 +0,0 @@ - -
- - - {{notifications.length}} -
diff --git a/platform/commonUI/notification/src/NotificationIndicatorController.js b/platform/commonUI/notification/src/NotificationIndicatorController.js deleted file mode 100644 index ced8b5b87e..0000000000 --- a/platform/commonUI/notification/src/NotificationIndicatorController.js +++ /dev/null @@ -1,73 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, 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( - [], - function () { - - /** - * Provides an indicator that is visible when there are - * banner notifications that have been minimized. Will also indicate - * the number of notifications. Notifications can be viewed by - * clicking on the indicator to launch a dialog showing a list of - * notifications. - * @param $scope - * @param notificationService - * @param dialogService - * @constructor - */ - function NotificationIndicatorController($scope, openmct, dialogService) { - $scope.notifications = openmct.notifications.notifications; - $scope.highest = openmct.notifications.highest; - - /** - * Launch a dialog showing a list of current notifications. - */ - $scope.showNotificationsList = function () { - let notificationsList = openmct.notifications.notifications.map(notification => { - if (notification.model.severity === 'alert' || notification.model.severity === 'info') { - notification.model.primaryOption = { - label: 'Dismiss', - callback: () => { - let currentIndex = notificationsList.indexOf(notification); - notification.dismiss(); - notificationsList.splice(currentIndex, 1); - } - } - } - return notification; - }) - dialogService.getDialogResponse('overlay-message-list', { - dialog: { - title: "Messages", - //Launch the message list dialog with the models - // from the notifications - messages: notificationsList - } - }); - - }; - } - return NotificationIndicatorController; - } -); - diff --git a/platform/commonUI/notification/test/NotificationIndicatorControllerSpec.js b/platform/commonUI/notification/test/NotificationIndicatorControllerSpec.js deleted file mode 100644 index 9fe5ffb325..0000000000 --- a/platform/commonUI/notification/test/NotificationIndicatorControllerSpec.js +++ /dev/null @@ -1,60 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, 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/NotificationIndicatorController'], - function (NotificationIndicatorController) { - - xdescribe("The notification indicator controller ", function () { - var mockNotificationService, - mockScope, - mockDialogService, - controller; - - beforeEach(function () { - mockNotificationService = jasmine.createSpy("notificationService"); - mockScope = jasmine.createSpy("$scope"); - mockDialogService = jasmine.createSpyObj( - "dialogService", - ["getDialogResponse","dismiss"] - ); - mockNotificationService.highest = { - severity: "error" - }; - controller = new NotificationIndicatorController(mockScope, mockNotificationService, mockDialogService); - }); - - it("exposes the highest notification severity to the template", function () { - expect(mockScope.highest).toBeTruthy(); - expect(mockScope.highest.severity).toBe("error"); - }); - - it("invokes the dialog service to show list of messages", function () { - expect(mockScope.showNotificationsList).toBeDefined(); - mockScope.showNotificationsList(); - expect(mockDialogService.getDialogResponse).toHaveBeenCalled(); - expect(mockDialogService.getDialogResponse.calls.mostRecent().args[0]).toBe('overlay-message-list'); - expect(mockDialogService.getDialogResponse.calls.mostRecent().args[1].dialog).toBeDefined(); - }); - }); - } -); diff --git a/report.20200527.134750.93992.001.json b/report.20200527.134750.93992.001.json new file mode 100644 index 0000000000..d9515d66d7 --- /dev/null +++ b/report.20200527.134750.93992.001.json @@ -0,0 +1,621 @@ + +{ + "header": { + "event": "Allocation failed - JavaScript heap out of memory", + "location": "OnFatalError", + "filename": "report.20200527.134750.93992.001.json", + "dumpEventTime": "2020-05-27T13:47:50Z", + "dumpEventTimeStamp": "1590612470877", + "processId": 93992, + "commandLine": [ + "node", + "/Users/dtailor/Desktop/openmct/node_modules/.bin/karma", + "start", + "--single-run" + ], + "nodejsVersion": "v11.9.0", + "wordSize": 64, + "componentVersions": { + "node": "11.9.0", + "v8": "7.0.276.38-node.16", + "uv": "1.25.0", + "zlib": "1.2.11", + "brotli": "1.0.7", + "ares": "1.15.0", + "modules": "67", + "nghttp2": "1.34.0", + "napi": "4", + "llhttp": "1.0.1", + "http_parser": "2.8.0", + "openssl": "1.1.1a", + "cldr": "34.0", + "icu": "63.1", + "tz": "2018e", + "unicode": "11.0", + "arch": "x64", + "platform": "darwin", + "release": "node" + }, + "osVersion": "Darwin 18.7.0 Darwin Kernel Version 18.7.0: Thu Jan 23 06:52:12 PST 2020; root:xnu-4903.278.25~1/RELEASE_X86_64", + "machine": "Darwin 18.7.0 Darwin Kernel Version 18.7.0: Thu Jan 23 06:52:12 PST 2020; root:xnu-4903.278.25~1/RELEASE_X86_64tailor x86_64" + }, + "javascriptStack": { + "message": "No stack.", + "stack": [ + "Unavailable." + ] + }, + "nativeStack": [ + " [pc=0x10013090e] report::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__1::basic_string, std::__1::allocator >, v8::Local) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x100063744] node::OnFatalError(char const*, char const*) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x1001a8c47] v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x1001a8be4] v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x1005add42] v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x1005b0273] v8::internal::Heap::CheckIneffectiveMarkCompact(unsigned long, double) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x1005ac7a8] v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x1005aa965] v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x1005b720c] v8::internal::Heap::AllocateRawWithLightRetry(int, v8::internal::AllocationSpace, v8::internal::AllocationAlignment) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x1005b728f] v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationSpace, v8::internal::AllocationAlignment) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x100586484] v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationSpace) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x1008389a4] v8::internal::Runtime_AllocateInNewSpace(int, v8::internal::Object**, v8::internal::Isolate*) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]", + " [pc=0x14acfddcfc7d] " + ], + "javascriptHeap": { + "totalMemory": 1479229440, + "totalCommittedMemory": 1477309024, + "usedMemory": 1445511032, + "availableMemory": 50296592, + "memoryLimit": 1526909922, + "heapSpaces": { + "read_only_space": { + "memorySize": 524288, + "committedMemory": 42224, + "capacity": 515584, + "used": 33520, + "available": 482064 + }, + "new_space": { + "memorySize": 4194304, + "committedMemory": 4194288, + "capacity": 2062336, + "used": 59016, + "available": 2003320 + }, + "old_space": { + "memorySize": 305860608, + "committedMemory": 305138544, + "capacity": 283264904, + "used": 282942208, + "available": 322696 + }, + "code_space": { + "memorySize": 6291456, + "committedMemory": 5687328, + "capacity": 5237152, + "used": 5237152, + "available": 0 + }, + "map_space": { + "memorySize": 5255168, + "committedMemory": 5143024, + "capacity": 2523280, + "used": 2523280, + "available": 0 + }, + "large_object_space": { + "memorySize": 1157103616, + "committedMemory": 1157103616, + "capacity": 1202204368, + "used": 1154715856, + "available": 47488512 + }, + "new_large_object_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + } + } + }, + "resourceUsage": { + "userCpuSeconds": 43.1616, + "kernelCpuSeconds": 43.1616, + "cpuConsumptionPercent": 5.42705e-06, + "maxRss": 1966080000000, + "pageFaults": { + "IORequired": 245, + "IONotRequired": 832598 + }, + "fsActivity": { + "reads": 0, + "writes": 0 + } + }, + "libuv": [ + ], + "environmentVariables": { + "npm_config_save_dev": "", + "npm_config_legacy_bundling": "", + "npm_config_dry_run": "", + "npm_package_devDependencies_markdown_toc": "^0.11.7", + "npm_config_only": "", + "npm_config_browser": "", + "npm_config_viewer": "man", + "npm_config_commit_hooks": "true", + "npm_package_gitHead": "7126abe7ec1d66d3252f3598fbd6bd27217018bc", + "npm_config_also": "", + "npm_package_scripts_otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", + "npm_package_devDependencies_minimist": "^1.1.1", + "npm_config_sign_git_commit": "", + "npm_config_rollback": "true", + "npm_package_devDependencies_fast_sass_loader": "1.4.6", + "TERM_PROGRAM": "Apple_Terminal", + "npm_config_usage": "", + "npm_config_audit": "true", + "npm_package_devDependencies_git_rev_sync": "^1.4.0", + "npm_package_devDependencies_file_loader": "^1.1.11", + "npm_package_devDependencies_d3_selection": "1.3.x", + "NODE": "/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node", + "npm_package_homepage": "https://github.com/nasa/openmct#readme", + "INIT_CWD": "/Users/dtailor/Desktop/openmct", + "NVM_CD_FLAGS": "", + "npm_config_globalignorefile": "/Users/dtailor/.nvm/versions/node/v11.9.0/etc/npmignore", + "npm_package_devDependencies_comma_separated_values": "^3.6.4", + "SHELL": "/bin/bash", + "TERM": "xterm-256color", + "npm_config_init_author_url": "", + "npm_config_shell": "/bin/bash", + "npm_config_maxsockets": "50", + "npm_package_devDependencies_vue_template_compiler": "2.5.6", + "npm_package_devDependencies_style_loader": "^1.0.1", + "npm_package_devDependencies_moment_duration_format": "^2.2.2", + "npm_config_parseable": "", + "npm_config_shrinkwrap": "true", + "npm_config_metrics_registry": "https://registry.npmjs.org/", + "TMPDIR": "/var/folders/ks/ytghmh9x4lj3cchr5km5lhkcb7v9y2/T/", + "npm_config_timing": "", + "npm_config_init_license": "ISC", + "npm_package_scripts_lint": "eslint platform example src --ext .js,.vue openmct.js", + "npm_package_devDependencies_d3_array": "1.2.x", + "Apple_PubSub_Socket_Render": "/private/tmp/com.apple.launchd.PsV6Dfq4Tm/Render", + "npm_config_if_present": "", + "npm_package_devDependencies_concurrently": "^3.6.1", + "TERM_PROGRAM_VERSION": "421.2", + "npm_package_scripts_jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", + "npm_config_sign_git_tag": "", + "npm_config_init_author_email": "", + "npm_config_cache_max": "Infinity", + "npm_config_preid": "", + "npm_config_long": "", + "npm_config_local_address": "", + "npm_config_cert": "", + "npm_config_git_tag_version": "true", + "npm_package_devDependencies_exports_loader": "^0.7.0", + "TERM_SESSION_ID": "0630D2FA-BAC2-48D3-A21D-9AB58A79FB14", + "npm_config_noproxy": "", + "npm_config_registry": "https://registry.npmjs.org/", + "npm_config_fetch_retries": "2", + "npm_package_private": "true", + "npm_package_devDependencies_karma_jasmine": "^1.1.2", + "npm_package_repository_url": "git+https://github.com/nasa/openmct.git", + "npm_config_versions": "", + "npm_config_key": "", + "npm_config_message": "%s", + "npm_package_readmeFilename": "README.md", + "npm_package_devDependencies_painterro": "^0.2.65", + "npm_package_scripts_verify": "concurrently 'npm:test' 'npm:lint'", + "npm_package_devDependencies_webpack": "^4.16.2", + "npm_package_devDependencies_eventemitter3": "^1.2.0", + "npm_package_description": "The Open MCT core platform", + "USER": "dtailor", + "NVM_DIR": "/Users/dtailor/.nvm", + "npm_package_license": "Apache-2.0", + "npm_package_scripts_build_dev": "webpack", + "npm_package_devDependencies_webpack_cli": "^3.1.0", + "npm_package_devDependencies_location_bar": "^3.0.1", + "npm_package_devDependencies_jasmine_core": "^3.1.0", + "npm_config_globalconfig": "/Users/dtailor/.nvm/versions/node/v11.9.0/etc/npmrc", + "npm_package_devDependencies_karma": "^2.0.3", + "npm_config_prefer_online": "", + "npm_config_always_auth": "", + "npm_config_logs_max": "10", + "npm_package_devDependencies_angular": "1.7.9", + "SSH_AUTH_SOCK": "/private/tmp/com.apple.launchd.JH8E4KgH06/Listeners", + "npm_package_devDependencies_request": "^2.69.0", + "npm_package_devDependencies_eslint": "5.2.0", + "__CF_USER_TEXT_ENCODING": "0x167DA7C2:0x0:0x0", + "npm_execpath": "/Users/dtailor/.nvm/versions/node/v11.9.0/lib/node_modules/npm/bin/npm-cli.js", + "npm_config_global_style": "", + "npm_config_cache_lock_retries": "10", + "npm_config_cafile": "", + "npm_config_update_notifier": "true", + "npm_package_scripts_test_debug": "cross-env NODE_ENV=debug karma start --no-single-run", + "npm_package_devDependencies_glob": ">= 3.0.0", + "npm_config_heading": "npm", + "npm_config_audit_level": "low", + "npm_package_devDependencies_mini_css_extract_plugin": "^0.4.1", + "npm_package_devDependencies_copy_webpack_plugin": "^4.5.2", + "npm_config_read_only": "", + "npm_config_offline": "", + "npm_config_searchlimit": "20", + "npm_config_fetch_retry_mintimeout": "10000", + "npm_package_devDependencies_webpack_dev_middleware": "^3.1.3", + "npm_config_json": "", + "npm_config_access": "", + "npm_config_argv": "{\"remain\":[],\"cooked\":[\"run\",\"test\"],\"original\":[\"run\",\"test\"]}", + "npm_package_scripts_lint_fix": "eslint platform example src --ext .js,.vue openmct.js --fix", + "npm_package_devDependencies_uuid": "^3.3.3", + "npm_package_devDependencies_karma_coverage": "^1.1.2", + "PATH": "/Users/dtailor/.nvm/versions/node/v11.9.0/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/Users/dtailor/Desktop/openmct/node_modules/.bin:/Users/dtailor/.nvm/versions/node/v11.9.0/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/Users/dtailor/Desktop/openmct/node_modules/.bin:/Users/dtailor/.nvm/versions/node/v11.9.0/bin:/Users/dtailor/.homebrew/bin:/Users/dtailor/local/bin:/Users/dtailor/.homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Users/dtailor/Applications/Visual Studio Code.app/Contents/Resources/app/bin:/opt/local/bin:/Users/dtailor/.homebrew/bin:/Users/dtailor/.homebrew/bin:/Users/dtailor/.homebrew/bin:/Users/dtailor/local/bin:/Users/dtailor/.homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin", + "npm_config_allow_same_version": "", + "npm_config_https_proxy": "", + "npm_config_engine_strict": "", + "npm_config_description": "true", + "npm_package_devDependencies_html2canvas": "^1.0.0-alpha.12", + "_": "/Users/dtailor/Desktop/openmct/node_modules/.bin/karma", + "npm_config_userconfig": "/Users/dtailor/.npmrc", + "npm_config_init_module": "/Users/dtailor/.npm-init.js", + "npm_package_author": "", + "npm_package_devDependencies_karma_chrome_launcher": "^2.2.0", + "npm_package_devDependencies_d3_scale": "1.0.x", + "npm_config_cidr": "", + "npm_package_devDependencies_printj": "^1.2.1", + "PWD": "/Users/dtailor/Desktop/openmct", + "npm_config_user": "377333698", + "npm_config_node_version": "11.9.0", + "npm_package_bugs_url": "https://github.com/nasa/openmct/issues", + "npm_package_scripts_test_watch": "karma start --no-single-run", + "npm_lifecycle_event": "test", + "npm_package_devDependencies_v8_compile_cache": "^1.1.0", + "npm_config_ignore_prepublish": "", + "npm_config_save": "true", + "npm_config_editor": "vi", + "npm_config_auth_type": "legacy", + "npm_package_repository_type": "git", + "npm_package_devDependencies_vue": "2.5.6", + "npm_package_devDependencies_marked": "^0.3.5", + "npm_package_devDependencies_angular_route": "1.4.14", + "npm_package_name": "openmct", + "LANG": "en_US.UTF-8", + "npm_config_script_shell": "", + "npm_config_tag": "latest", + "npm_config_global": "", + "npm_config_progress": "true", + "npm_package_scripts_start": "node app.js", + "npm_package_devDependencies_karma_coverage_istanbul_reporter": "^2.1.1", + "npm_config_ham_it_up": "", + "npm_config_searchstaleness": "900", + "npm_config_optional": "true", + "npm_package_scripts_docs": "npm run jsdoc ; npm run otherdoc", + "npm_package_devDependencies_istanbul_instrumenter_loader": "^3.0.1", + "XPC_FLAGS": "0x0", + "npm_config_save_prod": "", + "npm_config_force": "", + "npm_config_bin_links": "true", + "npm_package_devDependencies_moment": "2.25.3", + "npm_package_devDependencies_karma_webpack": "^3.0.0", + "npm_package_devDependencies_express": "^4.13.1", + "npm_config_searchopts": "", + "npm_package_devDependencies_d3_time": "1.0.x", + "FORCE_COLOR": "2", + "npm_config_node_gyp": "/Users/dtailor/.nvm/versions/node/v11.9.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js", + "npm_config_depth": "Infinity", + "npm_package_scripts_build_prod": "cross-env NODE_ENV=production webpack", + "npm_config_sso_poll_frequency": "500", + "npm_config_rebuild_bundle": "true", + "npm_package_version": "1.0.0-snapshot", + "XPC_SERVICE_NAME": "0", + "npm_config_unicode": "true", + "npm_package_devDependencies_jsdoc": "^3.3.2", + "SHLVL": "4", + "HOME": "/Users/dtailor", + "npm_config_fetch_retry_maxtimeout": "60000", + "npm_package_scripts_test": "karma start --single-run", + "npm_package_devDependencies_zepto": "^1.2.0", + "npm_package_devDependencies_eslint_plugin_vue": "^6.0.0", + "npm_config_ca": "", + "npm_config_tag_version_prefix": "v", + "npm_config_strict_ssl": "true", + "npm_config_sso_type": "oauth", + "npm_config_scripts_prepend_node_path": "warn-only", + "npm_config_save_prefix": "^", + "npm_config_loglevel": "notice", + "npm_package_devDependencies_lodash": "^3.10.1", + "npm_package_devDependencies_karma_cli": "^1.0.1", + "npm_package_devDependencies_d3_color": "1.0.x", + "npm_config_save_exact": "", + "npm_config_dev": "", + "npm_config_group": "1286109195", + "npm_config_fetch_retry_factor": "10", + "npm_package_devDependencies_webpack_hot_middleware": "^2.22.3", + "npm_package_devDependencies_cross_env": "^6.0.3", + "npm_package_devDependencies_babel_eslint": "8.2.6", + "HOMEBREW_PREFIX": "/Users/dtailor/.homebrew", + "npm_config_version": "", + "npm_config_prefer_offline": "", + "npm_config_cache_lock_stale": "60000", + "npm_config_otp": "", + "npm_config_cache_min": "10", + "npm_package_devDependencies_vue_loader": "^15.2.6", + "npm_config_searchexclude": "", + "npm_config_cache": "/Users/dtailor/.npm", + "npm_package_scripts_test_coverage": "./scripts/test-coverage.sh", + "npm_package_devDependencies_d3_interpolate": "1.1.x", + "npm_package_devDependencies_d3_format": "1.2.x", + "LOGNAME": "dtailor", + "npm_lifecycle_script": "karma start --single-run", + "npm_config_color": "true", + "npm_package_devDependencies_node_bourbon": "^4.2.3", + "npm_package_devDependencies_karma_sourcemap_loader": "^0.3.7", + "npm_package_devDependencies_karma_html_reporter": "^0.2.7", + "npm_config_proxy": "", + "npm_config_package_lock": "true", + "npm_package_devDependencies_d3_time_format": "2.1.x", + "npm_package_devDependencies_d3_axis": "1.0.x", + "npm_config_package_lock_only": "", + "npm_package_devDependencies_moment_timezone": "0.5.28", + "npm_config_save_optional": "", + "NVM_BIN": "/Users/dtailor/.nvm/versions/node/v11.9.0/bin", + "npm_config_ignore_scripts": "", + "npm_config_user_agent": "npm/6.5.0 node/v11.9.0 darwin x64", + "npm_package_devDependencies_imports_loader": "^0.8.0", + "npm_package_devDependencies_file_saver": "^1.3.8", + "npm_config_cache_lock_wait": "10000", + "npm_config_production": "", + "npm_package_scripts_build_watch": "webpack --watch", + "DISPLAY": "/private/tmp/com.apple.launchd.E3N8oC6RMf/org.macosforge.xquartz:0", + "npm_config_send_metrics": "", + "npm_config_save_bundle": "", + "npm_package_scripts_prepare": "npm run build:prod", + "npm_config_node_options": "", + "npm_config_umask": "0022", + "npm_config_init_version": "1.0.0", + "npm_package_devDependencies_split": "^1.0.0", + "npm_package_devDependencies_raw_loader": "^0.5.1", + "npm_config_init_author_name": "", + "npm_config_git": "git", + "npm_config_scope": "", + "npm_package_scripts_clean": "rm -rf ./dist", + "npm_package_devDependencies_node_sass": "^4.9.2", + "npm_package_devDependencies_css_loader": "^1.0.0", + "DISABLE_UPDATE_CHECK": "1", + "npm_config_onload_script": "", + "npm_config_unsafe_perm": "true", + "npm_config_tmp": "/var/folders/ks/ytghmh9x4lj3cchr5km5lhkcb7v9y2/T", + "npm_package_devDependencies_d3_collection": "1.0.x", + "npm_node_execpath": "/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node", + "npm_config_link": "", + "npm_config_prefix": "/Users/dtailor/.nvm/versions/node/v11.9.0", + "npm_package_devDependencies_html_loader": "^0.5.5" + }, + "userLimits": { + "core_file_size_blocks": { + "soft": 0, + "hard": "unlimited" + }, + "data_seg_size_kbytes": { + "soft": "unlimited", + "hard": "unlimited" + }, + "file_size_blocks": { + "soft": "unlimited", + "hard": "unlimited" + }, + "max_locked_memory_bytes": { + "soft": "unlimited", + "hard": "unlimited" + }, + "max_memory_size_kbytes": { + "soft": "unlimited", + "hard": "unlimited" + }, + "open_files": { + "soft": 24576, + "hard": "unlimited" + }, + "stack_size_bytes": { + "soft": 8388608, + "hard": 67104768 + }, + "cpu_time_seconds": { + "soft": "unlimited", + "hard": "unlimited" + }, + "max_user_processes": { + "soft": 1418, + "hard": 2128 + }, + "virtual_memory_kbytes": { + "soft": "unlimited", + "hard": "unlimited" + } + }, + "sharedObjects": [ + "/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node", + "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation", + "/usr/lib/libSystem.B.dylib", + "/usr/lib/libc++.1.dylib", + "/usr/lib/libobjc.A.dylib", + "/usr/lib/libDiagnosticMessagesClient.dylib", + "/usr/lib/libicucore.A.dylib", + "/usr/lib/libz.1.dylib", + "/usr/lib/libc++abi.dylib", + "/usr/lib/system/libcache.dylib", + "/usr/lib/system/libcommonCrypto.dylib", + "/usr/lib/system/libcompiler_rt.dylib", + "/usr/lib/system/libcopyfile.dylib", + "/usr/lib/system/libcorecrypto.dylib", + "/usr/lib/system/libdispatch.dylib", + "/usr/lib/system/libdyld.dylib", + "/usr/lib/system/libkeymgr.dylib", + "/usr/lib/system/liblaunch.dylib", + "/usr/lib/system/libmacho.dylib", + "/usr/lib/system/libquarantine.dylib", + "/usr/lib/system/libremovefile.dylib", + "/usr/lib/system/libsystem_asl.dylib", + "/usr/lib/system/libsystem_blocks.dylib", + "/usr/lib/system/libsystem_c.dylib", + "/usr/lib/system/libsystem_configuration.dylib", + "/usr/lib/system/libsystem_coreservices.dylib", + "/usr/lib/system/libsystem_darwin.dylib", + "/usr/lib/system/libsystem_dnssd.dylib", + "/usr/lib/system/libsystem_info.dylib", + "/usr/lib/system/libsystem_m.dylib", + "/usr/lib/system/libsystem_malloc.dylib", + "/usr/lib/system/libsystem_networkextension.dylib", + "/usr/lib/system/libsystem_notify.dylib", + "/usr/lib/system/libsystem_sandbox.dylib", + "/usr/lib/system/libsystem_secinit.dylib", + "/usr/lib/system/libsystem_kernel.dylib", + "/usr/lib/system/libsystem_platform.dylib", + "/usr/lib/system/libsystem_pthread.dylib", + "/usr/lib/system/libsystem_symptoms.dylib", + "/usr/lib/system/libsystem_trace.dylib", + "/usr/lib/system/libunwind.dylib", + "/usr/lib/system/libxpc.dylib", + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices", + "/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics", + "/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText", + "/System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO", + "/System/Library/Frameworks/ColorSync.framework/Versions/A/ColorSync", + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/ATS", + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSyncLegacy.framework/Versions/A/ColorSyncLegacy", + "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices", + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/HIServices", + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/LangAnalysis.framework/Versions/A/LangAnalysis", + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/PrintCore", + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/QD.framework/Versions/A/QD", + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/SpeechSynthesis.framework/Versions/A/SpeechSynthesis", + "/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight", + "/System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface", + "/usr/lib/libxml2.2.dylib", + "/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate", + "/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation", + "/usr/lib/libcompression.dylib", + "/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration", + "/System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay", + "/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit", + "/System/Library/Frameworks/Metal.framework/Versions/A/Metal", + "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders", + "/System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/A/MultitouchSupport", + "/System/Library/Frameworks/Security.framework/Versions/A/Security", + "/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore", + "/usr/lib/libbsm.0.dylib", + "/usr/lib/liblzma.5.dylib", + "/usr/lib/libauto.dylib", + "/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration", + "/usr/lib/libarchive.2.dylib", + "/usr/lib/liblangid.dylib", + "/usr/lib/libCRFSuite.dylib", + "/usr/lib/libenergytrace.dylib", + "/usr/lib/system/libkxld.dylib", + "/System/Library/PrivateFrameworks/AppleFSCompression.framework/Versions/A/AppleFSCompression", + "/usr/lib/libOpenScriptingUtil.dylib", + "/usr/lib/libcoretls.dylib", + "/usr/lib/libcoretls_cfhelpers.dylib", + "/usr/lib/libpam.2.dylib", + "/usr/lib/libsqlite3.dylib", + "/usr/lib/libxar.1.dylib", + "/usr/lib/libbz2.1.0.dylib", + "/usr/lib/libnetwork.dylib", + "/usr/lib/libapple_nghttp2.dylib", + "/usr/lib/libpcap.A.dylib", + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/FSEvents", + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore", + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata", + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/OSServices.framework/Versions/A/OSServices", + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SearchKit.framework/Versions/A/SearchKit", + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/AE.framework/Versions/A/AE", + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices", + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices", + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList", + "/System/Library/Frameworks/NetFS.framework/Versions/A/NetFS", + "/System/Library/PrivateFrameworks/NetAuth.framework/Versions/A/NetAuth", + "/System/Library/PrivateFrameworks/login.framework/Versions/A/Frameworks/loginsupport.framework/Versions/A/loginsupport", + "/System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC", + "/System/Library/PrivateFrameworks/CoreNLP.framework/Versions/A/CoreNLP", + "/System/Library/PrivateFrameworks/MetadataUtilities.framework/Versions/A/MetadataUtilities", + "/usr/lib/libmecabra.dylib", + "/usr/lib/libmecab.1.0.0.dylib", + "/usr/lib/libgermantok.dylib", + "/usr/lib/libThaiTokenizer.dylib", + "/usr/lib/libChineseTokenizer.dylib", + "/usr/lib/libiconv.2.dylib", + "/usr/lib/libcharset.1.dylib", + "/System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling", + "/System/Library/PrivateFrameworks/CoreEmoji.framework/Versions/A/CoreEmoji", + "/System/Library/PrivateFrameworks/Lexicon.framework/Versions/A/Lexicon", + "/System/Library/PrivateFrameworks/LinguisticData.framework/Versions/A/LinguisticData", + "/usr/lib/libcmph.dylib", + "/System/Library/Frameworks/CoreData.framework/Versions/A/CoreData", + "/System/Library/Frameworks/OpenDirectory.framework/Versions/A/Frameworks/CFOpenDirectory.framework/Versions/A/CFOpenDirectory", + "/System/Library/PrivateFrameworks/APFS.framework/Versions/A/APFS", + "/usr/lib/libutil.dylib", + "/System/Library/Frameworks/ServiceManagement.framework/Versions/A/ServiceManagement", + "/System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Versions/A/BackgroundTaskManagement", + "/usr/lib/libxslt.1.dylib", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/vImage", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvMisc.dylib", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLinearAlgebra.dylib", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparseBLAS.dylib", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libQuadrature.dylib", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBNNS.dylib", + "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparse.dylib", + "/System/Library/PrivateFrameworks/GPUWrangler.framework/Versions/A/GPUWrangler", + "/System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator", + "/System/Library/PrivateFrameworks/IOPresentment.framework/Versions/A/IOPresentment", + "/System/Library/PrivateFrameworks/DSExternalDisplay.framework/Versions/A/DSExternalDisplay", + "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreFSCache.dylib", + "/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSCore.framework/Versions/A/MPSCore", + "/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSImage.framework/Versions/A/MPSImage", + "/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSNeuralNetwork.framework/Versions/A/MPSNeuralNetwork", + "/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSMatrix.framework/Versions/A/MPSMatrix", + "/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSRayIntersector.framework/Versions/A/MPSRayIntersector", + "/System/Library/PrivateFrameworks/MetalTools.framework/Versions/A/MetalTools", + "/System/Library/PrivateFrameworks/AggregateDictionary.framework/Versions/A/AggregateDictionary", + "/usr/lib/libMobileGestalt.dylib", + "/System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage", + "/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo", + "/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL", + "/System/Library/PrivateFrameworks/GraphVisualizer.framework/Versions/A/GraphVisualizer", + "/System/Library/PrivateFrameworks/FaceCore.framework/Versions/A/FaceCore", + "/System/Library/Frameworks/OpenCL.framework/Versions/A/OpenCL", + "/usr/lib/libFosl_dynamic.dylib", + "/System/Library/PrivateFrameworks/OTSVG.framework/Versions/A/OTSVG", + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontParser.dylib", + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontRegistry.dylib", + "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib", + "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib", + "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib", + "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib", + "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib", + "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libRadiance.dylib", + "/System/Library/PrivateFrameworks/AppleJPEG.framework/Versions/A/AppleJPEG", + "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGFXShared.dylib", + "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLU.dylib", + "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib", + "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLImage.dylib", + "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCVMSPluginSupport.dylib", + "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreVMClient.dylib", + "/usr/lib/libcups.2.dylib", + "/System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos", + "/System/Library/Frameworks/GSS.framework/Versions/A/GSS", + "/usr/lib/libresolv.9.dylib", + "/System/Library/PrivateFrameworks/Heimdal.framework/Versions/A/Heimdal", + "/usr/lib/libheimdal-asn1.dylib", + "/System/Library/Frameworks/OpenDirectory.framework/Versions/A/OpenDirectory", + "/System/Library/PrivateFrameworks/CommonAuth.framework/Versions/A/CommonAuth", + "/System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation", + "/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio", + "/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox", + "/System/Library/PrivateFrameworks/AppleSauce.framework/Versions/A/AppleSauce", + "/System/Library/PrivateFrameworks/AssertionServices.framework/Versions/A/AssertionServices", + "/System/Library/PrivateFrameworks/BaseBoard.framework/Versions/A/BaseBoard" + ] +} \ No newline at end of file diff --git a/scripts/test-coverage.sh b/scripts/test-coverage.sh deleted file mode 100755 index de6807b61b..0000000000 --- a/scripts/test-coverage.sh +++ /dev/null @@ -1,2 +0,0 @@ -export NODE_OPTIONS=--max_old_space_size=4096 -cross-env COVERAGE=true karma start --single-run diff --git a/src/MCT.js b/src/MCT.js index 9eedf2bee7..f219368c97 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -266,6 +266,8 @@ define([ this.install(this.plugins.WebPage()); this.install(this.plugins.Condition()); this.install(this.plugins.ConditionWidget()); + this.install(this.plugins.URLTimeSettingsSynchronizer()); + this.install(this.plugins.NotificationIndicator()); } MCT.prototype = Object.create(EventEmitter.prototype); @@ -432,6 +434,10 @@ define([ plugin(this); }; + MCT.prototype.destroy = function () { + this.emit('destroy'); + }; + MCT.prototype.plugins = plugins; return MCT; diff --git a/src/MCTSpec.js b/src/MCTSpec.js index 53417f4314..65678e6e3d 100644 --- a/src/MCTSpec.js +++ b/src/MCTSpec.js @@ -23,7 +23,7 @@ define([ './plugins/plugins', 'legacyRegistry', - 'testUtils' + 'utils/testing' ], function (plugins, legacyRegistry, testUtils) { describe("MCT", function () { var openmct; @@ -32,6 +32,10 @@ define([ var mockListener; var oldBundles; + beforeAll(() => { + testUtils.resetApplicationState(); + }); + beforeEach(function () { mockPlugin = jasmine.createSpy('plugin'); mockPlugin2 = jasmine.createSpy('plugin2'); @@ -52,6 +56,7 @@ define([ legacyRegistry.delete(bundle); } }); + testUtils.resetApplicationState(openmct); }); it("exposes plugins", function () { diff --git a/src/adapter/bundle.js b/src/adapter/bundle.js index 94b1c73823..aca7248b21 100644 --- a/src/adapter/bundle.js +++ b/src/adapter/bundle.js @@ -29,7 +29,6 @@ define([ './capabilities/APICapabilityDecorator', './policies/AdaptedViewPolicy', './runs/AlternateCompositionInitializer', - './runs/TimeSettingsURLHandler', './runs/TypeDeprecationChecker', './runs/LegacyTelemetryProvider', './runs/RegisterLegacyTypes', @@ -46,7 +45,6 @@ define([ APICapabilityDecorator, AdaptedViewPolicy, AlternateCompositionInitializer, - TimeSettingsURLHandler, TypeDeprecationChecker, LegacyTelemetryProvider, RegisterLegacyTypes, @@ -134,16 +132,6 @@ define([ implementation: AlternateCompositionInitializer, depends: ["openmct"] }, - { - implementation: function (openmct, $location, $rootScope) { - return new TimeSettingsURLHandler( - openmct.time, - $location, - $rootScope - ); - }, - depends: ["openmct", "$location", "$rootScope"] - }, { implementation: LegacyTelemetryProvider, depends: [ diff --git a/src/adapter/runs/TimeSettingsURLHandler.js b/src/adapter/runs/TimeSettingsURLHandler.js deleted file mode 100644 index 1806cc66dc..0000000000 --- a/src/adapter/runs/TimeSettingsURLHandler.js +++ /dev/null @@ -1,150 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, 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([ - 'lodash' -], function ( - _ -) { - // Parameter names in query string - var SEARCH = { - MODE: 'tc.mode', - TIME_SYSTEM: 'tc.timeSystem', - START_BOUND: 'tc.startBound', - END_BOUND: 'tc.endBound', - START_DELTA: 'tc.startDelta', - END_DELTA: 'tc.endDelta' - }; - var TIME_EVENTS = ['bounds', 'timeSystem', 'clock', 'clockOffsets']; - // Used to shorthand calls to $location, which clears null parameters - var NULL_PARAMETERS = { key: null, start: null, end: null }; - - /** - * Communicates settings from the URL to the time API, - * and vice versa. - */ - function TimeSettingsURLHandler(time, $location, $rootScope) { - this.time = time; - this.$location = $location; - - $rootScope.$on('$locationChangeSuccess', this.updateTime.bind(this)); - - TIME_EVENTS.forEach(function (event) { - this.time.on(event, this.updateQueryParams.bind(this)); - }, this); - - this.updateTime(); // Initialize - } - - TimeSettingsURLHandler.prototype.updateQueryParams = function () { - var clock = this.time.clock(); - var fixed = !clock; - var mode = fixed ? 'fixed' : clock.key; - var timeSystem = this.time.timeSystem() || NULL_PARAMETERS; - var bounds = fixed ? this.time.bounds() : NULL_PARAMETERS; - var deltas = fixed ? NULL_PARAMETERS : this.time.clockOffsets(); - - bounds = bounds || NULL_PARAMETERS; - deltas = deltas || NULL_PARAMETERS; - if (deltas.start) { - deltas = { start: -deltas.start, end: deltas.end }; - } - - this.$location.search(SEARCH.MODE, mode); - this.$location.search(SEARCH.TIME_SYSTEM, timeSystem.key); - this.$location.search(SEARCH.START_BOUND, bounds.start); - this.$location.search(SEARCH.END_BOUND, bounds.end); - this.$location.search(SEARCH.START_DELTA, deltas.start); - this.$location.search(SEARCH.END_DELTA, deltas.end); - }; - - TimeSettingsURLHandler.prototype.parseQueryParams = function () { - var searchParams = _.pick(this.$location.search(), Object.values(SEARCH)); - var parsedParams = { - clock: searchParams[SEARCH.MODE], - timeSystem: searchParams[SEARCH.TIME_SYSTEM] - }; - if (!isNaN(parseInt(searchParams[SEARCH.START_DELTA], 0xA)) && - !isNaN(parseInt(searchParams[SEARCH.END_DELTA], 0xA))) { - parsedParams.clockOffsets = { - start: -searchParams[SEARCH.START_DELTA], - end: +searchParams[SEARCH.END_DELTA] - }; - } - if (!isNaN(parseInt(searchParams[SEARCH.START_BOUND], 0xA)) && - !isNaN(parseInt(searchParams[SEARCH.END_BOUND], 0xA))) { - parsedParams.bounds = { - start: +searchParams[SEARCH.START_BOUND], - end: +searchParams[SEARCH.END_BOUND] - }; - } - return parsedParams; - }; - - TimeSettingsURLHandler.prototype.updateTime = function () { - var params = this.parseQueryParams(); - if (_.isEqual(params, this.last)) { - return; // Do nothing; - } - this.last = params; - - if (!params.timeSystem) { - this.updateQueryParams(); - } else if (params.clock === 'fixed' && params.bounds) { - if (!this.time.timeSystem() || - this.time.timeSystem().key !== params.timeSystem) { - - this.time.timeSystem( - params.timeSystem, - params.bounds - ); - } else if (!_.isEqual(this.time.bounds(), params.bounds)) { - this.time.bounds(params.bounds); - } - if (this.time.clock()) { - this.time.stopClock(); - } - } else if (params.clockOffsets) { - if (params.clock === 'fixed') { - this.time.stopClock(); - return; - } - if (!this.time.clock() || - this.time.clock().key !== params.clock) { - - this.time.clock(params.clock, params.clockOffsets); - } else if (!_.isEqual(this.time.clockOffsets(), params.clockOffsets)) { - this.time.clockOffsets(params.clockOffsets); - } - if (!this.time.timeSystem() || - this.time.timeSystem().key !== params.timeSystem) { - - this.time.timeSystem(params.timeSystem); - } - } else { - // Neither found, update from timeSystem. - this.updateQueryParams(); - } - }; - - return TimeSettingsURLHandler; -}); diff --git a/src/adapter/runs/TimeSettingsURLHandlerSpec.js b/src/adapter/runs/TimeSettingsURLHandlerSpec.js deleted file mode 100644 index 0c5e8115d5..0000000000 --- a/src/adapter/runs/TimeSettingsURLHandlerSpec.js +++ /dev/null @@ -1,576 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, 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([ - './TimeSettingsURLHandler', - '../../api/time/TimeAPI' -], function ( - TimeSettingsURLHandler, - TimeAPI -) { - describe("TimeSettingsURLHandler", function () { - var time; - var $location; - var $rootScope; - var search; - var handler; // eslint-disable-line - var clockA; - var clockB; - var timeSystemA; - var timeSystemB; - var boundsA; - var boundsB; - var offsetsA; - var offsetsB; - var initialize; - var triggerLocationChange; - - - beforeEach(function () { - clockA = jasmine.createSpyObj('clockA', ['on', 'off']); - clockA.key = 'clockA'; - clockA.currentValue = function () { - return 1000; - }; - clockB = jasmine.createSpyObj('clockB', ['on', 'off']); - clockB.key = 'clockB'; - clockB.currentValue = function () { - return 2000; - }; - timeSystemA = {key: 'timeSystemA'}; - timeSystemB = {key: 'timeSystemB'}; - boundsA = { - start: 10, - end: 20 - }; - boundsB = { - start: 120, - end: 360 - }; - offsetsA = { - start: -100, - end: 0 - }; - offsetsB = { - start: -50, - end: 50 - }; - - time = new TimeAPI(); - - [ - 'on', - 'bounds', - 'clockOffsets', - 'timeSystem', - 'clock', - 'stopClock' - ].forEach(function (method) { - spyOn(time, method).and.callThrough(); - }); - time.addTimeSystem(timeSystemA); - time.addTimeSystem(timeSystemB); - time.addClock(clockA); - time.addClock(clockB); - - $location = jasmine.createSpyObj('$location', [ - 'search' - ]); - $rootScope = jasmine.createSpyObj('$rootScope', [ - '$on' - ]); - - search = {}; - $location.search.and.callFake(function (key, value) { - if (arguments.length === 0) { - return search; - } - if (value === null) { - delete search[key]; - } else { - search[key] = String(value); - } - return this; - }); - - expect(time.timeSystem()).toBeUndefined(); - expect(time.bounds()).toEqual({}); - expect(time.clockOffsets()).toBeUndefined(); - expect(time.clock()).toBeUndefined(); - - initialize = function () { - handler = new TimeSettingsURLHandler( - time, - $location, - $rootScope - ); - expect($rootScope.$on).toHaveBeenCalledWith( - '$locationChangeSuccess', - jasmine.any(Function) - ); - triggerLocationChange = $rootScope.$on.calls.mostRecent().args[1]; - - }; - }); - - it("initializes with missing time system", function () { - // This handles an odd transitory case where a url does not include - // a timeSystem. It's generally only experienced by those who - // based their code on the tutorial before it specified a time - // system. - search['tc.mode'] = 'clockA'; - search['tc.timeSystem'] = undefined; - search['tc.startDelta'] = '123'; - search['tc.endDelta'] = '456'; - - // We don't specify behavior right now other than "don't break." - expect(initialize).not.toThrow(); - }); - - it("can initalize fixed mode from location", function () { - search['tc.mode'] = 'fixed'; - search['tc.timeSystem'] = 'timeSystemA'; - search['tc.startBound'] = '123'; - search['tc.endBound'] = '456'; - - initialize(); - - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemA', - { - start: 123, - end: 456 - } - ); - }); - - it("can initialize clock mode from location", function () { - search['tc.mode'] = 'clockA'; - search['tc.timeSystem'] = 'timeSystemA'; - search['tc.startDelta'] = '123'; - search['tc.endDelta'] = '456'; - - initialize(); - - expect(time.clock).toHaveBeenCalledWith( - 'clockA', - { - start: -123, - end: 456 - } - ); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemA' - ); - }); - - it("can initialize fixed mode from time API", function () { - time.timeSystem(timeSystemA.key, boundsA); - initialize(); - expect($location.search) - .toHaveBeenCalledWith('tc.mode', 'fixed'); - expect($location.search) - .toHaveBeenCalledWith('tc.timeSystem', 'timeSystemA'); - expect($location.search) - .toHaveBeenCalledWith('tc.startBound', 10); - expect($location.search) - .toHaveBeenCalledWith('tc.endBound', 20); - expect($location.search) - .toHaveBeenCalledWith('tc.startDelta', null); - expect($location.search) - .toHaveBeenCalledWith('tc.endDelta', null); - }); - - it("can initialize clock mode from time API", function () { - time.clock(clockA.key, offsetsA); - time.timeSystem(timeSystemA.key); - initialize(); - expect($location.search) - .toHaveBeenCalledWith('tc.mode', 'clockA'); - expect($location.search) - .toHaveBeenCalledWith('tc.timeSystem', 'timeSystemA'); - expect($location.search) - .toHaveBeenCalledWith('tc.startBound', null); - expect($location.search) - .toHaveBeenCalledWith('tc.endBound', null); - expect($location.search) - .toHaveBeenCalledWith('tc.startDelta', 100); - expect($location.search) - .toHaveBeenCalledWith('tc.endDelta', 0); - }); - - describe('location changes in fixed mode', function () { - - beforeEach(function () { - time.timeSystem(timeSystemA.key, boundsA); - initialize(); - time.timeSystem.calls.reset(); - time.bounds.calls.reset(); - time.clock.calls.reset(); - time.stopClock.calls.reset(); - }); - - it("does not change on spurious location change", function () { - triggerLocationChange(); - expect(time.timeSystem).not.toHaveBeenCalledWith( - 'timeSystemA', - jasmine.any(Object) - ); - expect(time.bounds).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - expect(time.stopClock).not.toHaveBeenCalled(); - }); - - it("updates timeSystem changes", function () { - search['tc.timeSystem'] = 'timeSystemB'; - triggerLocationChange(); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemB', - { - start: 10, - end: 20 - } - ); - }); - - it("updates bounds changes", function () { - search['tc.startBound'] = '100'; - search['tc.endBound'] = '200'; - triggerLocationChange(); - expect(time.timeSystem).not.toHaveBeenCalledWith( - jasmine.anything(), jasmine.anything() - ); - expect(time.bounds).toHaveBeenCalledWith({ - start: 100, - end: 200 - }); - search['tc.endBound'] = '300'; - triggerLocationChange(); - expect(time.timeSystem).not.toHaveBeenCalledWith( - jasmine.anything(), jasmine.anything() - ); - expect(time.bounds).toHaveBeenCalledWith({ - start: 100, - end: 300 - }); - }); - - it("updates clock mode w/o timeSystem change", function () { - search['tc.mode'] = 'clockA'; - search['tc.startDelta'] = '50'; - search['tc.endDelta'] = '50'; - delete search['tc.endBound']; - delete search['tc.startBound']; - triggerLocationChange(); - expect(time.clock).toHaveBeenCalledWith( - 'clockA', - { - start: -50, - end: 50 - } - ); - expect(time.timeSystem).not.toHaveBeenCalledWith( - jasmine.anything(), jasmine.anything() - ); - }); - - it("updates clock mode and timeSystem", function () { - search['tc.mode'] = 'clockA'; - search['tc.startDelta'] = '50'; - search['tc.endDelta'] = '50'; - search['tc.timeSystem'] = 'timeSystemB'; - delete search['tc.endBound']; - delete search['tc.startBound']; - triggerLocationChange(); - expect(time.clock).toHaveBeenCalledWith( - 'clockA', - { - start: -50, - end: 50 - } - ); - expect(time.timeSystem).toHaveBeenCalledWith('timeSystemB'); - }); - }); - - describe('location changes in clock mode', function () { - - beforeEach(function () { - time.clock(clockA.key, offsetsA); - time.timeSystem(timeSystemA.key); - initialize(); - time.timeSystem.calls.reset(); - time.bounds.calls.reset(); - time.clock.calls.reset(); - time.clockOffsets.calls.reset(); - time.stopClock.calls.reset(); - }); - - it("does not change on spurious location change", function () { - triggerLocationChange(); - expect(time.timeSystem).not.toHaveBeenCalledWith( - 'timeSystemA', - jasmine.any(Object) - ); - expect(time.clockOffsets).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - expect(time.clock).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - expect(time.bounds).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - }); - - it("changes time system", function () { - search['tc.timeSystem'] = 'timeSystemB'; - triggerLocationChange(); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemB' - ); - expect(time.clockOffsets).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - expect(time.clock).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - expect(time.stopClock).not.toHaveBeenCalled(); - expect(time.bounds).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - }); - - it("changes offsets", function () { - search['tc.startDelta'] = '50'; - search['tc.endDelta'] = '50'; - triggerLocationChange(); - expect(time.timeSystem).not.toHaveBeenCalledWith( - 'timeSystemA', - jasmine.any(Object) - ); - expect(time.clockOffsets).toHaveBeenCalledWith( - { - start: -50, - end: 50 - } - ); - expect(time.clock).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - }); - - it("updates to fixed w/o timeSystem change", function () { - search['tc.mode'] = 'fixed'; - search['tc.startBound'] = '234'; - search['tc.endBound'] = '567'; - delete search['tc.endDelta']; - delete search['tc.startDelta']; - - triggerLocationChange(); - expect(time.stopClock).toHaveBeenCalled(); - expect(time.bounds).toHaveBeenCalledWith({ - start: 234, - end: 567 - }); - expect(time.timeSystem).not.toHaveBeenCalledWith( - jasmine.anything(), jasmine.anything() - ); - }); - - it("updates fixed and timeSystem", function () { - search['tc.mode'] = 'fixed'; - search['tc.startBound'] = '234'; - search['tc.endBound'] = '567'; - search['tc.timeSystem'] = 'timeSystemB'; - delete search['tc.endDelta']; - delete search['tc.startDelta']; - - triggerLocationChange(); - expect(time.stopClock).toHaveBeenCalled(); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemB', - { - start: 234, - end: 567 - } - ); - }); - - it("updates clock", function () { - search['tc.mode'] = 'clockB'; - triggerLocationChange(); - expect(time.clock).toHaveBeenCalledWith( - 'clockB', - { - start: -100, - end: 0 - } - ); - expect(time.timeSystem).not.toHaveBeenCalledWith(jasmine.anything()); - }); - - it("updates clock and timeSystem", function () { - search['tc.mode'] = 'clockB'; - search['tc.timeSystem'] = 'timeSystemB'; - triggerLocationChange(); - expect(time.clock).toHaveBeenCalledWith( - 'clockB', - { - start: -100, - end: 0 - } - ); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemB' - ); - }); - - it("updates clock and timeSystem and offsets", function () { - search['tc.mode'] = 'clockB'; - search['tc.timeSystem'] = 'timeSystemB'; - search['tc.startDelta'] = '50'; - search['tc.endDelta'] = '50'; - triggerLocationChange(); - expect(time.clock).toHaveBeenCalledWith( - 'clockB', - { - start: -50, - end: 50 - } - ); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemB' - ); - }); - - it("stops the clock", function () { - // this is a robustness test, unsure if desired, requires - // user to be manually editing location strings. - search['tc.mode'] = 'fixed'; - triggerLocationChange(); - expect(time.stopClock).toHaveBeenCalled(); - }); - }); - - - describe("location updates from time API in fixed", function () { - beforeEach(function () { - time.timeSystem(timeSystemA.key, boundsA); - initialize(); - }); - - it("updates on bounds change", function () { - time.bounds(boundsB); - expect(search).toEqual({ - 'tc.mode': 'fixed', - 'tc.startBound': '120', - 'tc.endBound': '360', - 'tc.timeSystem': 'timeSystemA' - }); - }); - - it("updates on timeSystem change", function () { - time.timeSystem(timeSystemB, boundsA); - expect(search).toEqual({ - 'tc.mode': 'fixed', - 'tc.startBound': '10', - 'tc.endBound': '20', - 'tc.timeSystem': 'timeSystemB' - }); - time.timeSystem(timeSystemA, boundsB); - expect(search).toEqual({ - 'tc.mode': 'fixed', - 'tc.startBound': '120', - 'tc.endBound': '360', - 'tc.timeSystem': 'timeSystemA' - }); - }); - - it("Updates to clock", function () { - time.clock(clockA, offsetsA); - expect(search).toEqual({ - 'tc.mode': 'clockA', - 'tc.startDelta': '100', - 'tc.endDelta': '0', - 'tc.timeSystem': 'timeSystemA' - }); - }); - }); - - describe("location updates from time API in fixed", function () { - beforeEach(function () { - time.clock(clockA.key, offsetsA); - time.timeSystem(timeSystemA.key); - initialize(); - }); - - it("updates offsets", function () { - time.clockOffsets(offsetsB); - expect(search).toEqual({ - 'tc.mode': 'clockA', - 'tc.startDelta': '50', - 'tc.endDelta': '50', - 'tc.timeSystem': 'timeSystemA' - }); - }); - - it("updates clocks", function () { - time.clock(clockB, offsetsA); - expect(search).toEqual({ - 'tc.mode': 'clockB', - 'tc.startDelta': '100', - 'tc.endDelta': '0', - 'tc.timeSystem': 'timeSystemA' - }); - time.clock(clockA, offsetsB); - expect(search).toEqual({ - 'tc.mode': 'clockA', - 'tc.startDelta': '50', - 'tc.endDelta': '50', - 'tc.timeSystem': 'timeSystemA' - }); - }); - - it("updates timesystems", function () { - time.timeSystem(timeSystemB); - expect(search).toEqual({ - 'tc.mode': 'clockA', - 'tc.startDelta': '100', - 'tc.endDelta': '0', - 'tc.timeSystem': 'timeSystemB' - }); - }); - - it("stops the clock", function () { - time.stopClock(); - expect(search).toEqual({ - 'tc.mode': 'fixed', - 'tc.startBound': '900', - 'tc.endBound': '1000', - 'tc.timeSystem': 'timeSystemA' - }); - }); - }); - }); -}); diff --git a/src/api/notifications/NotificationAPI.js b/src/api/notifications/NotificationAPI.js index 505af91b47..ec092ec900 100644 --- a/src/api/notifications/NotificationAPI.js +++ b/src/api/notifications/NotificationAPI.js @@ -128,6 +128,11 @@ export default class NotificationAPI extends EventEmitter { return this._notify(notificationModel); } + dismissAllNotifications() { + this.notifications = []; + this.emit('dismiss-all'); + } + /** * Minimize a notification. The notification will still be available * from the notification list. Typically notifications with a diff --git a/src/plugins/LADTable/components/LADRow.vue b/src/plugins/LADTable/components/LADRow.vue index 1c2694758a..0c481a7d02 100644 --- a/src/plugins/LADTable/components/LADRow.vue +++ b/src/plugins/LADTable/components/LADRow.vue @@ -1,6 +1,6 @@ /***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government + * Open MCT, Copyright (c) 2014-2020, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * @@ -24,10 +24,8 @@ @@ -52,16 +50,22 @@ export default { return { name: this.domainObject.name, - timestamp: '---', + timestamp: undefined, value: '---', valueClass: '', currentObjectPath } }, + computed: { + formattedTimestamp() { + return this.timestamp !== undefined ? this.formats[this.timestampKey].format(this.timestamp) : '---'; + } + }, mounted() { this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.formats = this.openmct.telemetry.getFormatMap(this.metadata); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); + this.bounds = this.openmct.time.bounds(); this.limitEvaluator = this.openmct .telemetry @@ -76,6 +80,7 @@ export default { ); this.openmct.time.on('timeSystem', this.updateTimeSystem); + this.openmct.time.on('bounds', this.updateBounds); this.timestampKey = this.openmct.time.timeSystem().key; @@ -89,43 +94,63 @@ export default { .telemetry .subscribe(this.domainObject, this.updateValues); - this.openmct - .telemetry - .request(this.domainObject, {strategy: 'latest'}) - .then((array) => this.updateValues(array[array.length - 1])); + this.requestHistory(); }, destroyed() { this.stopWatchingMutation(); this.unsubscribe(); - this.openmct.off('timeSystem', this.updateTimeSystem); + this.openmct.time.off('timeSystem', this.updateTimeSystem); + this.openmct.time.off('bounds', this.updateBounds); }, methods: { updateValues(datum) { - this.timestamp = this.formats[this.timestampKey].format(datum); - this.value = this.formats[this.valueKey].format(datum); + let newTimestamp = this.formats[this.timestampKey].parse(datum), + limit; - var limit = this.limitEvaluator.evaluate(datum, this.valueMetadata); - - if (limit) { - this.valueClass = limit.cssClass; - } else { - this.valueClass = ''; + if(this.shouldUpdate(newTimestamp)) { + this.timestamp = this.formats[this.timestampKey].parse(datum); + this.value = this.formats[this.valueKey].format(datum); + limit = this.limitEvaluator.evaluate(datum, this.valueMetadata); + if (limit) { + this.valueClass = limit.cssClass; + } else { + this.valueClass = ''; + } } }, + shouldUpdate(newTimestamp) { + return (this.timestamp === undefined) || + (this.inBounds(newTimestamp) && + newTimestamp > this.timestamp); + }, + requestHistory() { + this.openmct + .telemetry + .request(this.domainObject, { + start: this.bounds.start, + end: this.bounds.end, + size: 1, + strategy: 'latest' + }) + .then((array) => this.updateValues(array[array.length - 1])); + }, updateName(name) { this.name = name; }, + updateBounds(bounds, isTick) { + this.bounds = bounds; + if(!isTick) { + this.requestHistory(); + } + }, + inBounds(timestamp) { + return timestamp >= this.bounds.start && timestamp <= this.bounds.end; + }, updateTimeSystem(timeSystem) { this.value = '---'; this.timestamp = '---'; this.valueClass = ''; this.timestampKey = timeSystem.key; - - this.openmct - .telemetry - .request(this.domainObject, {strategy: 'latest'}) - .then((array) => this.updateValues(array[array.length - 1])); - }, showContextMenu(event) { this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); diff --git a/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js b/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js new file mode 100644 index 0000000000..878b66d221 --- /dev/null +++ b/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js @@ -0,0 +1,230 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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 { + getAllSearchParams, + setAllSearchParams +} from 'utils/openmctLocation'; + +const TIME_EVENTS = ['bounds', 'timeSystem', 'clock', 'clockOffsets']; +const SEARCH_MODE = 'tc.mode'; +const SEARCH_TIME_SYSTEM = 'tc.timeSystem'; +const SEARCH_START_BOUND = 'tc.startBound'; +const SEARCH_END_BOUND = 'tc.endBound'; +const SEARCH_START_DELTA = 'tc.startDelta'; +const SEARCH_END_DELTA = 'tc.endDelta'; +const MODE_FIXED = 'fixed'; + +export default class URLTimeSettingsSynchronizer { + constructor(openmct) { + this.openmct = openmct; + this.isUrlUpdateInProgress = false; + + this.initialize = this.initialize.bind(this); + this.destroy = this.destroy.bind(this); + this.updateTimeSettings = this.updateTimeSettings.bind(this); + this.setUrlFromTimeApi = this.setUrlFromTimeApi.bind(this); + + openmct.on('start', this.initialize); + openmct.on('destroy', this.destroy); + } + + initialize() { + this.updateTimeSettings(); + + window.addEventListener('hashchange', this.updateTimeSettings); + TIME_EVENTS.forEach(event => { + this.openmct.time.on(event, this.setUrlFromTimeApi); + }); + + } + + destroy() { + window.removeEventListener('hashchange', this.updateTimeSettings); + this.openmct.off('start', this.initialize); + this.openmct.off('destroy', this.destroy); + + TIME_EVENTS.forEach(event => { + this.openmct.time.off(event, this.setUrlFromTimeApi); + }); + } + + updateTimeSettings() { + // Prevent from triggering self + if (!this.isUrlUpdateInProgress) { + let timeParameters = this.parseParametersFromUrl(); + + + if (this.areTimeParametersValid(timeParameters)) { + this.setTimeApiFromUrl(timeParameters); + } else { + this.setUrlFromTimeApi(); + } + } else { + this.isUrlUpdateInProgress = false; + } + } + + parseParametersFromUrl() { + let searchParams = getAllSearchParams(); + + let mode = searchParams.get(SEARCH_MODE); + let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM); + + let startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10); + let endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10); + let bounds = { + start: startBound, + end: endBound + }; + + let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10); + let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10); + let clockOffsets = { + start: 0 - startOffset, + end: endOffset + }; + + return { + mode, + timeSystem, + bounds, + clockOffsets + }; + } + + setTimeApiFromUrl(timeParameters) { + if (timeParameters.mode === 'fixed') { + if (this.openmct.time.timeSystem().key !== timeParameters.timeSystem) { + this.openmct.time.timeSystem( + timeParameters.timeSystem, + timeParameters.bounds + ); + } else if (!this.areStartAndEndEqual(this.openmct.time.bounds(), timeParameters.bounds)) { + this.openmct.time.bounds(timeParameters.bounds); + } + if (this.openmct.time.clock()) { + this.openmct.time.stopClock(); + } + } else { + if (!this.openmct.time.clock() || + this.openmct.time.clock().key !== timeParameters.mode) { + this.openmct.time.clock(timeParameters.mode, timeParameters.clockOffsets); + } else if (!this.areStartAndEndEqual(this.openmct.time.clockOffsets(), timeParameters.clockOffsets)) { + this.openmct.time.clockOffsets(timeParameters.clockOffsets); + } + if (!this.openmct.time.timeSystem() || + this.openmct.time.timeSystem().key !== timeParameters.timeSystem) { + this.openmct.time.timeSystem(timeParameters.timeSystem); + } + } + } + + setUrlFromTimeApi() { + let searchParams = getAllSearchParams(); + let clock = this.openmct.time.clock(); + let bounds = this.openmct.time.bounds(); + let clockOffsets = this.openmct.time.clockOffsets(); + + if (clock === undefined) { + searchParams.set(SEARCH_MODE, MODE_FIXED); + searchParams.set(SEARCH_START_BOUND, bounds.start); + searchParams.set(SEARCH_END_BOUND, bounds.end); + + searchParams.delete(SEARCH_START_DELTA); + searchParams.delete(SEARCH_END_DELTA); + } else { + searchParams.set(SEARCH_MODE, clock.key); + + if (clockOffsets !== undefined) { + searchParams.set(SEARCH_START_DELTA, 0 - clockOffsets.start); + searchParams.set(SEARCH_END_DELTA, clockOffsets.end); + } else { + searchParams.delete(SEARCH_START_DELTA); + searchParams.delete(SEARCH_END_DELTA); + } + searchParams.delete(SEARCH_START_BOUND); + searchParams.delete(SEARCH_END_BOUND); + } + + searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key); + this.isUrlUpdateInProgress = true; + setAllSearchParams(searchParams); + } + + areTimeParametersValid(timeParameters) { + let isValid = false; + + if (this.isModeValid(timeParameters.mode) && + this.isTimeSystemValid(timeParameters.timeSystem)) { + + if (timeParameters.mode === 'fixed') { + isValid = this.areStartAndEndValid(timeParameters.bounds); + } else { + isValid = this.areStartAndEndValid(timeParameters.clockOffsets); + } + } + + return isValid; + } + + areStartAndEndValid(bounds) { + return bounds !== undefined && + bounds.start !== undefined && + bounds.start !== null && + bounds.end !== undefined && + bounds.start !== null && + !isNaN(bounds.start) && + !isNaN(bounds.end); + } + + isTimeSystemValid(timeSystem) { + let isValid = timeSystem !== undefined; + if (isValid) { + let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem); + isValid = timeSystemObject !== undefined; + } + return isValid; + } + + isModeValid(mode) { + let isValid = false; + + if (mode !== undefined && + mode !== null) { + isValid = true; + } + + if (isValid) { + if (mode.toLowerCase() === MODE_FIXED) { + isValid = true; + } else { + isValid = this.openmct.time.clocks.get(mode) !== undefined; + } + } + return isValid; + } + + areStartAndEndEqual(firstBounds, secondBounds) { + return firstBounds.start === secondBounds.start && + firstBounds.end === secondBounds.end; + } +} diff --git a/platform/commonUI/notification/src/NotificationIndicator.js b/src/plugins/URLTimeSettingsSynchronizer/plugin.js similarity index 81% rename from platform/commonUI/notification/src/NotificationIndicator.js rename to src/plugins/URLTimeSettingsSynchronizer/plugin.js index af399985c9..a6db4dfcf5 100644 --- a/platform/commonUI/notification/src/NotificationIndicator.js +++ b/src/plugins/URLTimeSettingsSynchronizer/plugin.js @@ -1,5 +1,5 @@ /***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government + * Open MCT, Copyright (c) 2014-2020, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * @@ -19,15 +19,10 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +import URLTimeSettingsSynchronizer from "./URLTimeSettingsSynchronizer.js"; -define( - [], - function () { - - function NotificationIndicator() {} - - NotificationIndicator.template = 'notificationIndicatorTemplate'; - - return NotificationIndicator; +export default function () { + return function install(openmct) { + return new URLTimeSettingsSynchronizer(openmct); } -); +} diff --git a/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js new file mode 100644 index 0000000000..49943d4581 --- /dev/null +++ b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js @@ -0,0 +1,307 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +import { + createOpenMct, + resetApplicationState +} from 'utils/testing'; + +describe("The URLTimeSettingsSynchronizer", () => { + let openmct; + let testClock; + beforeAll(() => resetApplicationState()); + + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(openmct.plugins.LocalTimeSystem()); + testClock = jasmine.createSpyObj("testClock", ["start", "stop", "tick", "currentValue", "on", "off"]); + testClock.key = "test-clock"; + testClock.currentValue.and.returnValue(0); + + openmct.time.addClock(testClock); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => resetApplicationState(openmct)); + + describe("realtime mode", () => { + it("when the clock is set via the time API, it is immediately reflected in the URL", () => { + //Test expected initial conditions + expect(window.location.hash.includes('tc.mode=fixed')).toBe(true); + + openmct.time.clock('local', {start: -1000, end: 100}); + + expect(window.location.hash.includes('tc.mode=local')).toBe(true); + + //Test that expected initial conditions are no longer true + expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); + }); + it("when offsets are set via the time API, they are immediately reflected in the URL", () => { + //Test expected initial conditions + expect(window.location.hash.includes('tc.startDelta')).toBe(false); + expect(window.location.hash.includes('tc.endDelta')).toBe(false); + + openmct.time.clock('local', {start: -1000, end: 100}); + expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true); + expect(window.location.hash.includes('tc.endDelta=100')).toBe(true); + + openmct.time.clockOffsets({start: -2000, end: 200}); + expect(window.location.hash.includes('tc.startDelta=2000')).toBe(true); + expect(window.location.hash.includes('tc.endDelta=200')).toBe(true); + + //Test that expected initial conditions are no longer true + expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); + }); + describe("when set in the url", () => { + it("will change from fixed to realtime mode when the mode changes", () => { + expectLocationToBeInFixedMode(); + return switchToRealtimeMode().then(() => { + let clock = openmct.time.clock(); + + expect(clock).toBeDefined(); + expect(clock.key).toBe('local'); + }); + }); + it("the clock is correctly set in the API from the URL parameters", () => { + return switchToRealtimeMode().then(() => { + let resolveFunction; + return new Promise((resolve) => { + resolveFunction = resolve; + + //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been + //detected in the API. + openmct.time.on('clock', resolveFunction); + let hash = window.location.hash; + hash = hash.replace('tc.mode=local', 'tc.mode=test-clock'); + window.location.hash = hash; + }).then(() => { + let clock = openmct.time.clock(); + expect(clock).toBeDefined(); + expect(clock.key).toBe('test-clock'); + openmct.time.off('clock', resolveFunction); + }); + }); + }); + it("the clock offsets are correctly set in the API from the URL parameters", () => { + return switchToRealtimeMode().then(() => { + let resolveFunction; + return new Promise((resolve) => { + resolveFunction = resolve; + //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been + //detected in the API. + openmct.time.on('clockOffsets', resolveFunction); + let hash = window.location.hash; + hash = hash.replace('tc.startDelta=1000', 'tc.startDelta=2000'); + hash = hash.replace('tc.endDelta=100', 'tc.endDelta=200'); + window.location.hash = hash; + }).then(() => { + let clockOffsets = openmct.time.clockOffsets(); + expect(clockOffsets).toBeDefined(); + expect(clockOffsets.start).toBe(-2000); + expect(clockOffsets.end).toBe(200); + openmct.time.off('clockOffsets', resolveFunction); + }); + }); + }); + it("the time system is correctly set in the API from the URL parameters", () => { + return switchToRealtimeMode().then(() => { + let resolveFunction; + return new Promise((resolve) => { + resolveFunction = resolve; + + //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been + //detected in the API. + openmct.time.on('timeSystem', resolveFunction); + let hash = window.location.hash; + hash = hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local'); + window.location.hash = hash; + }).then(() => { + let timeSystem = openmct.time.timeSystem(); + expect(timeSystem).toBeDefined(); + expect(timeSystem.key).toBe('local'); + openmct.time.off('timeSystem', resolveFunction); + }); + }); + }); + }); + }); + describe("fixed timespan mode", () => { + beforeEach(() => { + openmct.time.stopClock(); + openmct.time.timeSystem('utc', {start: 0, end: 1}); + }); + + it("when bounds are set via the time API, they are immediately reflected in the URL", ()=>{ + //Test expected initial conditions + expect(window.location.hash.includes('tc.startBound=0')).toBe(true); + expect(window.location.hash.includes('tc.endBound=1')).toBe(true); + + openmct.time.bounds({start: 10, end: 20}); + + expect(window.location.hash.includes('tc.startBound=10')).toBe(true); + expect(window.location.hash.includes('tc.endBound=20')).toBe(true); + + //Test that expected initial conditions are no longer true + expect(window.location.hash.includes('tc.startBound=0')).toBe(false); + expect(window.location.hash.includes('tc.endBound=1')).toBe(false); + }); + + it("when time system is set via the time API, it is immediately reflected in the URL", ()=>{ + //Test expected initial conditions + expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(true); + + openmct.time.timeSystem('local', {start: 20, end: 30}); + + expect(window.location.hash.includes('tc.timeSystem=local')).toBe(true); + + //Test that expected initial conditions are no longer true + expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(false); + }); + describe("when set in the url", () => { + it("time system changes are reflected in the API", () => { + let resolveFunction; + + return new Promise((resolve) => { + let timeSystem = openmct.time.timeSystem(); + resolveFunction = resolve; + + expect(timeSystem.key).toBe('utc'); + window.location.hash = window.location.hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local'); + + openmct.time.on('timeSystem', resolveFunction); + }).then(() => { + let timeSystem = openmct.time.timeSystem(); + expect(timeSystem.key).toBe('local'); + + openmct.time.off('timeSystem', resolveFunction); + }); + }); + it("mode can be changed from realtime to fixed", () => { + return switchToRealtimeMode().then(() => { + expectLocationToBeInRealtimeMode(); + + expect(openmct.time.clock()).toBeDefined(); + }).then(switchToFixedMode).then(() => { + let clock = openmct.time.clock(); + expect(clock).not.toBeDefined(); + }); + }); + it("bounds are correctly set in the API from the URL parameters", () => { + let resolveFunction; + + expectLocationToBeInFixedMode(); + + return new Promise((resolve) => { + resolveFunction = resolve; + openmct.time.on('bounds', resolveFunction); + let hash = window.location.hash; + hash = hash.replace('tc.startBound=0', 'tc.startBound=222') + .replace('tc.endBound=1', 'tc.endBound=333'); + window.location.hash = hash; + }).then(() => { + let bounds = openmct.time.bounds(); + + expect(bounds).toBeDefined(); + expect(bounds.start).toBe(222); + expect(bounds.end).toBe(333); + }); + }); + it("bounds are correctly set in the API from the URL parameters where only the end bound changes", () => { + let resolveFunction; + + expectLocationToBeInFixedMode(); + + return new Promise((resolve) => { + resolveFunction = resolve; + openmct.time.on('bounds', resolveFunction); + let hash = window.location.hash; + hash = hash.replace('tc.endBound=1', 'tc.endBound=333'); + window.location.hash = hash; + }).then(() => { + let bounds = openmct.time.bounds(); + + expect(bounds).toBeDefined(); + expect(bounds.start).toBe(0); + expect(bounds.end).toBe(333); + }); + }); + }); + }); + + function setRealtimeLocationParameters() { + let hash = window.location.hash.toString() + .replace('tc.mode=fixed', 'tc.mode=local') + .replace('tc.startBound=0', 'tc.startDelta=1000') + .replace('tc.endBound=1', 'tc.endDelta=100'); + + window.location.hash = hash; + } + + function setFixedLocationParameters() { + let hash = window.location.hash.toString() + .replace('tc.mode=local', 'tc.mode=fixed') + .replace('tc.timeSystem=utc', 'tc.timeSystem=local') + .replace('tc.startDelta=1000', 'tc.startBound=50') + .replace('tc.endDelta=100', 'tc.endBound=60'); + + window.location.hash = hash; + } + + function switchToRealtimeMode() { + let resolveFunction; + return new Promise((resolve) => { + resolveFunction = resolve; + openmct.time.on('clock', resolveFunction); + setRealtimeLocationParameters(); + }).then(() => { + openmct.time.off('clock', resolveFunction); + }); + } + + function switchToFixedMode() { + let resolveFunction; + return new Promise((resolve) => { + resolveFunction = resolve; + //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been + //detected in the API. + openmct.time.on('clock', resolveFunction); + setFixedLocationParameters(); + }).then(() => { + openmct.time.off('clock', resolveFunction); + }); + } + + function expectLocationToBeInRealtimeMode() { + expect(window.location.hash.includes('tc.mode=local')).toBe(true); + expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true); + expect(window.location.hash.includes('tc.endDelta=100')).toBe(true); + expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); + } + + function expectLocationToBeInFixedMode() { + expect(window.location.hash.includes('tc.mode=fixed')).toBe(true); + expect(window.location.hash.includes('tc.startBound=0')).toBe(true); + expect(window.location.hash.includes('tc.endBound=1')).toBe(true); + expect(window.location.hash.includes('tc.mode=local')).toBe(false); + } +}); diff --git a/src/plugins/condition/Condition.js b/src/plugins/condition/Condition.js index e99ac0e384..dfd21f0636 100644 --- a/src/plugins/condition/Condition.js +++ b/src/plugins/condition/Condition.js @@ -26,6 +26,7 @@ import TelemetryCriterion from "./criterion/TelemetryCriterion"; import { evaluateResults } from './utils/evaluator'; import { getLatestTimestamp } from './utils/time'; import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion"; +import {TRIGGER_CONJUNCTION, TRIGGER_LABEL} from "./utils/constants"; /* * conditionConfiguration = { @@ -41,13 +42,14 @@ import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion"; * ] * } */ -export default class ConditionClass extends EventEmitter { +export default class Condition extends EventEmitter { /** * Manages criteria and emits the result of - true or false - based on criteria evaluated. * @constructor * @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} } * @param openmct + * @param conditionManager */ constructor(conditionConfiguration, openmct, conditionManager) { super(); @@ -62,6 +64,7 @@ export default class ConditionClass extends EventEmitter { this.createCriteria(conditionConfiguration.configuration.criteria); } this.trigger = conditionConfiguration.configuration.trigger; + this.description = ''; } getResult(datum) { @@ -109,7 +112,7 @@ export default class ConditionClass extends EventEmitter { return { id: criterionConfiguration.id || uuid(), telemetry: criterionConfiguration.telemetry || '', - telemetryObject: this.conditionManager.telemetryObjects[this.openmct.objects.makeKeyString(criterionConfiguration.telemetry)], + telemetryObjects: this.conditionManager.telemetryObjects, operation: criterionConfiguration.operation || '', input: criterionConfiguration.input === undefined ? [] : criterionConfiguration.input, metadata: criterionConfiguration.metadata || '' @@ -120,6 +123,7 @@ export default class ConditionClass extends EventEmitter { criterionConfigurations.forEach((criterionConfiguration) => { this.addCriterion(criterionConfiguration); }); + this.updateDescription(); } updateCriteria(criterionConfigurations) { @@ -127,10 +131,11 @@ export default class ConditionClass extends EventEmitter { this.createCriteria(criterionConfigurations); } - updateTelemetry() { + updateTelemetryObjects() { this.criteria.forEach((criterion) => { - criterion.updateTelemetry(this.conditionManager.telemetryObjects); + criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects); }); + this.updateDescription(); } /** @@ -178,6 +183,7 @@ export default class ConditionClass extends EventEmitter { criterion.unsubscribe(); criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); this.criteria.splice(found.index, 1, newCriterion); + this.updateDescription(); } } @@ -190,6 +196,7 @@ export default class ConditionClass extends EventEmitter { }); criterion.destroy(); this.criteria.splice(found.index, 1); + this.updateDescription(); return true; } @@ -200,9 +207,30 @@ export default class ConditionClass extends EventEmitter { let found = this.findCriterion(criterion.id); if (found) { this.criteria[found.index] = criterion.data; + this.updateDescription(); } } + updateDescription() { + const triggerDescription = this.getTriggerDescription(); + let description = ''; + this.criteria.forEach((criterion, index) => { + if (!index) { + description = `Match if ${triggerDescription.prefix}`; + } + description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`; + }); + this.description = description; + this.conditionManager.updateConditionDescription(this); + } + + getTriggerDescription() { + return { + conjunction: TRIGGER_CONJUNCTION[this.trigger], + prefix: `${TRIGGER_LABEL[this.trigger]}: ` + }; + } + requestLADConditionResult() { let latestTimestamp; let criteriaResults = {}; diff --git a/src/plugins/condition/ConditionManager.js b/src/plugins/condition/ConditionManager.js index 7c0bbb7a69..a29f338be7 100644 --- a/src/plugins/condition/ConditionManager.js +++ b/src/plugins/condition/ConditionManager.js @@ -57,7 +57,7 @@ export default class ConditionManager extends EventEmitter { endpoint, this.telemetryReceived.bind(this, endpoint) ); - this.updateConditionTelemetry(); + this.updateConditionTelemetryObjects(); } unsubscribeFromTelemetry(endpointIdentifier) { @@ -70,11 +70,11 @@ export default class ConditionManager extends EventEmitter { this.subscriptions[id](); delete this.subscriptions[id]; delete this.telemetryObjects[id]; - this.removeConditionTelemetry(); + this.removeConditionTelemetryObjects(); } initialize() { - this.conditionClassCollection = []; + this.conditions = []; if (this.conditionSetDomainObject.configuration.conditionCollection.length) { this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => { this.initCondition(conditionConfiguration, index); @@ -82,13 +82,14 @@ export default class ConditionManager extends EventEmitter { } } - updateConditionTelemetry() { - this.conditionClassCollection.forEach((condition) => condition.updateTelemetry()); + updateConditionTelemetryObjects() { + this.conditions.forEach((condition) => condition.updateTelemetryObjects()); } - removeConditionTelemetry() { + removeConditionTelemetryObjects() { let conditionsChanged = false; - this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration) => { + this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, conditionIndex) => { + let conditionChanged = false; conditionConfiguration.configuration.criteria.forEach((criterion, index) => { const isAnyAllTelemetry = criterion.telemetry && (criterion.telemetry === 'any' || criterion.telemetry === 'all'); if (!isAnyAllTelemetry) { @@ -100,10 +101,14 @@ export default class ConditionManager extends EventEmitter { criterion.metadata = ''; criterion.input = []; criterion.operation = ''; - conditionsChanged = true; + conditionChanged = true; } } }); + if (conditionChanged) { + this.updateCondition(conditionConfiguration, conditionIndex); + conditionsChanged = true; + } }); if (conditionsChanged) { this.persistConditions(); @@ -111,18 +116,24 @@ export default class ConditionManager extends EventEmitter { } updateCondition(conditionConfiguration, index) { - let condition = this.conditionClassCollection[index]; - condition.update(conditionConfiguration); + let condition = this.conditions[index]; this.conditionSetDomainObject.configuration.conditionCollection[index] = conditionConfiguration; + condition.update(conditionConfiguration); + this.persistConditions(); + } + + updateConditionDescription(condition) { + const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id)); + found.summary = condition.description; this.persistConditions(); } initCondition(conditionConfiguration, index) { let condition = new Condition(conditionConfiguration, this.openmct, this); if (index !== undefined) { - this.conditionClassCollection.splice(index + 1, 0, condition); + this.conditions.splice(index + 1, 0, condition); } else { - this.conditionClassCollection.unshift(condition); + this.conditions.unshift(condition); } } @@ -181,15 +192,15 @@ export default class ConditionManager extends EventEmitter { } removeCondition(index) { - let condition = this.conditionClassCollection[index]; + let condition = this.conditions[index]; condition.destroy(); - this.conditionClassCollection.splice(index, 1); + this.conditions.splice(index, 1); this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1); this.persistConditions(); } findConditionById(id) { - return this.conditionClassCollection.find(conditionClass => conditionClass.id === id); + return this.conditions.find(condition => condition.id === id); } reorderConditions(reorderPlan) { @@ -234,14 +245,14 @@ export default class ConditionManager extends EventEmitter { } requestLADConditionSetOutput() { - if (!this.conditionClassCollection.length) { + if (!this.conditions.length) { return Promise.resolve([]); } return this.compositionLoad.then(() => { let latestTimestamp; let conditionResults = {}; - const conditionRequests = this.conditionClassCollection + const conditionRequests = this.conditions .map(condition => condition.requestLADConditionResult()); return Promise.all(conditionRequests) @@ -281,7 +292,7 @@ export default class ConditionManager extends EventEmitter { isTelemetryUsed(endpoint) { const id = this.openmct.objects.makeKeyString(endpoint.identifier); - for(const condition of this.conditionClassCollection) { + for(const condition of this.conditions) { if (condition.isTelemetryUsed(id)) { return true; } @@ -300,7 +311,7 @@ export default class ConditionManager extends EventEmitter { let timestamp = {}; timestamp[timeSystemKey] = normalizedDatum[timeSystemKey]; - this.conditionClassCollection.forEach(condition => { + this.conditions.forEach(condition => { condition.getResult(normalizedDatum); }); @@ -364,7 +375,7 @@ export default class ConditionManager extends EventEmitter { this.stopObservingForChanges(); } - this.conditionClassCollection.forEach((condition) => { + this.conditions.forEach((condition) => { condition.destroy(); }) } diff --git a/src/plugins/condition/ConditionManagerSpec.js b/src/plugins/condition/ConditionManagerSpec.js index 2f9a6c3499..483053ba4e 100644 --- a/src/plugins/condition/ConditionManagerSpec.js +++ b/src/plugins/condition/ConditionManagerSpec.js @@ -126,7 +126,7 @@ describe('ConditionManager', () => { it('creates a conditionCollection with a default condition', function () { expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(1); - let defaultConditionId = conditionMgr.conditionClassCollection[0].id; + let defaultConditionId = conditionMgr.conditions[0].id; expect(defaultConditionId).toEqual(mockCondition.id); }); diff --git a/src/plugins/condition/ConditionSpec.js b/src/plugins/condition/ConditionSpec.js index 5cd0c33558..d887771647 100644 --- a/src/plugins/condition/ConditionSpec.js +++ b/src/plugins/condition/ConditionSpec.js @@ -36,19 +36,20 @@ describe("The condition", function () { beforeEach (() => { conditionManager = jasmine.createSpyObj('conditionManager', - ['on'] + ['on', 'updateConditionDescription'] ); mockTelemetryReceived = jasmine.createSpy('listener'); conditionManager.on('telemetryReceived', mockTelemetryReceived); + conditionManager.updateConditionDescription.and.returnValue(function () {}); testTelemetryObject = { identifier:{ namespace: "", key: "test-object"}, type: "test-object", name: "Test Object", telemetry: { - values: [{ - key: "value", - name: "Value", + valueMetadatas: [{ + key: "some-key", + name: "Some attribute", hints: { range: 2 } @@ -78,7 +79,7 @@ describe("The condition", function () { openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', 'subscribe', 'getMetadata']); openmct.telemetry.isTelemetryObject.and.returnValue(true); openmct.telemetry.subscribe.and.returnValue(function () {}); - openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values); + openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); mockTimeSystems = { key: 'utc' diff --git a/src/plugins/condition/StyleRuleManager.js b/src/plugins/condition/StyleRuleManager.js index fb585455cb..6fe7b38c37 100644 --- a/src/plugins/condition/StyleRuleManager.js +++ b/src/plugins/condition/StyleRuleManager.js @@ -29,6 +29,7 @@ export default class StyleRuleManager extends EventEmitter { this.callback = callback; if (suppressSubscriptionOnEdit) { this.openmct.editor.on('isEditing', this.toggleSubscription.bind(this)); + this.isEditing = this.openmct.editor.editing; } if (styleConfiguration) { this.initialize(styleConfiguration); @@ -156,7 +157,6 @@ export default class StyleRuleManager extends EventEmitter { } delete this.stopProvidingTelemetry; this.conditionSetIdentifier = undefined; - this.isEditing = undefined; } } diff --git a/src/plugins/condition/components/Condition.vue b/src/plugins/condition/components/Condition.vue index 6f1deb7d3c..51c58b6da3 100644 --- a/src/plugins/condition/components/Condition.vue +++ b/src/plugins/condition/components/Condition.vue @@ -50,7 +50,7 @@ {{ condition.configuration.name }} - diff --git a/src/plugins/condition/criterion/AllTelemetryCriterion.js b/src/plugins/condition/criterion/AllTelemetryCriterion.js index 6e5142995e..71ca16475a 100644 --- a/src/plugins/condition/criterion/AllTelemetryCriterion.js +++ b/src/plugins/condition/criterion/AllTelemetryCriterion.js @@ -23,6 +23,7 @@ import TelemetryCriterion from './TelemetryCriterion'; import { evaluateResults } from "../utils/evaluator"; import { getLatestTimestamp } from '../utils/time'; +import { getOperatorText } from "@/plugins/condition/utils/operations"; export default class AllTelemetryCriterion extends TelemetryCriterion { @@ -46,7 +47,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion { return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation; } - updateTelemetry(telemetryObjects) { + updateTelemetryObjects(telemetryObjects) { this.telemetryObjects = { ...telemetryObjects }; this.removeTelemetryDataCache(); } @@ -159,6 +160,25 @@ export default class AllTelemetryCriterion extends TelemetryCriterion { }); } + getDescription() { + const telemetryDescription = this.telemetry === 'all' ? 'all telemetry' : 'any telemetry'; + let metadataValue = this.metadata; + let inputValue = this.input; + if (this.metadata) { + const telemetryObjects = Object.values(this.telemetryObjects); + for (let i=0; i < telemetryObjects.length; i++) { + const telemetryObject = telemetryObjects[i]; + const metadataObject = this.getMetaDataObject(telemetryObject, this.metadata); + if (metadataObject) { + metadataValue = this.getMetadataValueFromMetaData(metadataObject) || this.metadata; + inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input; + break; + } + } + } + return `${telemetryDescription} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`; + } + destroy() { delete this.telemetryObjects; delete this.telemetryDataCache; diff --git a/src/plugins/condition/criterion/TelemetryCriterion.js b/src/plugins/condition/criterion/TelemetryCriterion.js index fd710685b8..c941c18f02 100644 --- a/src/plugins/condition/criterion/TelemetryCriterion.js +++ b/src/plugins/condition/criterion/TelemetryCriterion.js @@ -21,7 +21,7 @@ *****************************************************************************/ import EventEmitter from 'EventEmitter'; -import { OPERATIONS } from '../utils/operations'; +import { OPERATIONS, getOperatorText } from '../utils/operations'; export default class TelemetryCriterion extends EventEmitter { @@ -49,15 +49,15 @@ export default class TelemetryCriterion extends EventEmitter { } initialize() { - this.telemetryObject = this.telemetryDomainObjectDefinition.telemetryObject; this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry); + this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects); } isValid() { return this.telemetryObject && this.metadata && this.operation; } - updateTelemetry(telemetryObjects) { + updateTelemetryObjects(telemetryObjects) { this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString]; } @@ -153,6 +153,51 @@ export default class TelemetryCriterion extends EventEmitter { }); } + getMetaDataObject(telemetryObject, metadata) { + let metadataObject; + if (metadata) { + const telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); + metadataObject = telemetryMetadata.valueMetadatas.find((valueMetadata) => valueMetadata.key === metadata); + } + return metadataObject; + } + + getInputValueFromMetaData(metadataObject, input) { + let inputValue; + if (metadataObject) { + if(metadataObject.enumerations && input.length) { + const enumeration = metadataObject.enumerations[input[0]]; + if (enumeration !== undefined && enumeration.string) { + inputValue = [enumeration.string]; + } + } + } + return inputValue; + } + + getMetadataValueFromMetaData(metadataObject) { + let metadataValue; + if (metadataObject) { + if (metadataObject.name) { + metadataValue = metadataObject.name; + } + } + return metadataValue; + } + + getDescription(criterion, index) { + let description; + if (!this.telemetry || !this.telemetryObject || (this.telemetryObject.type === 'unknown')) { + description = `Unknown ${this.metadata} ${getOperatorText(this.operation, this.input)}`; + } else { + const metadataObject = this.getMetaDataObject(this.telemetryObject, this.metadata); + const metadataValue = this.getMetadataValueFromMetaData(metadataObject) || this.metadata; + const inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input; + description = `${this.telemetryObject.name} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`; + } + + return description; + } destroy() { delete this.telemetryObject; diff --git a/src/plugins/condition/criterion/TelemetryCriterionSpec.js b/src/plugins/condition/criterion/TelemetryCriterionSpec.js index a5ff94c7e0..340c6d2212 100644 --- a/src/plugins/condition/criterion/TelemetryCriterionSpec.js +++ b/src/plugins/condition/criterion/TelemetryCriterionSpec.js @@ -83,7 +83,7 @@ describe("The telemetry criterion", function () { operation: 'textContains', metadata: 'value', input: ['Hell'], - telemetryObject: testTelemetryObject + telemetryObjects: {[testTelemetryObject.identifier.key]: testTelemetryObject} }; mockListener = jasmine.createSpy('listener'); @@ -109,13 +109,4 @@ describe("The telemetry criterion", function () { }); expect(telemetryCriterion.result).toBeTrue(); }); - - // it("does not return a result on new data from irrelavant telemetry providers", function () { - // telemetryCriterion.getResult({ - // value: 'Hello', - // utc: 'Hi', - // id: '1234' - // }); - // expect(telemetryCriterion.result).toBeFalse(); - // }); }); diff --git a/src/plugins/condition/pluginSpec.js b/src/plugins/condition/pluginSpec.js index 93a1930bc4..55caedb446 100644 --- a/src/plugins/condition/pluginSpec.js +++ b/src/plugins/condition/pluginSpec.js @@ -20,8 +20,11 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createOpenMct } from "testUtils"; +import { createOpenMct, resetApplicationState } from "utils/testing"; import ConditionPlugin from "./plugin"; +import StylesView from "./components/inspector/StylesView.vue"; +import Vue from 'vue'; +import {getApplicableStylesForItem} from "./utils/styleUtils"; describe('the plugin', function () { let conditionSetDefinition; @@ -30,7 +33,11 @@ describe('the plugin', function () { let child; let openmct; - beforeAll((done) => { + beforeAll(() => { + resetApplicationState(openmct); + }); + + beforeEach((done) => { openmct = createOpenMct(); openmct.install(new ConditionPlugin()); @@ -54,6 +61,10 @@ describe('the plugin', function () { openmct.startHeadless(); }); + afterEach(() => { + resetApplicationState(openmct); + }); + let mockConditionSetObject = { name: 'Condition Set', key: 'conditionSet', @@ -90,4 +101,259 @@ describe('the plugin', function () { }); }); + + describe('the condition set usage for multiple display layout items', () => { + let displayLayoutItem; + let lineLayoutItem; + let boxLayoutItem; + let selection; + let component; + let styleViewComponentObject; + const conditionSetDomainObject = { + "configuration":{ + "conditionTestData":[ + { + "telemetry":"", + "metadata":"", + "input":"" + } + ], + "conditionCollection":[ + { + "id":"39584410-cbf9-499e-96dc-76f27e69885d", + "configuration":{ + "name":"Unnamed Condition", + "output":"Sine > 0", + "trigger":"all", + "criteria":[ + { + "id":"85fbb2f7-7595-42bd-9767-a932266c5225", + "telemetry":{ + "namespace":"", + "key":"be0ba97f-b510-4f40-a18d-4ff121d5ea1a" + }, + "operation":"greaterThan", + "input":[ + "0" + ], + "metadata":"sin" + }, + { + "id":"35400132-63b0-425c-ac30-8197df7d5862", + "telemetry":"any", + "operation":"enumValueIs", + "input":[ + "0" + ], + "metadata":"state" + } + ] + }, + "summary":"Match if all criteria are met: Sine Wave Generator Sine > 0 and any telemetry State is OFF " + }, + { + "isDefault":true, + "id":"2532d90a-e0d6-4935-b546-3123522da2de", + "configuration":{ + "name":"Default", + "output":"Default", + "trigger":"all", + "criteria":[ + ] + }, + "summary":"" + } + ] + }, + "composition":[ + { + "namespace":"", + "key":"be0ba97f-b510-4f40-a18d-4ff121d5ea1a" + }, + { + "namespace":"", + "key":"077ffa67-e78f-4e99-80e0-522ac33a3888" + } + ], + "telemetry":{ + }, + "name":"Condition Set", + "type":"conditionSet", + "identifier":{ + "namespace":"", + "key":"863012c1-f6ca-4ab0-aed7-fd43d5e4cd12" + } + + }; + const staticStyle = { + "style":{ + "backgroundColor":"#717171", + "border":"1px solid #00ffff" + } + }; + const conditionalStyle = { + "conditionId":"39584410-cbf9-499e-96dc-76f27e69885d", + "style":{ + "isStyleInvisible":"", + "backgroundColor":"#717171", + "border":"1px solid #ffff00" + } + }; + + beforeEach(() => { + displayLayoutItem = { + "composition":[ + ], + "configuration":{ + "items":[ + { + "fill":"#717171", + "stroke":"", + "x":1, + "y":1, + "width":10, + "height":5, + "type":"box-view", + "id":"89b88746-d325-487b-aec4-11b79afff9e8" + }, + { + "x":18, + "y":9, + "x2":23, + "y2":4, + "stroke":"#717171", + "type":"line-view", + "id":"57d49a28-7863-43bd-9593-6570758916f0" + } + ], + "layoutGrid":[ + 10, + 10 + ] + }, + "name":"Display Layout", + "type":"layout", + "identifier":{ + "namespace":"", + "key":"c5e636c1-6771-4c9c-b933-8665cab189b3" + } + }; + lineLayoutItem = { + "x":18, + "y":9, + "x2":23, + "y2":4, + "stroke":"#717171", + "type":"line-view", + "id":"57d49a28-7863-43bd-9593-6570758916f0" + }; + boxLayoutItem = { + "fill": "#717171", + "stroke": "", + "x": 1, + "y": 1, + "width": 10, + "height": 5, + "type": "box-view", + "id": "89b88746-d325-487b-aec4-11b79afff9e8" + }; + selection = [ + [{ + context: { + "layoutItem": lineLayoutItem, + "index":1 + } + }, + { + context: { + "item": displayLayoutItem, + "supportsMultiSelect":true + } + }], + [{ + context: { + "layoutItem": boxLayoutItem, + "index": 0 + } + }, + { + context: { + item: displayLayoutItem, + "supportsMultiSelect":true + } + }] + ]; + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + provide: { + openmct: openmct, + selection: selection + }, + el: viewContainer, + components: { + StylesView + }, + template: '' + }); + return Vue.nextTick().then(() => { + styleViewComponentObject = component.$root.$children[0]; + styleViewComponentObject.setEditState(true); + }); + }); + + it('initializes the items in the view', () => { + expect(styleViewComponentObject.items.length).toBe(2); + }); + + it('initializes conditional styles', () => { + styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; + styleViewComponentObject.conditionalStyles = []; + styleViewComponentObject.initializeConditionalStyles(); + expect(styleViewComponentObject.conditionalStyles.length).toBe(2); + }); + + it('updates applicable conditional styles', () => { + styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; + styleViewComponentObject.conditionalStyles = []; + styleViewComponentObject.initializeConditionalStyles(); + expect(styleViewComponentObject.conditionalStyles.length).toBe(2); + styleViewComponentObject.updateConditionalStyle(conditionalStyle, 'border'); + return Vue.nextTick().then(() => { + expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); + [boxLayoutItem, lineLayoutItem].forEach((item) => { + const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles; + expect(itemStyles.length).toBe(2); + const foundStyle = itemStyles.find((style) => { + return style.conditionId === conditionalStyle.conditionId; + }); + expect(foundStyle).toBeDefined(); + const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item); + const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']); + Object.keys(foundStyle.style).forEach((key) => { + expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1); + expect(foundStyle.style[key]).toEqual(conditionalStyle.style[key]); + }); + }); + }); + }); + + it('updates applicable static styles', () => { + styleViewComponentObject.updateStaticStyle(staticStyle, 'border'); + return Vue.nextTick().then(() => { + expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); + [boxLayoutItem, lineLayoutItem].forEach((item) => { + const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle; + expect(itemStyle).toBeDefined(); + const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item); + const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']); + Object.keys(itemStyle.style).forEach((key) => { + expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1); + expect(itemStyle.style[key]).toEqual(staticStyle.style[key]); + }); + }); + }); + }); + + }); }); diff --git a/src/plugins/condition/utils/constants.js b/src/plugins/condition/utils/constants.js index 9ccd08436e..881fc75eea 100644 --- a/src/plugins/condition/utils/constants.js +++ b/src/plugins/condition/utils/constants.js @@ -28,10 +28,17 @@ export const TRIGGER = { }; export const TRIGGER_LABEL = { - 'any': 'when any criteria are met', - 'all': 'when all criteria are met', - 'not': 'when no criteria are met', - 'xor': 'when only one criteria is met' + 'any': 'any criteria are met', + 'all': 'all criteria are met', + 'not': 'no criteria are met', + 'xor': 'only one criterion is met' +}; + +export const TRIGGER_CONJUNCTION = { + 'any': 'or', + 'all': 'and', + 'not': 'and', + 'xor': 'or' }; export const STYLE_CONSTANTS = { diff --git a/src/plugins/condition/utils/evaluator.js b/src/plugins/condition/utils/evaluator.js index 1b1ecb71a5..1f3aa00825 100644 --- a/src/plugins/condition/utils/evaluator.js +++ b/src/plugins/condition/utils/evaluator.js @@ -35,7 +35,7 @@ export const evaluateResults = (results, trigger) => { function matchAll(results) { for (const result of results) { - if (!result) { + if (result !== true) { return false; } } @@ -45,7 +45,7 @@ function matchAll(results) { function matchAny(results) { for (const result of results) { - if (result) { + if (result === true) { return true; } } @@ -56,7 +56,7 @@ function matchAny(results) { function matchExact(results, target) { let matches = 0; for (const result of results) { - if (result) { + if (result === true) { matches++; } if (matches > target) { diff --git a/src/plugins/condition/utils/operations.js b/src/plugins/condition/utils/operations.js index 86d67d47d9..0a89fd6a84 100644 --- a/src/plugins/condition/utils/operations.js +++ b/src/plugins/condition/utils/operations.js @@ -250,12 +250,12 @@ export const OPERATIONS = [ } }, { - name: 'valueIs', + name: 'isOneOf', operation: function (input) { const lhsValue = input[0] !== undefined ? input[0].toString() : ''; if (input[1]) { const values = input[1].split(','); - return values.find((value) => lhsValue === value.toString().trim()); + return values.some((value) => lhsValue === value.toString().trim()); } return false; }, @@ -267,12 +267,12 @@ export const OPERATIONS = [ } }, { - name: 'valueIsNot', + name: 'isNotOneOf', operation: function (input) { const lhsValue = input[0] !== undefined ? input[0].toString() : ''; if (input[1]) { const values = input[1].split(','); - const found = values.find((value) => lhsValue === value.toString().trim()); + const found = values.some((value) => lhsValue === value.toString().trim()); return !found; } return false; @@ -290,3 +290,8 @@ export const INPUT_TYPES = { 'string': 'text', 'number': 'number' }; + +export const getOperatorText = (operationName, values) => { + const found = OPERATIONS.find((operation) => operation.name === operationName); + return found ? found.getDescription(values) : ''; +}; diff --git a/src/plugins/condition/utils/operationsSpec.js b/src/plugins/condition/utils/operationsSpec.js index ccd0ad3414..f3c898d8ad 100644 --- a/src/plugins/condition/utils/operationsSpec.js +++ b/src/plugins/condition/utils/operationsSpec.js @@ -21,8 +21,8 @@ *****************************************************************************/ import { OPERATIONS } from "./operations"; -let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIs'); -let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIsNot'); +let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'isOneOf'); +let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'isNotOneOf'); let isBetween = OPERATIONS.find((operation) => operation.name === 'between'); let isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween'); let enumIsOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIs'); diff --git a/src/plugins/condition/utils/styleUtils.js b/src/plugins/condition/utils/styleUtils.js index 24bf82a1d7..03a83d9c2f 100644 --- a/src/plugins/condition/utils/styleUtils.js +++ b/src/plugins/condition/utils/styleUtils.js @@ -124,12 +124,25 @@ export const getConditionalStyleForItem = (domainObject, id) => { if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) { return domainObjectStyles[id].styles; } - } else if (domainObjectStyles.staticStyle) { + } else if (domainObjectStyles.conditionSetIdentifier) { return domainObjectStyles.styles; } } }; +export const getConditionSetIdentifierForItem = (domainObject, id) => { + let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles; + if (domainObjectStyles) { + if (id) { + if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) { + return domainObjectStyles[id].conditionSetIdentifier; + } + } else if (domainObjectStyles.conditionSetIdentifier) { + return domainObjectStyles.conditionSetIdentifier; + } + } +}; + //Returns either existing static styles or uses SVG defaults if available export const getApplicableStylesForItem = (domainObject, item) => { const type = item && item.type; diff --git a/src/plugins/imagery/components/ImageryViewLayout.vue b/src/plugins/imagery/components/ImageryViewLayout.vue index 0821eeabd9..1975bd0803 100644 --- a/src/plugins/imagery/components/ImageryViewLayout.vue +++ b/src/plugins/imagery/components/ImageryViewLayout.vue @@ -66,7 +66,6 @@ export default { data() { return { autoScroll: true, - date: '', filters : { brightness: 100, contrast: 100 @@ -78,22 +77,39 @@ export default { imageHistory: [], imageUrl: '', isPaused: false, + metadata: {}, requestCount: 0, timeFormat: '' } }, mounted() { + // set this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier); - this.subscribe(this.domainObject); + this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); + this.imageFormat = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]); + // initialize + this.timeKey = this.openmct.time.timeSystem().key; + this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey)); + // listen + this.openmct.time.on('bounds', this.boundsChange); + this.openmct.time.on('timeSystem', this.timeSystemChange); + // kickoff + this.subscribe(); + this.requestHistory(); }, updated() { this.scrollToRight(); }, beforeDestroy() { - this.stopListening(); + if (this.unsubscribe) { + this.unsubscribe(); + delete this.unsubscribe; + } + this.openmct.time.off('bounds', this.boundsChange); + this.openmct.time.off('timeSystem', this.timeSystemChange); }, methods: { - datumMatchesMostRecent(datum) { + datumIsNotValid(datum) { if (this.imageHistory.length === 0) { return false; } @@ -103,7 +119,14 @@ export default { const lastHistoryTime = this.timeFormat.format(this.imageHistory.slice(-1)[0]); const lastHistoryURL = this.imageFormat.format(this.imageHistory.slice(-1)[0]); - return (datumTime === lastHistoryTime) && (datumURL === lastHistoryURL); + // datum is not valid if it matches the last datum in history, + // or it is before the last datum in the history + const datumTimeCheck = this.timeFormat.parse(datum); + const historyTimeCheck = this.timeFormat.parse(this.imageHistory.slice(-1)[0]); + const matchesLast = (datumTime === lastHistoryTime) && (datumURL === lastHistoryURL); + const isStale = datumTimeCheck < historyTimeCheck; + + return matchesLast || isStale; }, getImageUrl(datum) { return datum ? @@ -147,21 +170,6 @@ export default { return this.isPaused; }, - requestHistory(bounds) { - this.requestCount++; - this.imageHistory = []; - const requestId = this.requestCount; - this.openmct.telemetry - .request(this.domainObject, bounds) - .then((values = []) => { - if (this.requestCount > requestId) { - return Promise.resolve('Stale request'); - } - - values.forEach(this.updateHistory); - this.updateValues(values[values.length - 1]); - }); - }, scrollToRight() { if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) { return; @@ -188,40 +196,56 @@ export default { image.selected = true; } }, - stopListening() { - if (this.unsubscribe) { - this.unsubscribe(); - delete this.unsubscribe; + boundsChange(bounds, isTick) { + if(!isTick) { + this.requestHistory(); } }, - subscribe(domainObject) { - this.date = '' - this.imageUrl = ''; - this.openmct.objects.get(this.keystring) - .then((object) => { - const metadata = this.openmct.telemetry.getMetadata(this.domainObject); - this.timeKey = this.openmct.time.timeSystem().key; - this.timeFormat = this.openmct.telemetry.getValueFormatter(metadata.value(this.timeKey)); - this.imageFormat = this.openmct.telemetry.getValueFormatter(metadata.valuesForHints(['image'])[0]); - this.unsubscribe = this.openmct.telemetry - .subscribe(this.domainObject, (datum) => { - this.updateHistory(datum); - this.updateValues(datum); - }); + requestHistory() { + let bounds = this.openmct.time.bounds(); + this.requestCount++; + const requestId = this.requestCount; + this.imageHistory = []; + this.openmct.telemetry + .request(this.domainObject, bounds) + .then((values = []) => { + if (this.requestCount === requestId) { + values.forEach(this.updateHistory, false); + this.updateValues(values[values.length - 1]); + } + }); + }, + timeSystemChange(system) { + // reset timesystem dependent variables + this.timeKey = system.key; + this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey)); + }, + subscribe() { + this.unsubscribe = this.openmct.telemetry + .subscribe(this.domainObject, (datum) => { + let parsedTimestamp = this.timeFormat.parse(datum[this.timeKey]), + bounds = this.openmct.time.bounds(); - this.requestHistory(this.openmct.time.bounds()); + if(parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { + this.updateHistory(datum); + this.updateValues(datum); + } }); }, unselectAllImages() { this.imageHistory.forEach(image => image.selected = false); }, - updateHistory(datum) { - if (this.datumMatchesMostRecent(datum)) { + updateHistory(datum, updateValues = true) { + if (this.datumIsNotValid(datum)) { return; } const index = _.sortedIndexBy(this.imageHistory, datum, this.timeFormat.format.bind(this.timeFormat)); this.imageHistory.splice(index, 0, datum); + + if(updateValues) { + this.updateValues(datum); + } }, updateValues(datum) { if (this.isPaused) { diff --git a/src/plugins/notificationIndicator/components/NotificationIndicator.vue b/src/plugins/notificationIndicator/components/NotificationIndicator.vue new file mode 100644 index 0000000000..a3a3cffa1a --- /dev/null +++ b/src/plugins/notificationIndicator/components/NotificationIndicator.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/plugins/notificationIndicator/components/NotificationMessage.vue b/src/plugins/notificationIndicator/components/NotificationMessage.vue new file mode 100644 index 0000000000..96fd02cfc7 --- /dev/null +++ b/src/plugins/notificationIndicator/components/NotificationMessage.vue @@ -0,0 +1,85 @@ + + + diff --git a/src/plugins/notificationIndicator/components/NotificationsList.vue b/src/plugins/notificationIndicator/components/NotificationsList.vue new file mode 100644 index 0000000000..dadbf7074d --- /dev/null +++ b/src/plugins/notificationIndicator/components/NotificationsList.vue @@ -0,0 +1,69 @@ + + + diff --git a/src/plugins/notificationIndicator/plugin.js b/src/plugins/notificationIndicator/plugin.js new file mode 100644 index 0000000000..d870d67c90 --- /dev/null +++ b/src/plugins/notificationIndicator/plugin.js @@ -0,0 +1,43 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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 Vue from 'vue'; +import NotificationIndicator from './components/NotificationIndicator.vue'; + +export default function plugin() { + return function install(openmct) { + let component = new Vue ({ + provide: { + openmct + }, + components: { + NotificationIndicator: NotificationIndicator + }, + template: '' + }), + indicator = { + key: 'notifications-indicator', + element: component.$mount().$el + }; + + openmct.indicators.add(indicator); + }; +} diff --git a/src/plugins/notificationIndicator/pluginSpec.js b/src/plugins/notificationIndicator/pluginSpec.js new file mode 100644 index 0000000000..9ee8401b6d --- /dev/null +++ b/src/plugins/notificationIndicator/pluginSpec.js @@ -0,0 +1,80 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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 NotificationIndicatorPlugin from './plugin.js'; +import Vue from 'vue'; +import { + createOpenMct, + resetApplicationState +} from 'utils/testing'; + +describe('the plugin', () => { + let notificationIndicatorPlugin, + openmct, + indicatorObject, + indicatorElement, + parentElement, + mockMessages = ['error', 'test', 'notifications']; + + beforeAll(() => { + resetApplicationState(); + }); + + beforeEach((done) => { + openmct = createOpenMct(); + + notificationIndicatorPlugin = new NotificationIndicatorPlugin(); + openmct.install(notificationIndicatorPlugin); + + parentElement = document.createElement('div'); + + indicatorObject = openmct.indicators.indicatorObjects.find(indicator => indicator.key === 'notifications-indicator'); + indicatorElement = indicatorObject.element; + + openmct.on('start', () => { + mockMessages.forEach(message => { + openmct.notifications.error(message); + }); + done(); + }); + + openmct.startHeadless(); + }); + + afterEach(() => { + resetApplicationState(openmct); + }); + + describe('the indicator plugin element', () => { + beforeEach(() => { + parentElement.append(indicatorElement); + return Vue.nextTick(); + }); + + it('notifies the user of the number of notifications', () => { + let notificationCountElement = parentElement.querySelector('.c-indicator__count'); + + expect(notificationCountElement.innerText).toEqual(mockMessages.length.toString()); + }); + }); + +}); diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index ca64422819..6ba5b8d676 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -51,7 +51,9 @@ define([ './conditionWidget/plugin', './themes/espresso', './themes/maelstrom', - './themes/snow' + './themes/snow', + './URLTimeSettingsSynchronizer/plugin', + './notificationIndicator/plugin' ], function ( _, UTCTimeSystem, @@ -83,7 +85,9 @@ define([ ConditionWidgetPlugin, Espresso, Maelstrom, - Snow + Snow, + URLTimeSettingsSynchronizer, + NotificationIndicator ) { var bundleMap = { LocalStorage: 'platform/persistence/local', @@ -192,6 +196,8 @@ define([ plugins.Snow = Snow.default; plugins.Condition = ConditionPlugin.default; plugins.ConditionWidget = ConditionWidgetPlugin.default; + plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default; + plugins.NotificationIndicator = NotificationIndicator.default; return plugins; }); diff --git a/src/plugins/telemetryTable/components/table-configuration.vue b/src/plugins/telemetryTable/components/table-configuration.vue index 364d287393..105f8a0995 100644 --- a/src/plugins/telemetryTable/components/table-configuration.vue +++ b/src/plugins/telemetryTable/components/table-configuration.vue @@ -2,7 +2,7 @@