Compare commits

...

101 Commits

Author SHA1 Message Date
043d6aa9c3 extend traces on subscribe 2020-05-07 12:23:32 -07:00
ecfab8f7f3 add subscribe 2020-05-07 12:00:39 -07:00
05c38c37aa working historical 2020-05-07 11:37:01 -07:00
ce78925119 wip: added telemetry provider 2020-05-07 10:45:15 -07:00
26aca0f433 working on viewProvider 2020-05-06 11:31:17 -07:00
41259bbd40 added hardcoded test plot 2020-05-05 16:00:48 -07:00
580640ff47 basic plotly plugin structure 2020-05-05 12:50:55 -07:00
a4aec5d492 Merge branch 'master' of https://github.com/nasa/openmct 2020-05-01 10:46:57 -07:00
23303c910e Don't allow recursive Preview actions #2775 (#2869)
* Don't allow recursive Preview actions #2775

* actionsToBeIncluded and actionsToBeSkipped passed in as options object.

* Revert "actionsToBeIncluded and actionsToBeSkipped passed in as options object."

This reverts commit f501d0b4ba.

* Revert "Don't allow recursive Preview actions #2775"

This reverts commit 5563cbea3a.

* Don't allow recursive Preview actions

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-05-01 09:33:10 -07:00
00b3f3ac0b Merge branch 'master' of https://github.com/nasa/openmct 2020-05-01 09:25:44 -07:00
3282934cf6 fix test coverage. (#2951)
* fix test coverage.

* changes per comments + added test-coverage script to increase max-old-space-size of V8
ref: https://nodejs.org/api/cli.html#cli_max_old_space_size_size_in_mbytes

* renamed test:coverage and test:debug.

* circle-ci to use test:coverage.

* reduced test coverage thresholds.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-30 16:57:53 -07:00
c157fab081 [Notebook] Save snapshot dropdown should be available from "view large" overlay #2922 (#2926)
* [Notebook] Save snapshot dropdown should be available from "view large" overlay #2922\
* Significant improvements to Snapshot styling
* [Notebook] Embed links aren't navigating #2979

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-30 16:39:20 -07:00
7c07b66cc9 [Notebook] : Error in event handler for "updateSection": "TypeError: Cannot read property 'id' of undefined" #2921 (#2924)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-30 16:30:09 -07:00
7a906ccf5c Preview condition styles on selecting that condition or one of it's styles (#2925)
* Preview condition styles on selecting that condition or one of it's styles
* Do not evaluate conditional styles in edit mode

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-30 13:00:43 -07:00
ff7debfb81 [Notebook] Entries and Embeds need to use the same timesystem #2920 (#2923)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-30 11:56:08 -07:00
92ba103f45 modified eslint script and fixed errors found (#2905)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-30 11:53:07 -07:00
2c2d8d6b56 [Notebook]: Unnecessary notification "Time bounds changed to fixed timespan mode" #2843 (#2866)
* [Notebook]: Unnecessary notification "Time bounds changed to fixed timespan mode" #2843

* removed stopclock call.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-04-30 10:47:49 -07:00
cfadb9f4fd Fixes #2901 (#2902)
- Mod PreviewAction to prevent folders from being previewed;

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-30 09:48:16 -07:00
c185f77a15 Merge branch 'master' of https://github.com/nasa/openmct 2020-04-29 16:52:05 -07:00
396817b2d1 handle non-valid requests (#2984)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-04-29 15:24:12 -07:00
96eb6d6b74 [Conditionals] evaluation fixes (#2981)
* change single output to state and value

* do not send telemetryObjects to telemetry api request cal

* normalize data on requests

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-04-29 14:56:07 -07:00
cb5d47f66f Use only the values required for description (#2919)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-28 16:58:58 -07:00
ea90d02d66 Show the Styles tab for non creatable layout objects including condition sets (#2975) 2020-04-28 13:10:29 -07:00
95f73d8eb8 [Conditionals] fix #2961 in master (#2969)
* use correct id for telemetry requests

* request and subscription data cache should be mutually exclusive

use latest timestamp for any/all requests

* do not add prop to datum

remove unnecessary if check

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-04-28 13:02:23 -07:00
0dff431f4a Merge branch 'master' of https://github.com/nasa/openmct 2020-04-27 12:02:12 -07:00
a37c686993 Merge pull request #2968 from nasa/revert-2885-lodash-upgrade-test
Revert "Lodash upgrade"
2020-04-24 11:55:44 -07:00
f12166097c Revert "Lodash upgrade (#2885)"
This reverts commit d103a22fa0.
2020-04-24 11:53:31 -07:00
61d238a097 Merge branch 'master' of https://github.com/nasa/openmct 2020-04-23 16:30:01 -07:00
d103a22fa0 Lodash upgrade (#2885)
* upgraded lodash, changed method names
* native implementations as requested
2020-04-23 10:38:44 -07:00
04a60cfcbb fixes #2713 (#2928)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-22 15:22:11 -07:00
8d723960f4 removed acorn from package.json (#2906)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-21 15:50:45 -07:00
6d3cd2c699 Upgrade angular from 1.4.14 to 1.7.9 (#2955)
* successfully upgraded to v1.6 with $compileProvider.preAssignBindingsEnabled(true)
* removed $compileProvider.preAssignBindingsEnabled(true), wrapped constructors for plot and chart inside onInit function
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-21 15:34:12 -07:00
87bf94fe0a Updated year in index.html (#2930) 2020-04-21 15:28:10 -07:00
af93823b6f Elasticsearch support for change to typeless API (#2941)
* added elasticsearch to bundlemap
2020-04-21 15:23:43 -07:00
f9deb80350 Merge branch 'master' of https://github.com/nasa/openmct 2020-04-16 15:01:42 -07:00
4a39ddf425 Check for any and all criteria (#2948) 2020-04-16 15:01:14 -07:00
83c273b976 Remove telemetry from criteria when not editing a condition set (#2933) 2020-04-16 12:32:32 -07:00
021d730814 resolve merge conflicts 2020-04-13 09:05:43 -07:00
7dd81beb03 Remove telemetry data cache if a telemetry endpoint is removed (#2916) 2020-04-10 16:49:29 -07:00
1842d3923c [Conditionals] Only provide telemetry for incoming telemetry that is used (#2914)
* Ensures that results for a specific datapoint are evaluated atomically.

* Removes timestamp based evalutation from conditionManager

* Remove unused code

* remove generating timestamp for telemetry data

* get results directly instead of using events

* remove unused listeners, events, and helpers

* linting

* remove commented code

* telemetry criterion stores its own result

* refactor all/any telemetry criterion to use new evaluator

* tie in requests and eliminate unused code

* use current timesystem to compare latest

* scope function names

* AllTelemetryCriterion extends TelemetryCriterion

* fix telemetrycriterion and unit testing

* fix unit tests

* check if telemetry is used at condition manager level

* move check to condition manager

* remove whitespace

Co-authored-by: Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 16:45:09 -07:00
26838635b6 Ensures correct results are returned for conditions and criteria for a given telemetry datapoint (#2904)
* Ensures that results for a specific datapoint are evaluated atomically.
* Remove generating timestamp for telemetry data
* Get results directly instead of using events
* Refactor all/any telemetry criterion to use new evaluator
* Use current timesystem to compare latest
* AllTelemetryCriterion extends TelemetryCriterion

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:57:38 -07:00
ae62b15abf Merge branch 'fix-default-output' of github.com:nasa/openmct into fix-default-output 2020-04-10 15:44:42 -07:00
ba41c1a30e fix unit tests 2020-04-10 15:43:48 -07:00
b9a85d9c4d Merge branch 'master' into fix-default-output 2020-04-10 15:34:10 -07:00
11f2c35bb2 [Notebook]: Entries filter #2820 (#2864)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:33:50 -07:00
80eab8bad1 fix telemetrycriterion and unit testing 2020-04-10 15:26:18 -07:00
b2d8d640ae Merge branch 'master' into fix-default-output 2020-04-10 15:23:10 -07:00
766f48c1ba Handles static and mixed styles for multiple items in a layout (#2907)
* Show non specific styles when updating multiple item styles
* Save sub object styles to it's domain object
* Layout UI tweak
* Fixes flexible layout bug.
* Fixes font size bug in telemetry view
* Fixes issues with newly places TVOs including transparent properties.
* Fixes #2908
* Say NO to 'transparent' === '__no_value'
- Fixes #2895;
* Ensure styles are correctly applied to domain objects and drawing objects when selected individually
* Ensure none treatment is correctly applied to objects when multple selecting
* Fix intial box border
* Tweaks to c-text-view layout
- Vertically center text;
- Normalize padding;
- Overflow: hidden;

* Tweaks to Clock and Timer layout
- Fixes #2893;
- Vertically center text;
- Normalize padding;
- Overflow: hidden;
- `position: absolute` when in Layout;

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:22:47 -07:00
da7b93f9b3 Notebook context menu (#2888)
Notebook popup menu fix
Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:17:01 -07:00
56e6fa66c2 Merge branch 'master' into fix-default-output 2020-04-10 15:13:36 -07:00
99c095a69f Fixed condition-improve-reorder branch (#2912)
* wip: changing to condition as drag target

* wip

* wip

* wip

* fixed dragging issues

* fixed dragging classes and added temp border on condition with dragging class

* Conditionals sanding and shimming

- CSS and `all-dragging`;

* wip

* fixed drag end issue and changed dragging class to go on parent condition h

* drag with counter

* wip

* wip

* wip

* return to logic in ConditionCollection.vue

* wip

* completed js part with highlighted c-condition-h on dragover

* restored grippy as draggable elem, improved isValidTarget

* fixed drag text bug

* added moveIndex prop in Condition.vue

* Conditionals drag reorder styling

- Moved `.is-drag-target` class up to conditions-h element;
- Renamed `.all-dragging` to `is-active-dragging`;
- Styling for `__drop-target` elements;

* fixed incorrect default for moveIndex in condition collection, unnecessary reset in condition

* fixed downward move reorder

* removed prevent from dragenter and drag leave, changed @blur to @change for name and output fields

* removed console log

* Repair merge-damaged conditionals.scss

- Manual merge from latest master;

* Test data layout tweaked

- Prevent c-cs__test-data__controls from collapsing;

Co-authored-by: Joel McKinnon <joel.g.mckinnon@nasa.gov>
Co-authored-by: Joel McKinnon <JoelMcKinnon@users.noreply.github.com>
2020-04-10 15:04:04 -07:00
f885e83505 Merge pull request #2911 from nasa/revert-2818-condition-improve-reorder
Revert "Condition improve reorder"
2020-04-10 14:35:41 -07:00
928bc4c68a Revert "Condition improve reorder (#2818)"
This reverts commit 46fedc1a30.
2020-04-10 14:32:48 -07:00
d5539c7ae4 Merge pull request #2898 from nasa/fix-enum-comparison
Fixes enum comparisons
2020-04-10 14:03:05 -07:00
c86a104fb6 Merge branch 'master' into fix-enum-comparison 2020-04-10 13:54:04 -07:00
9fa4707c82 Merge branch 'master' into fix-default-output 2020-04-10 13:04:14 -07:00
7e2cfa36de AllTelemetryCriterion extends TelemetryCriterion 2020-04-10 12:17:55 -07:00
aaa60a1545 scope function names 2020-04-10 11:28:13 -07:00
717231fed2 use current timesystem to compare latest 2020-04-10 11:20:51 -07:00
7fb2bc9729 tie in requests and eliminate unused code 2020-04-10 10:42:31 -07:00
46fedc1a30 Condition improve reorder (#2818)
* wip: changing to condition as drag target

* wip

* wip

* wip

* fixed dragging issues

* fixed dragging classes and added temp border on condition with dragging class

* Conditionals sanding and shimming

- CSS and `all-dragging`;

* wip

* fixed drag end issue and changed dragging class to go on parent condition h

* drag with counter

* wip

* wip

* wip

* return to logic in ConditionCollection.vue

* wip

* completed js part with highlighted c-condition-h on dragover

* restored grippy as draggable elem, improved isValidTarget

* fixed drag text bug

* added moveIndex prop in Condition.vue

* Conditionals drag reorder styling

- Moved `.is-drag-target` class up to conditions-h element;
- Renamed `.all-dragging` to `is-active-dragging`;
- Styling for `__drop-target` elements;

* fixed incorrect default for moveIndex in condition collection, unnecessary reset in condition

* fixed downward move reorder

* removed prevent from dragenter and drag leave, changed @blur to @change for name and output fields

* removed console log

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-04-10 10:02:33 -07:00
addeb635e9 refactor all/any telemetry criterion to use new evaluator 2020-04-10 09:48:31 -07:00
608d63a7b0 telemetry criterion stores its own result 2020-04-10 09:00:39 -07:00
10679e5f4f remove commented code 2020-04-10 00:12:55 -07:00
38b8f03b1a linting 2020-04-09 21:03:56 -07:00
779a42c28c remove unused listeners, events, and helpers 2020-04-09 18:14:19 -07:00
80c2504768 get results directly instead of using events 2020-04-09 17:54:47 -07:00
80359e3f16 remove generating timestamp for telemetry data 2020-04-09 16:20:16 -07:00
3b6ef9b44b Refactor duplicate code into functions 2020-04-09 15:33:52 -07:00
66aa4f099f Remove unused code 2020-04-09 15:22:35 -07:00
aa6c6cb88b Removes timestamp based evalutation from conditionManager 2020-04-09 15:21:25 -07:00
4e5cc840d7 Ensures that results for a specific datapoint are evaluated atomically. 2020-04-09 12:10:29 -07:00
c68edd9b7d Fixes enum comparisons
Adds check for undefined
2020-04-09 09:14:31 -07:00
11574b7c40 Merge pull request #2861 from nasa/fix-telemetryview-styles
Styles for telemetry are stored on their container domain objects
2020-04-08 15:19:15 -07:00
abc2cd2413 Merge branch 'master' into fix-telemetryview-styles 2020-04-08 14:40:54 -07:00
5d74882646 Merge pull request #2871 from nasa/dave/conditionals-own-results
[Conditionals] Request and subscription results are mutually exclusive
2020-04-08 14:21:31 -07:00
9fe7f230e6 Merge branch 'master' into dave/conditionals-own-results 2020-04-08 14:16:50 -07:00
de4c5b3729 Disables conditional and static styles for hyperlinks and summary widgets. (#2887) 2020-04-08 12:30:59 -07:00
2a7901914a Merge branch 'master' into fix-telemetryview-styles 2020-04-08 11:57:33 -07:00
73b0fc6f79 Removes preventNone as per conversation with UX designer. 2020-04-08 11:46:12 -07:00
ddef16795c Conditionals and Notebook UI fixes (#2868)
- Significant fixes for Safari-compatible Flex layout in Condition Set
view;
- Changed visual approach to current-value section;
- Firefox scrollbar coloring
- Fix layout issues in Firefox;
- Consolidate Conditionals styles into single scss file;
- Fix test datum elements layout, better wrapping;
- Better approach to presence/absence of URL property in Condition
Widget;
- Fixes #2853;
- Fix errors in URL property handling in Condition Widget;
- Fixes #2853;
- Fixes #2867 - hide the View Switcher when an object is being edited;
- Refined titling on View Switcher and Notebook menu button;
- Cleaned up styles in l-browse-bar and moved into
ui/layout/layout.scss;
- Removed styles/_layout.scss;
- Hide the main view Edit button when in mobile
2020-04-08 09:36:23 -07:00
d188b9a056 do not mutate function args for criteria results either 2020-04-07 13:22:03 -07:00
f510f3edd0 Removes missed code 2020-04-07 11:59:24 -07:00
e05b0bb562 Address review comments:
Fixes telemetry view visibility and styling issue
Removes none option for border and background styles for drawing objects
2020-04-07 11:34:48 -07:00
713c5e9fb7 Merge branch 'master' into dave/conditionals-own-results 2020-04-06 18:56:08 -07:00
17bca04560 do not mutate function args 2020-04-06 18:53:48 -07:00
e0c5bca47d fix unit tests 2020-04-06 14:25:55 -07:00
cdc7c1af64 Removes unused data 2020-04-06 14:04:11 -07:00
3158baa998 Merge branch 'fix-telemetryview-styles' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-06 14:02:45 -07:00
698508fde4 Use the subobject view type to determine where styles should be saved 2020-04-06 14:02:06 -07:00
68a96989e1 Merge branch 'master' into fix-telemetryview-styles 2020-04-06 13:57:41 -07:00
46a6a43234 Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-06 13:56:30 -07:00
d41fc27b55 subscriptions should use latest timestamp 2020-04-06 11:17:59 -07:00
24bb96cc90 WIP detach request data from subscription data
extract getLatestTimestamp function
2020-04-03 18:39:26 -07:00
483ee173d6 pass correct args into off listener 2020-04-02 16:34:40 -07:00
8f81a45b9b Removes coverage branch 2020-04-02 10:30:23 -07:00
666459be87 Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-02 10:30:07 -07:00
d3fe2a6811 Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-01 15:52:55 -07:00
97b37edce4 Store telemetry styles on their container domain objects. 2020-04-01 15:51:40 -07:00
dd70bb470f Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-01 10:18:30 -07:00
072bf361de Store styles for telemetry on domain objects 2020-03-30 16:20:22 -07:00
94 changed files with 3164 additions and 1702 deletions

View File

@ -20,8 +20,8 @@ jobs:
paths: paths:
- node_modules - node_modules
- run: - run:
name: npm run test name: npm run test:coverage
command: npm run test command: npm run test:coverage
- run: - run:
name: npm run lint name: npm run lint
command: npm run lint command: npm run lint

View File

@ -1,5 +1,5 @@
<!-- <!--
Open MCT, Copyright (c) 2014-2017, United States Government Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved. Administration. All rights reserved.
@ -43,9 +43,9 @@
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry) openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
); );
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.Espresso()); openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems()); openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.Generator()); openmct.install(openmct.plugins.Generator());
openmct.install(openmct.plugins.ExampleImagery()); openmct.install(openmct.plugins.ExampleImagery());
openmct.install(openmct.plugins.UTCTimeSystem()); openmct.install(openmct.plugins.UTCTimeSystem());

View File

@ -24,16 +24,27 @@
const devMode = process.env.NODE_ENV !== 'production'; const devMode = process.env.NODE_ENV !== 'production';
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless']; const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
const coverageEnabled = process.env.COVERAGE === 'true';
const reporters = ['progress', 'html'];
if (coverageEnabled) {
reporters.push('coverage-istanbul');
}
module.exports = (config) => { module.exports = (config) => {
const webpackConfig = require('./webpack.config.js'); const webpackConfig = require('./webpack.config.js');
delete webpackConfig.output; delete webpackConfig.output;
if (!devMode) { if (!devMode || coverageEnabled) {
webpackConfig.module.rules.push({ webpackConfig.module.rules.push({
test: /\.js$/, test: /\.js$/,
exclude: /node_modules|example/, exclude: /node_modules|example|lib|dist/,
use: 'istanbul-instrumenter-loader' use: {
loader: 'istanbul-instrumenter-loader',
options: {
esModules: true
}
}
}); });
} }
@ -45,11 +56,7 @@ module.exports = (config) => {
'src/**/*Spec.js' 'src/**/*Spec.js'
], ],
port: 9876, port: 9876,
reporters: [ reporters: reporters,
'progress',
'coverage',
'html'
],
browsers: browsers, browsers: browsers,
customLaunchers: { customLaunchers: {
ChromeDebugging: { ChromeDebugging: {
@ -61,25 +68,25 @@ module.exports = (config) => {
colors: true, colors: true,
logLevel: config.LOG_INFO, logLevel: config.LOG_INFO,
autoWatch: true, autoWatch: true,
coverageReporter: {
dir: process.env.CIRCLE_ARTIFACTS ?
process.env.CIRCLE_ARTIFACTS + '/coverage' :
"dist/reports/coverage",
check: {
global: {
lines: 80,
excludes: ['src/plugins/plot/**/*.js']
}
}
},
// HTML test reporting. // HTML test reporting.
htmlReporter: { htmlReporter: {
outputDir: "dist/reports/tests", outputDir: "dist/reports/tests",
preserveDescribeNesting: true, preserveDescribeNesting: true,
foldAll: false foldAll: false
}, },
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
dir: process.env.CIRCLE_ARTIFACTS ?
process.env.CIRCLE_ARTIFACTS + '/coverage' :
"dist/reports/coverage",
reports: ['html', 'lcovonly', 'text-summary'],
thresholds: {
global: {
lines: 62
}
}
},
preprocessors: { preprocessors: {
// add webpack as preprocessor
'platform/**/*Spec.js': ['webpack', 'sourcemap'], 'platform/**/*Spec.js': ['webpack', 'sourcemap'],
'src/**/*Spec.js': ['webpack', 'sourcemap'] 'src/**/*Spec.js': ['webpack', 'sourcemap']
}, },

View File

@ -2,10 +2,11 @@
"name": "openmct", "name": "openmct",
"version": "1.0.0-snapshot", "version": "1.0.0-snapshot",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"dependencies": {}, "dependencies": {
"plotly.js-dist": "^1.54.1"
},
"devDependencies": { "devDependencies": {
"acorn": "6.2.0", "angular": "1.7.9",
"angular": "1.4.14",
"angular-route": "1.4.14", "angular-route": "1.4.14",
"babel-eslint": "8.2.6", "babel-eslint": "8.2.6",
"comma-separated-values": "^3.6.4", "comma-separated-values": "^3.6.4",
@ -43,6 +44,7 @@
"karma-chrome-launcher": "^2.2.0", "karma-chrome-launcher": "^2.2.0",
"karma-cli": "^1.0.1", "karma-cli": "^1.0.1",
"karma-coverage": "^1.1.2", "karma-coverage": "^1.1.2",
"karma-coverage-istanbul-reporter": "^2.1.1",
"karma-html-reporter": "^0.2.7", "karma-html-reporter": "^0.2.7",
"karma-jasmine": "^1.1.2", "karma-jasmine": "^1.1.2",
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
@ -53,7 +55,7 @@
"marked": "^0.3.5", "marked": "^0.3.5",
"mini-css-extract-plugin": "^0.4.1", "mini-css-extract-plugin": "^0.4.1",
"minimist": "^1.1.1", "minimist": "^1.1.1",
"moment": "^2.11.1", "moment": "^2.25.3",
"moment-duration-format": "^2.2.2", "moment-duration-format": "^2.2.2",
"moment-timezone": "^0.5.21", "moment-timezone": "^0.5.21",
"node-bourbon": "^4.2.3", "node-bourbon": "^4.2.3",
@ -76,14 +78,16 @@
"zepto": "^1.2.0" "zepto": "^1.2.0"
}, },
"scripts": { "scripts": {
"clean": "rm -rf ./dist",
"start": "node app.js", "start": "node app.js",
"lint": "eslint platform example src/**/*.{js,vue} openmct.js", "lint": "eslint platform example src --ext .js,.vue openmct.js",
"lint:fix": "eslint platform example src/**/*.{js,vue} openmct.js --fix", "lint:fix": "eslint platform example src --ext .js,.vue openmct.js --fix",
"build:prod": "cross-env NODE_ENV=production webpack", "build:prod": "cross-env NODE_ENV=production webpack",
"build:dev": "webpack", "build:dev": "webpack",
"build:watch": "webpack --watch", "build:watch": "webpack --watch",
"test": "karma start --single-run", "test": "karma start --single-run",
"test-debug": "cross-env NODE_ENV=debug karma start --no-single-run", "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:coverage": "./scripts/test-coverage.sh",
"test:watch": "karma start --no-single-run", "test:watch": "karma start --no-single-run",
"verify": "concurrently 'npm:test' 'npm:lint'", "verify": "concurrently 'npm:test' 'npm:lint'",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",

View File

@ -87,6 +87,11 @@ define([
bootstrapper bootstrapper
); );
// Override of angular1.6 ! hashPrefix
app.config(['$locationProvider', function ($locationProvider) {
$locationProvider.hashPrefix('');
}]);
// Apply logging levels; this must be done now, before the // Apply logging levels; this must be done now, before the
// first log statement. // first log statement.
new LogLevel(logLevel).configure(app, $log); new LogLevel(logLevel).configure(app, $log);

View File

@ -71,7 +71,7 @@ define([
}, },
{ {
"key": "ELASTIC_PATH", "key": "ELASTIC_PATH",
"value": "mct/domain_object", "value": "mct/_doc",
"priority": "fallback" "priority": "fallback"
}, },
{ {

View File

@ -32,9 +32,9 @@ define(
// JSLint doesn't like underscore-prefixed properties, // JSLint doesn't like underscore-prefixed properties,
// so hide them here. // so hide them here.
var SRC = "_source", var SRC = "_source",
REV = "_version", CONFLICT = 409,
ID = "_id", SEQ_NO = "_seq_no",
CONFLICT = 409; PRIMARY_TERM = "_primary_term";
/** /**
* The ElasticPersistenceProvider reads and writes JSON documents * The ElasticPersistenceProvider reads and writes JSON documents
@ -104,7 +104,8 @@ define(
// Get a domain object model out of ElasticSearch's response // Get a domain object model out of ElasticSearch's response
ElasticPersistenceProvider.prototype.getModel = function (response) { ElasticPersistenceProvider.prototype.getModel = function (response) {
if (response && response[SRC]) { if (response && response[SRC]) {
this.revs[response[ID]] = response[REV]; this.revs[response[SEQ_NO]] = response[SEQ_NO];
this.revs[response[PRIMARY_TERM]] = response[PRIMARY_TERM];
return response[SRC]; return response[SRC];
} else { } else {
return undefined; return undefined;
@ -116,7 +117,8 @@ define(
// indicate that the request failed. // indicate that the request failed.
ElasticPersistenceProvider.prototype.checkResponse = function (response, key) { ElasticPersistenceProvider.prototype.checkResponse = function (response, key) {
if (response && !response.error) { if (response && !response.error) {
this.revs[key] = response[REV]; this.revs[SEQ_NO] = response[SEQ_NO];
this.revs[PRIMARY_TERM] = response[PRIMARY_TERM];
return response; return response;
} else { } else {
return this.handleError(response, key); return this.handleError(response, key);
@ -147,7 +149,7 @@ define(
function checkUpdate(response) { function checkUpdate(response) {
return self.checkResponse(response, key); return self.checkResponse(response, key);
} }
return this.put(key, value, { version: this.revs[key] }) return this.put(key, value)
.then(checkUpdate); .then(checkUpdate);
}; };

View File

@ -85,7 +85,7 @@ define(
it("allows object creation", function () { it("allows object creation", function () {
var model = { someKey: "some value" }; var model = { someKey: "some value" };
mockHttp.and.returnValue(mockPromise({ mockHttp.and.returnValue(mockPromise({
data: { "_id": "abc", "_version": 1 } data: { "_id": "abc", "_seq_no": 1, "_primary_term": 1 }
})); }));
provider.createObject("testSpace", "abc", model).then(capture); provider.createObject("testSpace", "abc", model).then(capture);
expect(mockHttp).toHaveBeenCalledWith({ expect(mockHttp).toHaveBeenCalledWith({
@ -100,7 +100,7 @@ define(
it("allows object models to be read back", function () { it("allows object models to be read back", function () {
var model = { someKey: "some value" }; var model = { someKey: "some value" };
mockHttp.and.returnValue(mockPromise({ mockHttp.and.returnValue(mockPromise({
data: { "_id": "abc", "_version": 1, "_source": model } data: { "_id": "abc", "_seq_no": 1, "_primary_term": 1, "_source": model }
})); }));
provider.readObject("testSpace", "abc").then(capture); provider.readObject("testSpace", "abc").then(capture);
expect(mockHttp).toHaveBeenCalledWith({ expect(mockHttp).toHaveBeenCalledWith({
@ -117,19 +117,19 @@ define(
// First do a read to populate rev tags... // First do a read to populate rev tags...
mockHttp.and.returnValue(mockPromise({ mockHttp.and.returnValue(mockPromise({
data: { "_id": "abc", "_version": 42, "_source": {} } data: { "_id": "abc", "_source": {} }
})); }));
provider.readObject("testSpace", "abc"); provider.readObject("testSpace", "abc");
// Now perform an update // Now perform an update
mockHttp.and.returnValue(mockPromise({ mockHttp.and.returnValue(mockPromise({
data: { "_id": "abc", "_version": 43, "_source": {} } data: { "_id": "abc", "_seq_no": 1, "_source": {} }
})); }));
provider.updateObject("testSpace", "abc", model).then(capture); provider.updateObject("testSpace", "abc", model).then(capture);
expect(mockHttp).toHaveBeenCalledWith({ expect(mockHttp).toHaveBeenCalledWith({
url: "/test/db/abc", url: "/test/db/abc",
method: "PUT", method: "PUT",
params: { version: 42 }, params: undefined,
data: model data: model
}); });
expect(capture.calls.mostRecent().args[0]).toBeTruthy(); expect(capture.calls.mostRecent().args[0]).toBeTruthy();
@ -138,13 +138,13 @@ define(
it("allows object deletion", function () { it("allows object deletion", function () {
// First do a read to populate rev tags... // First do a read to populate rev tags...
mockHttp.and.returnValue(mockPromise({ mockHttp.and.returnValue(mockPromise({
data: { "_id": "abc", "_version": 42, "_source": {} } data: { "_id": "abc", "_source": {} }
})); }));
provider.readObject("testSpace", "abc"); provider.readObject("testSpace", "abc");
// Now perform an update // Now perform an update
mockHttp.and.returnValue(mockPromise({ mockHttp.and.returnValue(mockPromise({
data: { "_id": "abc", "_version": 42, "_source": {} } data: { "_id": "abc", "_source": {} }
})); }));
provider.deleteObject("testSpace", "abc", {}).then(capture); provider.deleteObject("testSpace", "abc", {}).then(capture);
expect(mockHttp).toHaveBeenCalledWith({ expect(mockHttp).toHaveBeenCalledWith({
@ -167,13 +167,13 @@ define(
expect(capture).toHaveBeenCalledWith(undefined); expect(capture).toHaveBeenCalledWith(undefined);
}); });
it("handles rejection due to version", function () { it("handles rejection due to _seq_no", function () {
var model = { someKey: "some value" }, var model = { someKey: "some value" },
mockErrorCallback = jasmine.createSpy('error'); mockErrorCallback = jasmine.createSpy('error');
// First do a read to populate rev tags... // First do a read to populate rev tags...
mockHttp.and.returnValue(mockPromise({ mockHttp.and.returnValue(mockPromise({
data: { "_id": "abc", "_version": 42, "_source": {} } data: { "_id": "abc", "_seq_no": 1, "_source": {} }
})); }));
provider.readObject("testSpace", "abc"); provider.readObject("testSpace", "abc");
@ -196,7 +196,7 @@ define(
// First do a read to populate rev tags... // First do a read to populate rev tags...
mockHttp.and.returnValue(mockPromise({ mockHttp.and.returnValue(mockPromise({
data: { "_id": "abc", "_version": 42, "_source": {} } data: { "_id": "abc", "_seq_no": 1, "_source": {} }
})); }));
provider.readObject("testSpace", "abc"); provider.readObject("testSpace", "abc");

2
scripts/test-coverage.sh Executable file
View File

@ -0,0 +1,2 @@
export NODE_OPTIONS=--max_old_space_size=4096
cross-env COVERAGE=true karma start --single-run

View File

@ -252,6 +252,7 @@ define([
// Plugin's that are installed by default // Plugin's that are installed by default
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
this.install(this.plugins.PlotlyPlot());
this.install(this.plugins.TelemetryTable()); this.install(this.plugins.TelemetryTable());
this.install(PreviewPlugin.default()); this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin()); this.install(LegacyIndicatorsPlugin());

View File

@ -23,8 +23,8 @@
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import uuid from 'uuid'; import uuid from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion"; import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { TRIGGER } from "./utils/constants"; import { evaluateResults } from './utils/evaluator';
import {computeCondition, computeConditionByLimit} from "./utils/evaluator"; import { getLatestTimestamp } from './utils/time';
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion"; import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
/* /*
@ -56,29 +56,38 @@ export default class ConditionClass extends EventEmitter {
this.conditionManager = conditionManager; this.conditionManager = conditionManager;
this.id = conditionConfiguration.id; this.id = conditionConfiguration.id;
this.criteria = []; this.criteria = [];
this.criteriaResults = {};
this.result = undefined; this.result = undefined;
this.latestTimestamp = {}; this.timeSystems = this.openmct.time.getAllTimeSystems();
if (conditionConfiguration.configuration.criteria) { if (conditionConfiguration.configuration.criteria) {
this.createCriteria(conditionConfiguration.configuration.criteria); this.createCriteria(conditionConfiguration.configuration.criteria);
} }
this.trigger = conditionConfiguration.configuration.trigger; this.trigger = conditionConfiguration.configuration.trigger;
this.conditionManager.on('broadcastTelemetry', this.handleBroadcastTelemetry, this);
} }
handleBroadcastTelemetry(datum) { getResult(datum) {
if (!datum || !datum.id) { if (!datum || !datum.id) {
console.log('no data received'); console.log('no data received');
return; return;
} }
this.criteria.forEach(criterion => { this.criteria.forEach(criterion => {
if (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')) { if (this.isAnyOrAllTelemetry(criterion)) {
criterion.handleSubscription(datum, this.conditionManager.telemetryObjects); criterion.getResult(datum, this.conditionManager.telemetryObjects);
} else { } else {
criterion.emit(`subscription:${datum.id}`, datum); criterion.getResult(datum);
} }
}); });
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
}
isAnyOrAllTelemetry(criterion) {
return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
}
isTelemetryUsed(id) {
return this.criteria.some(criterion => {
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
});
} }
update(conditionConfiguration) { update(conditionConfiguration) {
@ -89,7 +98,6 @@ export default class ConditionClass extends EventEmitter {
updateTrigger(trigger) { updateTrigger(trigger) {
if (this.trigger !== trigger) { if (this.trigger !== trigger) {
this.trigger = trigger; this.trigger = trigger;
this.handleConditionUpdated();
} }
} }
@ -133,7 +141,6 @@ export default class ConditionClass extends EventEmitter {
criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct); criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);
} }
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
if (!this.criteria) { if (!this.criteria) {
this.criteria = []; this.criteria = [];
} }
@ -162,22 +169,11 @@ export default class ConditionClass extends EventEmitter {
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration); const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct); let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
newCriterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
let criterion = found.item; let criterion = found.item;
criterion.unsubscribe(); criterion.unsubscribe();
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
this.criteria.splice(found.index, 1, newCriterion); this.criteria.splice(found.index, 1, newCriterion);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id];
}
}
}
removeCriterion(id) {
if (this.destroyCriterion(id)) {
this.handleConditionUpdated();
} }
} }
@ -185,15 +181,12 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(id); let found = this.findCriterion(id);
if (found) { if (found) {
let criterion = found.item; let criterion = found.item;
criterion.destroy(); criterion.off('criterionUpdated', (obj) => {
// TODO this is passing the wrong args this.handleCriterionUpdated(obj);
criterion.off('criterionUpdated', (result) => {
this.handleCriterionUpdated(id, result);
}); });
criterion.destroy();
this.criteria.splice(found.index, 1); this.criteria.splice(found.index, 1);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id];
}
return true; return true;
} }
return false; return false;
@ -203,59 +196,40 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(criterion.id); let found = this.findCriterion(criterion.id);
if (found) { if (found) {
this.criteria[found.index] = criterion.data; this.criteria[found.index] = criterion.data;
// TODO nothing is listening to this
this.emitEvent('conditionUpdated', {
trigger: this.trigger,
criteria: this.criteria
});
} }
} }
updateCriteriaResults(eventData) {
const id = eventData.id;
if (this.findCriterion(id)) {
// The !! here is important to convert undefined to false otherwise the criteriaResults won't get deleted when the criteria is destroyed
this.criteriaResults[id] = !!eventData.data.result;
}
}
handleCriterionResult(eventData) {
this.updateCriteriaResults(eventData);
this.handleConditionUpdated(eventData.data);
}
requestLADConditionResult() { requestLADConditionResult() {
const criteriaResults = this.criteria let latestTimestamp;
.map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects})); let criteriaResults = {};
const criteriaRequests = this.criteria
.map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects));
return Promise.all(criteriaResults) return Promise.all(criteriaRequests)
.then(results => { .then(results => {
results.forEach(result => { results.forEach(resultObj => {
this.updateCriteriaResults(result); const { id, data, data: { result } } = resultObj;
this.latestTimestamp = this.getLatestTimestamp(this.latestTimestamp, result.data) if (this.findCriterion(id)) {
criteriaResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
}); });
this.evaluate();
return { return {
id: this.id, id: this.id,
data: Object.assign({}, this.latestTimestamp, { result: this.result }) data: Object.assign(
} {},
latestTimestamp,
{ result: evaluateResults(Object.values(criteriaResults), this.trigger) }
)
};
}); });
} }
getTelemetrySubscriptions() {
return this.criteria.map(criterion => criterion.telemetryObjectIdAsString);
}
handleConditionUpdated(datum) {
// trigger an updated event so that consumers can react accordingly
this.evaluate();
this.emitEvent('conditionResultUpdated',
Object.assign({}, datum, { result: this.result })
);
}
getCriteria() { getCriteria() {
return this.criteria; return this.criteria;
} }
@ -269,41 +243,7 @@ export default class ConditionClass extends EventEmitter {
return success; return success;
} }
evaluate() {
if (this.trigger && this.trigger === TRIGGER.XOR) {
this.result = computeConditionByLimit(this.criteriaResults, 1);
} else if (this.trigger && this.trigger === TRIGGER.NOT) {
this.result = computeConditionByLimit(this.criteriaResults, 0);
} else {
this.result = computeCondition(this.criteriaResults, this.trigger === TRIGGER.ALL);
}
}
getLatestTimestamp(current, compare) {
const timestamp = Object.assign({}, current);
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
if (!timestamp[timeSystem.key]
|| compare[timeSystem.key] > timestamp[timeSystem.key]
) {
timestamp[timeSystem.key] = compare[timeSystem.key];
}
});
return timestamp;
}
emitEvent(eventName, data) {
this.emit(eventName, {
id: this.id,
data: data
});
}
destroy() { destroy() {
this.conditionManager.off('broadcastTelemetry', this.handleBroadcastTelemetry, this);
if (typeof this.stopObservingForChanges === 'function') {
this.stopObservingForChanges();
}
this.destroyCriteria(); this.destroyCriteria();
} }
} }

View File

@ -21,6 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import Condition from "./Condition"; import Condition from "./Condition";
import { getLatestTimestamp } from './utils/time';
import uuid from "uuid"; import uuid from "uuid";
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
@ -29,8 +30,7 @@ export default class ConditionManager extends EventEmitter {
super(); super();
this.openmct = openmct; this.openmct = openmct;
this.conditionSetDomainObject = conditionSetDomainObject; this.conditionSetDomainObject = conditionSetDomainObject;
this.timeAPI = this.openmct.time; this.timeSystems = this.openmct.time.getAllTimeSystems();
this.latestTimestamp = {};
this.composition = this.openmct.composition.get(conditionSetDomainObject); this.composition = this.openmct.composition.get(conditionSetDomainObject);
this.composition.on('add', this.subscribeToTelemetry, this); this.composition.on('add', this.subscribeToTelemetry, this);
this.composition.on('remove', this.unsubscribeFromTelemetry, this); this.composition.on('remove', this.unsubscribeFromTelemetry, this);
@ -55,8 +55,9 @@ export default class ConditionManager extends EventEmitter {
this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas}); this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas});
this.subscriptions[id] = this.openmct.telemetry.subscribe( this.subscriptions[id] = this.openmct.telemetry.subscribe(
endpoint, endpoint,
this.broadcastTelemetry.bind(this, id) this.telemetryReceived.bind(this, endpoint)
); );
// TODO check if this is needed
this.updateConditionTelemetry(); this.updateConditionTelemetry();
} }
@ -70,10 +71,10 @@ export default class ConditionManager extends EventEmitter {
this.subscriptions[id](); this.subscriptions[id]();
delete this.subscriptions[id]; delete this.subscriptions[id];
delete this.telemetryObjects[id]; delete this.telemetryObjects[id];
this.removeConditionTelemetry();
} }
initialize() { initialize() {
this.conditionResults = {};
this.conditionClassCollection = []; this.conditionClassCollection = [];
if (this.conditionSetDomainObject.configuration.conditionCollection.length) { if (this.conditionSetDomainObject.configuration.conditionCollection.length) {
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => { this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
@ -86,6 +87,30 @@ export default class ConditionManager extends EventEmitter {
this.conditionClassCollection.forEach((condition) => condition.updateTelemetry()); this.conditionClassCollection.forEach((condition) => condition.updateTelemetry());
} }
removeConditionTelemetry() {
let conditionsChanged = false;
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration) => {
conditionConfiguration.configuration.criteria.forEach((criterion, index) => {
const isAnyAllTelemetry = criterion.telemetry && (criterion.telemetry === 'any' || criterion.telemetry === 'all');
if (!isAnyAllTelemetry) {
const found = Object.values(this.telemetryObjects).find((telemetryObject) => {
return this.openmct.objects.areIdsEqual(telemetryObject.identifier, criterion.telemetry);
});
if (!found) {
criterion.telemetry = '';
criterion.metadata = '';
criterion.input = [];
criterion.operation = '';
conditionsChanged = true;
}
}
});
});
if (conditionsChanged) {
this.persistConditions();
}
}
updateCondition(conditionConfiguration, index) { updateCondition(conditionConfiguration, index) {
let condition = this.conditionClassCollection[index]; let condition = this.conditionClassCollection[index];
condition.update(conditionConfiguration); condition.update(conditionConfiguration);
@ -95,7 +120,6 @@ export default class ConditionManager extends EventEmitter {
initCondition(conditionConfiguration, index) { initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this); let condition = new Condition(conditionConfiguration, this.openmct, this);
condition.on('conditionResultUpdated', this.handleConditionResult.bind(this));
if (index !== undefined) { if (index !== undefined) {
this.conditionClassCollection.splice(index + 1, 0, condition); this.conditionClassCollection.splice(index + 1, 0, condition);
} else { } else {
@ -159,22 +183,16 @@ export default class ConditionManager extends EventEmitter {
removeCondition(index) { removeCondition(index) {
let condition = this.conditionClassCollection[index]; let condition = this.conditionClassCollection[index];
condition.destroyCriteria(); condition.destroy();
condition.off('conditionResultUpdated', this.handleConditionResult.bind(this));
this.conditionClassCollection.splice(index, 1); this.conditionClassCollection.splice(index, 1);
this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1); this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1);
if (this.conditionResults[condition.id] !== undefined) {
delete this.conditionResults[condition.id];
}
this.persistConditions(); this.persistConditions();
this.handleConditionResult();
} }
findConditionById(id) { findConditionById(id) {
return this.conditionClassCollection.find(conditionClass => conditionClass.id === id); return this.conditionClassCollection.find(conditionClass => conditionClass.id === id);
} }
//this.$set(this.conditionClassCollection, reorderEvent.newIndex, oldConditions[reorderEvent.oldIndex]);
reorderConditions(reorderPlan) { reorderConditions(reorderPlan) {
let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection); let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection);
let newCollection = []; let newCollection = [];
@ -191,7 +209,23 @@ export default class ConditionManager extends EventEmitter {
let currentCondition = conditionCollection[conditionCollection.length-1]; let currentCondition = conditionCollection[conditionCollection.length-1];
for (let i = 0; i < conditionCollection.length - 1; i++) { for (let i = 0; i < conditionCollection.length - 1; i++) {
if (this.conditionResults[conditionCollection[i].id]) { const condition = this.findConditionById(conditionCollection[i].id)
if (condition.result) {
//first condition to be true wins
currentCondition = conditionCollection[i];
break;
}
}
return currentCondition;
}
getCurrentConditionLAD(conditionResults) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length-1];
for (let i = 0; i < conditionCollection.length - 1; i++) {
if (conditionResults[conditionCollection[i].id]) {
//first condition to be true wins //first condition to be true wins
currentCondition = conditionCollection[i]; currentCondition = conditionCollection[i];
break; break;
@ -200,24 +234,79 @@ export default class ConditionManager extends EventEmitter {
return currentCondition; return currentCondition;
} }
updateConditionResults(resultObj) { requestLADConditionSetOutput() {
if (!resultObj) { if (!this.conditionClassCollection.length) {
return Promise.resolve([]);
}
return this.compositionLoad.then(() => {
let latestTimestamp;
let conditionResults = {};
const conditionRequests = this.conditionClassCollection
.map(condition => condition.requestLADConditionResult());
return Promise.all(conditionRequests)
.then((results) => {
results.forEach(resultObj => {
const { id, data, data: { result } } = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
if (!Object.values(latestTimestamp).some(timeSystem => timeSystem)) {
return [];
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
const currentOutput = Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
latestTimestamp
);
return [currentOutput];
});
});
}
isTelemetryUsed(endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
for(const condition of this.conditionClassCollection) {
if (condition.isTelemetryUsed(id)) {
return true;
}
}
return false;
}
telemetryReceived(endpoint, datum) {
if (!this.isTelemetryUsed(endpoint)) {
return; return;
} }
const id = resultObj.id; const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.timeSystem().key;
let timestamp = {};
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
if (this.findConditionById(id)) { this.conditionClassCollection.forEach(condition => {
this.conditionResults[id] = resultObj.data.result; condition.getResult(normalizedDatum);
} });
this.updateTimestamp(resultObj.data);
}
handleConditionResult(resultObj) {
// update conditions results and then calculate the current condition
this.updateConditionResults(resultObj);
const currentCondition = this.getCurrentCondition(); const currentCondition = this.getCurrentCondition();
this.emit('conditionSetResultUpdated', this.emit('conditionSetResultUpdated',
Object.assign( Object.assign(
{ {
@ -225,51 +314,11 @@ export default class ConditionManager extends EventEmitter {
id: this.conditionSetDomainObject.identifier, id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id conditionId: currentCondition.id
}, },
this.latestTimestamp timestamp
) )
) )
} }
updateTimestamp(timestamp) {
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
if (!this.latestTimestamp[timeSystem.key]
|| timestamp[timeSystem.key] > this.latestTimestamp[timeSystem.key]
) {
this.latestTimestamp[timeSystem.key] = timestamp[timeSystem.key];
}
});
}
requestLADConditionSetOutput() {
if (!this.conditionClassCollection.length) {
return Promise.resolve([]);
}
return this.compositionLoad.then(() => {
const ladConditionResults = this.conditionClassCollection
.map(condition => condition.requestLADConditionResult());
return Promise.all(ladConditionResults)
.then((results) => {
results.forEach(resultObj => { this.updateConditionResults(resultObj); });
const currentCondition = this.getCurrentCondition();
return Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
this.latestTimestamp
);
});
});
}
broadcastTelemetry(id, datum) {
this.emit(`broadcastTelemetry`, Object.assign({}, this.createNormalizedDatum(datum, id), {id: id}));
}
getTestData(metadatum) { getTestData(metadatum) {
let data = undefined; let data = undefined;
if (this.testData.applied) { if (this.testData.applied) {
@ -281,13 +330,20 @@ export default class ConditionManager extends EventEmitter {
return data; return data;
} }
createNormalizedDatum(telemetryDatum, id) { createNormalizedDatum(telemetryDatum, endpoint) {
return Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((normalizedDatum, metadatum) => { const id = this.openmct.objects.makeKeyString(endpoint.identifier);
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
const testValue = this.getTestData(metadatum); const testValue = this.getTestData(metadatum);
const formatter = this.openmct.telemetry.getValueFormatter(metadatum); const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
normalizedDatum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]); datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
return normalizedDatum; return datum;
}, {}); }, {});
normalizedDatum.id = id;
return normalizedDatum;
} }
updateTestData(testData) { updateTestData(testData) {
@ -303,14 +359,13 @@ export default class ConditionManager extends EventEmitter {
this.composition.off('add', this.subscribeToTelemetry, this); this.composition.off('add', this.subscribeToTelemetry, this);
this.composition.off('remove', this.unsubscribeFromTelemetry, this); this.composition.off('remove', this.unsubscribeFromTelemetry, this);
Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe()); Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe());
this.subscriptions = undefined; delete this.subscriptions;
if(this.stopObservingForChanges) { if(this.stopObservingForChanges) {
this.stopObservingForChanges(); this.stopObservingForChanges();
} }
this.conditionClassCollection.forEach((condition) => { this.conditionClassCollection.forEach((condition) => {
condition.off('conditionResultUpdated', this.handleConditionResult);
condition.destroy(); condition.destroy();
}) })
} }

View File

@ -49,6 +49,7 @@ describe('ConditionManager', () => {
}; };
let mockComposition; let mockComposition;
let loader; let loader;
let mockTimeSystems;
function mockAngularComponents() { function mockAngularComponents() {
let mockInjector = jasmine.createSpyObj('$injector', ['get']); let mockInjector = jasmine.createSpyObj('$injector', ['get']);
@ -111,10 +112,16 @@ describe('ConditionManager', () => {
openmct.objects.observe.and.returnValue(function () {}); openmct.objects.observe.and.returnValue(function () {});
openmct.objects.mutate.and.returnValue(function () {}); openmct.objects.mutate.and.returnValue(function () {});
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.on('broadcastTelemetry', mockListener); conditionMgr.on('telemetryReceived', mockListener);
}); });
it('creates a conditionCollection with a default condition', function () { it('creates a conditionCollection with a default condition', function () {

View File

@ -54,13 +54,22 @@ export default class ConditionSetMetadataProvider {
return { return {
values: this.getDomains().concat([ values: this.getDomains().concat([
{ {
name: 'Output', key: "state",
key: 'output', source: "output",
format: 'enum', name: "State",
format: "enum",
enumerations: enumerations, enumerations: enumerations,
hints: { hints: {
range: 1 range: 1
} }
},
{
key: "output",
name: "Value",
format: "string",
hints: {
range: 2
}
} }
]) ])
}; };

View File

@ -45,7 +45,7 @@ export default class ConditionSetTelemetryProvider {
return conditionManager.requestLADConditionSetOutput() return conditionManager.requestLADConditionSetOutput()
.then(latestOutput => { .then(latestOutput => {
return latestOutput ? [latestOutput] : []; return latestOutput;
}); });
} }

View File

@ -25,12 +25,12 @@ import {TRIGGER} from "./utils/constants";
import TelemetryCriterion from "./criterion/TelemetryCriterion"; import TelemetryCriterion from "./criterion/TelemetryCriterion";
let openmct = {}, let openmct = {},
mockListener,
testConditionDefinition, testConditionDefinition,
testTelemetryObject, testTelemetryObject,
conditionObj, conditionObj,
conditionManager, conditionManager,
mockBroadcastTelemetry; mockTelemetryReceived,
mockTimeSystems;
describe("The condition", function () { describe("The condition", function () {
@ -38,10 +38,9 @@ describe("The condition", function () {
conditionManager = jasmine.createSpyObj('conditionManager', conditionManager = jasmine.createSpyObj('conditionManager',
['on'] ['on']
); );
mockBroadcastTelemetry = jasmine.createSpy('listener'); mockTelemetryReceived = jasmine.createSpy('listener');
conditionManager.on('broadcastTelemetry', mockBroadcastTelemetry); conditionManager.on('telemetryReceived', mockTelemetryReceived);
mockListener = jasmine.createSpy('listener');
testTelemetryObject = { testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"}, identifier:{ namespace: "", key: "test-object"},
type: "test-object", type: "test-object",
@ -74,6 +73,12 @@ describe("The condition", function () {
openmct.telemetry.subscribe.and.returnValue(function () {}); openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values); openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values);
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
testConditionDefinition = { testConditionDefinition = {
id: '123-456', id: '123-456',
configuration: { configuration: {
@ -97,8 +102,6 @@ describe("The condition", function () {
openmct, openmct,
conditionManager conditionManager
); );
conditionObj.on('conditionUpdated', mockListener);
}); });
it("generates criteria with the correct properties", function () { it("generates criteria with the correct properties", function () {

View File

@ -23,10 +23,13 @@
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
export default class StyleRuleManager extends EventEmitter { export default class StyleRuleManager extends EventEmitter {
constructor(styleConfiguration, openmct, callback) { constructor(styleConfiguration, openmct, callback, suppressSubscriptionOnEdit) {
super(); super();
this.openmct = openmct; this.openmct = openmct;
this.callback = callback; this.callback = callback;
if (suppressSubscriptionOnEdit) {
this.openmct.editor.on('isEditing', this.toggleSubscription.bind(this));
}
if (styleConfiguration) { if (styleConfiguration) {
this.initialize(styleConfiguration); this.initialize(styleConfiguration);
if (styleConfiguration.conditionSetIdentifier) { if (styleConfiguration.conditionSetIdentifier) {
@ -37,9 +40,25 @@ export default class StyleRuleManager extends EventEmitter {
} }
} }
toggleSubscription(isEditing) {
this.isEditing = isEditing;
if (this.isEditing) {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
if (this.conditionSetIdentifier) {
this.applySelectedConditionStyle();
}
} else if (this.conditionSetIdentifier) {
this.subscribeToConditionSet();
}
}
initialize(styleConfiguration) { initialize(styleConfiguration) {
this.conditionSetIdentifier = styleConfiguration.conditionSetIdentifier; this.conditionSetIdentifier = styleConfiguration.conditionSetIdentifier;
this.staticStyle = styleConfiguration.staticStyle; this.staticStyle = styleConfiguration.staticStyle;
this.selectedConditionId = styleConfiguration.selectedConditionId;
this.defaultConditionId = styleConfiguration.defaultConditionId;
this.updateConditionStylesMap(styleConfiguration.styles || []); this.updateConditionStylesMap(styleConfiguration.styles || []);
} }
@ -54,7 +73,7 @@ export default class StyleRuleManager extends EventEmitter {
this.handleConditionSetResultUpdated(output[0]); this.handleConditionSetResultUpdated(output[0]);
} }
}); });
this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, output => this.handleConditionSetResultUpdated(output)); this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this));
}); });
} }
@ -66,12 +85,16 @@ export default class StyleRuleManager extends EventEmitter {
let isNewConditionSet = !this.conditionSetIdentifier || let isNewConditionSet = !this.conditionSetIdentifier ||
!this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier); !this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier);
this.initialize(styleConfiguration); this.initialize(styleConfiguration);
if (this.isEditing) {
this.applySelectedConditionStyle();
} else {
//Only resubscribe if the conditionSet has changed. //Only resubscribe if the conditionSet has changed.
if (isNewConditionSet) { if (isNewConditionSet) {
this.subscribeToConditionSet(); this.subscribeToConditionSet();
} }
} }
} }
}
updateConditionStylesMap(conditionStyles) { updateConditionStylesMap(conditionStyles) {
let conditionStyleMap = {}; let conditionStyleMap = {};
@ -103,13 +126,23 @@ export default class StyleRuleManager extends EventEmitter {
} }
} }
applySelectedConditionStyle() {
const conditionId = this.selectedConditionId || this.defaultConditionId;
if (!conditionId) {
this.applyStaticStyle();
} else if (this.conditionalStyleMap[conditionId]) {
this.currentStyle = this.conditionalStyleMap[conditionId];
this.updateDomainObjectStyle();
}
}
applyStaticStyle() { applyStaticStyle() {
if (this.staticStyle) { if (this.staticStyle) {
this.currentStyle = this.staticStyle.style; this.currentStyle = this.staticStyle.style;
} else { } else {
if (this.currentStyle) { if (this.currentStyle) {
Object.keys(this.currentStyle).forEach(key => { Object.keys(this.currentStyle).forEach(key => {
this.currentStyle[key] = 'transparent'; this.currentStyle[key] = '__no_value';
}); });
} }
} }
@ -123,6 +156,8 @@ export default class StyleRuleManager extends EventEmitter {
} }
delete this.stopProvidingTelemetry; delete this.stopProvidingTelemetry;
this.conditionSetIdentifier = undefined; this.conditionSetIdentifier = undefined;
this.isEditing = undefined;
this.callback = undefined;
} }
} }

View File

@ -21,8 +21,16 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div class="c-condition-h"
:class="{ 'is-drag-target': draggingOver }"
@dragover.prevent
@drop.prevent="dropCondition($event, conditionIndex)"
@dragenter="dragEnter($event, conditionIndex)"
@dragleave="dragLeave($event, conditionIndex)"
>
<div class="c-condition-h__drop-target"></div>
<div v-if="isEditing" <div v-if="isEditing"
class="c-condition c-condition--edit js-condition-drag-wrapper" class="c-condition c-condition--edit"
> >
<!-- Edit view --> <!-- Edit view -->
<div class="c-condition__header"> <div class="c-condition__header">
@ -31,8 +39,7 @@
:class="[{ 'is-enabled': !condition.isDefault }, { 'hide-nice': condition.isDefault }]" :class="[{ 'is-enabled': !condition.isDefault }, { 'hide-nice': condition.isDefault }]"
:draggable="!condition.isDefault" :draggable="!condition.isDefault"
@dragstart="dragStart" @dragstart="dragStart"
@dragstop="dragStop" @dragend="dragEnd"
@dragover.stop
></span> ></span>
<span class="c-condition__disclosure c-disclosure-triangle c-tree__item__view-control is-enabled" <span class="c-condition__disclosure c-disclosure-triangle c-tree__item__view-control is-enabled"
@ -75,7 +82,7 @@
<input v-model="condition.configuration.name" <input v-model="condition.configuration.name"
class="t-condition-input__name" class="t-condition-input__name"
type="text" type="text"
@blur="persist" @change="persist"
> >
</span> </span>
@ -98,7 +105,7 @@
v-model="condition.configuration.output" v-model="condition.configuration.output"
class="t-condition-name-input" class="t-condition-name-input"
type="text" type="text"
@blur="persist" @change="persist"
> >
</span> </span>
</span> </span>
@ -176,6 +183,7 @@
/> />
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -207,6 +215,14 @@ export default {
type: Array, type: Array,
required: true, required: true,
default: () => [] default: () => []
},
isDragging: {
type: Boolean,
default: false
},
moveIndex: {
type: Number,
default: 0
} }
}, },
data() { data() {
@ -217,8 +233,8 @@ export default {
selectedOutputSelection: '', selectedOutputSelection: '',
outputOptions: ['false', 'true', 'string'], outputOptions: ['false', 'true', 'string'],
criterionIndex: 0, criterionIndex: 0,
selectedTelemetryName: '', draggingOver: false,
selectedFieldName: '' isDefault: this.condition.isDefault
}; };
}, },
computed: { computed: {
@ -286,11 +302,39 @@ export default {
dragStart(e) { dragStart(e) {
e.dataTransfer.setData('dragging', e.target); // required for FF to initiate drag e.dataTransfer.setData('dragging', e.target); // required for FF to initiate drag
e.dataTransfer.effectAllowed = "copyMove"; e.dataTransfer.effectAllowed = "copyMove";
e.dataTransfer.setDragImage(e.target.closest('.js-condition-drag-wrapper'), 0, 0); e.dataTransfer.setDragImage(e.target.closest('.c-condition-h'), 0, 0);
this.$emit('setMoveIndex', this.conditionIndex); this.$emit('setMoveIndex', this.conditionIndex);
}, },
dragStop(e) { dragEnd(event) {
e.dataTransfer.clearData(); this.dragStarted = false;
event.dataTransfer.clearData();
this.$emit('dragComplete');
},
dropCondition(event, targetIndex) {
if (!this.isDragging) { return }
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.isValidTarget(targetIndex)) {
this.dragElement = undefined;
this.draggingOver = false;
this.$emit('dropCondition', targetIndex);
}
},
dragEnter(event, targetIndex) {
if (!this.isDragging) { return }
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.isValidTarget(targetIndex)) {
this.dragElement = event.target.parentElement;
this.draggingOver = true;
}
},
dragLeave(event) {
if (event.target.parentElement === this.dragElement) {
this.draggingOver = false;
this.dragElement = undefined;
}
},
isValidTarget(targetIndex) {
return this.moveIndex !== targetIndex;
}, },
destroy() { destroy() {
}, },

View File

@ -22,7 +22,6 @@
<template> <template>
<section id="conditionCollection" <section id="conditionCollection"
class="c-cs__conditions"
:class="{ 'is-expanded': expanded }" :class="{ 'is-expanded': expanded }"
> >
<div class="c-cs__header c-section__header"> <div class="c-cs__header c-section__header">
@ -53,30 +52,26 @@
<span class="c-cs-button__label">Add Condition</span> <span class="c-cs-button__label">Add Condition</span>
</button> </button>
<div class="c-cs__conditions-h"> <div class="c-cs__conditions-h"
<div v-for="(condition, index) in conditionCollection" :class="{ 'is-active-dragging': isDragging }"
:key="condition.id"
class="c-condition-h"
> >
<div v-if="isEditing" <Condition v-for="(condition, index) in conditionCollection"
class="c-c__drag-ghost" :key="condition.id"
@drop.prevent="dropCondition" :condition="condition"
@dragenter="dragEnter"
@dragleave="dragLeave"
@dragover.prevent
></div>
<Condition :condition="condition"
:condition-index="index" :condition-index="index"
:telemetry="telemetryObjs" :telemetry="telemetryObjs"
:is-editing="isEditing" :is-editing="isEditing"
:move-index="moveIndex"
:is-dragging="isDragging"
@updateCondition="updateCondition" @updateCondition="updateCondition"
@removeCondition="removeCondition" @removeCondition="removeCondition"
@cloneCondition="cloneCondition" @cloneCondition="cloneCondition"
@setMoveIndex="setMoveIndex" @setMoveIndex="setMoveIndex"
@dragComplete="dragComplete"
@dropCondition="dropCondition"
/> />
</div> </div>
</div> </div>
</div>
</section> </section>
</template> </template>
@ -109,9 +104,10 @@ export default {
conditionResults: {}, conditionResults: {},
conditions: [], conditions: [],
telemetryObjs: [], telemetryObjs: [],
moveIndex: Number, moveIndex: undefined,
isDragging: false, isDragging: false,
defaultOutput: undefined defaultOutput: undefined,
dragCounter: 0
}; };
}, },
watch: { watch: {
@ -166,9 +162,7 @@ export default {
this.moveIndex = index; this.moveIndex = index;
this.isDragging = true; this.isDragging = true;
}, },
dropCondition(e) { dropCondition(targetIndex) {
let targetIndex = Array.from(document.querySelectorAll('.c-c__drag-ghost')).indexOf(e.target);
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
const oldIndexArr = Object.keys(this.conditionCollection); const oldIndexArr = Object.keys(this.conditionCollection);
const move = function (arr, old_index, new_index) { const move = function (arr, old_index, new_index) {
while (old_index < 0) { while (old_index < 0) {
@ -194,20 +188,10 @@ export default {
} }
this.reorder(reorderPlan); this.reorder(reorderPlan);
},
e.target.classList.remove("dragging"); dragComplete() {
this.isDragging = false; this.isDragging = false;
}, },
dragEnter(e) {
if (!this.isDragging) { return }
let targetIndex = Array.from(document.querySelectorAll('.c-c__drag-ghost')).indexOf(e.target);
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.moveIndex === targetIndex) { return }
e.target.classList.add("dragging");
},
dragLeave(e) {
e.target.classList.remove("dragging");
},
addTelemetryObject(domainObject) { addTelemetryObject(domainObject) {
this.telemetryObjs.push(domainObject); this.telemetryObjs.push(domainObject);
this.$emit('telemetryUpdated', this.telemetryObjs); this.$emit('telemetryUpdated', this.telemetryObjs);

View File

@ -23,24 +23,26 @@
<template> <template>
<div class="c-cs"> <div class="c-cs">
<section class="c-cs__current-output c-section"> <section class="c-cs__current-output c-section">
<div class="c-cs__header c-section__header">
<span class="c-cs__header-label c-section__label">Current Output</span>
</div>
<div class="c-cs__content c-cs__current-output-value"> <div class="c-cs__content c-cs__current-output-value">
<span class="c-cs__current-output-value__label">Current Output</span>
<span class="c-cs__current-output-value__value">
<template v-if="currentConditionOutput"> <template v-if="currentConditionOutput">
{{ currentConditionOutput }} {{ currentConditionOutput }}
</template> </template>
<template v-else> <template v-else>
{{ defaultConditionOutput }} {{ defaultConditionOutput }}
</template> </template>
</span>
</div> </div>
</section> </section>
<TestData :is-editing="isEditing" <div class="c-cs__test-data-and-conditions-w">
<TestData class="c-cs__test-data"
:is-editing="isEditing"
:test-data="testData" :test-data="testData"
:telemetry="telemetryObjs" :telemetry="telemetryObjs"
@updateTestData="updateTestData" @updateTestData="updateTestData"
/> />
<ConditionCollection <ConditionCollection class="c-cs__conditions"
:is-editing="isEditing" :is-editing="isEditing"
:test-data="testData" :test-data="testData"
@conditionSetResultUpdated="updateCurrentOutput" @conditionSetResultUpdated="updateCurrentOutput"
@ -48,6 +50,7 @@
@telemetryUpdated="updateTelemetry" @telemetryUpdated="updateTelemetry"
/> />
</div> </div>
</div>
</template> </template>
<script> <script>

View File

@ -23,7 +23,6 @@
<template> <template>
<section v-show="isEditing" <section v-show="isEditing"
id="test-data" id="test-data"
class="c-cs__test-data"
:class="{ 'is-expanded': expanded }" :class="{ 'is-expanded': expanded }"
> >
<div class="c-cs__header c-section__header"> <div class="c-cs__header c-section__header">
@ -37,7 +36,7 @@
<div v-if="expanded" <div v-if="expanded"
class="c-cs__content" class="c-cs__content"
> >
<div class="c-cdef__controls" <div class="c-cs__test-data__controls c-cdef__controls"
:disabled="!telemetry.length" :disabled="!telemetry.length"
> >
<label class="c-toggle-switch"> <label class="c-toggle-switch">
@ -96,7 +95,7 @@
> >
</span> </span>
</span> </span>
<div class="c-test-datum__buttons"> <div class="c-cs-test__buttons">
<button class="c-click-icon c-test-data__duplicate-button icon-duplicate" <button class="c-click-icon c-test-data__duplicate-button icon-duplicate"
title="Duplicate this test datum" title="Duplicate this test datum"
@click="addTestInput(testInput)" @click="addTestInput(testInput)"

View File

@ -1,116 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
.c-cs {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
&__content {
display: flex;
flex-direction: column;
flex: 0 1 auto;
overflow: hidden;
> * {
flex: 0 0 auto;
overflow: hidden;
+ * {
margin-top: $interiorMarginSm;
}
}
.c-button {
align-self: start;
}
}
.is-editing & {
// Add some space to kick away from blue editing border indication
padding: $interiorMargin;
}
section {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__conditions-h {
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
&__conditions {
> * + * {
margin-top: $interiorMarginSm;
}
}
.hint {
padding: $interiorMarginSm;
}
/************************** SPECIFIC ITEMS */
&__current-output-value {
font-size: 1.25em;
padding: $interiorMargin;
}
}
/***************************** TEST DATA */
.c-cs-tests {
flex: 0 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.c-cs-test {
> * {
flex: 0 0 auto;
+ * {
margin-left: $interiorMargin;
}
}
&__controls {
display: flex;
flex: 1 1 auto;
> * + * {
margin-left: $interiorMargin;
}
}
}

View File

@ -1,133 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
.c-condition,
.c-test-datum {
@include discreteItem();
display: flex;
padding: $interiorMargin;
&--edit {
line-height: 160%; // For layout when inputs wrap, like in criteria
}
}
.c-condition {
flex-direction: column;
min-width: 400px;
> * + * {
margin-top: $interiorMarginSm;
}
&--browse {
.c-condition__summary {
border-top: 1px solid $colorInteriorBorder;
padding-top: $interiorMargin;
}
}
/***************************** HEADER */
&__header {
$h: 22px;
display: flex;
align-items: start;
align-content: stretch;
overflow: hidden;
min-height: $h;
line-height: $h;
> * {
flex: 0 0 auto;
+ * {
margin-left: $interiorMarginSm;
}
}
}
&__drag-grippy {
transform: translateY(50%);
}
&__name {
font-weight: bold;
align-self: baseline; // Fixes bold line-height offset problem
}
&__output,
&__summary {
flex: 1 1 auto;
}
}
/***************************** CONDITION DEFINITION, EDITING */
.c-cdef {
display: grid;
grid-row-gap: $interiorMarginSm;
grid-column-gap: $interiorMargin;
grid-auto-columns: min-content 1fr max-content;
align-items: start;
min-width: 150px;
margin-left: 29px;
overflow: hidden;
&__criteria,
&__match-and-criteria {
display: contents;
}
&__label {
grid-column: 1;
text-align: right;
white-space: nowrap;
}
&__separator {
grid-column: 1 / span 3;
}
&__controls {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
grid-column: 2;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
grid-column: 3;
}
}
.c-c__drag-ghost {
width: 100%;
min-height: $interiorMarginSm;
&.dragging {
min-height: 5em;
background-color: lightblue;
border-radius: 2px;
}
}

View File

@ -0,0 +1,311 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/***************************** DRAGGING */
.is-active-dragging {
.c-condition-h__drop-target {
height: 3px;
margin-bottom: $interiorMarginSm;
}
}
.c-condition-h {
&__drop-target {
border-radius: $controlCr;
height: 0;
min-height: 0;
transition: background-color, height;
transition-duration: 150ms;
}
&.is-drag-target {
.c-condition > * {
pointer-events: none; // Keeps the JS drop handler from being intercepted by internal elements
}
.c-condition-h__drop-target {
background-color: rgba($colorKey, 0.7);
}
}
}
.c-cs {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
/************************** CONDITION SET LAYOUT */
&__current-output {
flex: 0 0 auto;
}
&__test-data-and-conditions-w {
display: flex;
flex-direction: column;
flex: 1 1 auto;
height: 100%;
overflow: hidden;
}
&__test-data,
&__conditions {
flex: 0 0 auto;
overflow: hidden;
}
&__test-data {
flex: 0 0 auto;
max-height: 50%;
&.is-expanded {
margin-bottom: $interiorMargin * 4;
}
}
&__conditions {
flex: 1 1 auto;
> * + * {
margin-top: $interiorMarginSm;
}
}
&__content {
display: flex;
flex-direction: column;
flex: 0 1 auto;
overflow: hidden;
> * {
flex: 0 0 auto;
overflow: hidden;
+ * {
margin-top: $interiorMarginSm;
}
}
.c-button {
align-self: start;
}
}
.is-editing & {
// Add some space to kick away from blue editing border indication
padding: $interiorMargin;
}
section {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__conditions-h {
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.hint {
padding: $interiorMarginSm;
}
/************************** SPECIFIC ITEMS */
&__current-output-value {
flex-direction: row;
align-items: baseline;
padding: 0 $interiorMargin $interiorMarginLg $interiorMargin;
> * {
padding: $interiorMargin 0; // Must do this to align label and value
}
&__label {
color: $colorInspectorSectionHeaderFg;
opacity: 0.9;
text-transform: uppercase;
}
&__value {
$p: $interiorMargin * 3;
font-size: 1.25em;
margin-left: $interiorMargin;
padding-left: $p;
padding-right: $p;
background: rgba(black, 0.2);
border-radius: 5px;
}
}
}
/***************************** CONDITIONS AND TEST DATUM ELEMENTS */
.c-condition,
.c-test-datum {
@include discreteItem();
display: flex;
padding: $interiorMargin;
line-height: 170%; // Aligns text with controls like selects
}
.c-cdef,
.c-cs-test {
&__controls {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
white-space: nowrap;
}
}
.c-condition {
flex-direction: column;
min-width: 400px;
> * + * {
margin-top: $interiorMarginSm;
}
&--browse {
.c-condition__summary {
border-top: 1px solid $colorInteriorBorder;
padding-top: $interiorMargin;
}
}
/***************************** HEADER */
&__header {
$h: 22px;
display: flex;
align-items: start;
align-content: stretch;
overflow: hidden;
min-height: $h;
line-height: $h;
> * {
flex: 0 0 auto;
+ * {
margin-left: $interiorMarginSm;
}
}
}
&__drag-grippy {
transform: translateY(50%);
}
&__name {
font-weight: bold;
align-self: baseline; // Fixes bold line-height offset problem
}
&__output,
&__summary {
flex: 1 1 auto;
}
}
/***************************** CONDITION DEFINITION, EDITING */
.c-cdef {
display: grid;
grid-row-gap: $interiorMarginSm;
grid-column-gap: $interiorMargin;
grid-auto-columns: min-content 1fr max-content;
align-items: start;
min-width: 150px;
margin-left: 29px;
overflow: hidden;
&__criteria,
&__match-and-criteria {
display: contents;
}
&__label {
grid-column: 1;
text-align: right;
white-space: nowrap;
}
&__separator {
grid-column: 1 / span 3;
}
&__controls {
align-items: flex-start;
grid-column: 2;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
grid-column: 3;
}
}
.c-c__drag-ghost {
width: 100%;
min-height: $interiorMarginSm;
&.dragging {
min-height: 5em;
background-color: lightblue;
border-radius: 2px;
}
}
/***************************** TEST DATA */
.c-cs__test-data {
&__controls {
flex: 0 0 auto;
}
}
.c-cs-tests {
flex: 0 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.c-cs-test {
> * + * {
margin-left: $interiorMargin;
}
}

View File

@ -79,6 +79,8 @@
<div v-for="(conditionStyle, index) in conditionalStyles" <div v-for="(conditionStyle, index) in conditionalStyles"
:key="index" :key="index"
class="c-inspect-styles__condition" class="c-inspect-styles__condition"
:class="{'is-current': conditionStyle.conditionId === selectedConditionId}"
@click="applySelectedConditionStyle(conditionStyle.conditionId)"
> >
<condition-error :show-label="true" <condition-error :show-label="true"
:condition="getCondition(conditionStyle.conditionId)" :condition="getCondition(conditionStyle.conditionId)"
@ -105,6 +107,7 @@ import ConditionDescription from "@/plugins/condition/components/ConditionDescri
import ConditionError from "@/plugins/condition/components/ConditionError.vue"; import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import Vue from 'vue'; import Vue from 'vue';
import PreviewAction from "@/ui/preview/PreviewAction.js"; import PreviewAction from "@/ui/preview/PreviewAction.js";
import {getApplicableStylesForItem} from "@/plugins/condition/utils/styleUtils";
export default { export default {
name: 'ConditionalStylesView', name: 'ConditionalStylesView',
@ -115,24 +118,8 @@ export default {
}, },
inject: [ inject: [
'openmct', 'openmct',
'domainObject' 'selection'
], ],
props: {
itemId: {
type: String,
default: ''
},
initialStyles: {
type: Object,
default() {
return undefined;
}
},
canHide: {
type: Boolean,
default: false
}
},
data() { data() {
return { return {
conditionalStyles: [], conditionalStyles: [],
@ -141,13 +128,16 @@ export default {
isEditing: this.openmct.editor.isEditing(), isEditing: this.openmct.editor.isEditing(),
conditions: undefined, conditions: undefined,
conditionsLoaded: false, conditionsLoaded: false,
navigateToPath: '' navigateToPath: '',
selectedConditionId: ''
} }
}, },
destroyed() { destroyed() {
this.openmct.editor.off('isEditing', this.setEditState); this.removeListeners();
}, },
mounted() { mounted() {
this.itemId = '';
this.getDomainObjectFromSelection();
this.previewAction = new PreviewAction(this.openmct); this.previewAction = new PreviewAction(this.openmct);
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) { if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
let objectStyles = this.itemId ? this.domainObject.configuration.objectStyles[this.itemId] : this.domainObject.configuration.objectStyles; let objectStyles = this.itemId ? this.domainObject.configuration.objectStyles[this.itemId] : this.domainObject.configuration.objectStyles;
@ -162,6 +152,52 @@ export default {
this.openmct.editor.on('isEditing', this.setEditState); this.openmct.editor.on('isEditing', this.setEditState);
}, },
methods: { methods: {
isItemType(type, item) {
return item && (item.type === type);
},
getDomainObjectFromSelection() {
let layoutItem;
let domainObject;
if (this.selection[0].length > 1) {
//If there are more than 1 items in the this.selection[0] list, the first one could either be a sub domain object OR a layout drawing control.
//The second item in the this.selection[0] list is the container object (usually a layout)
layoutItem = this.selection[0][0].context.layoutItem;
const item = this.selection[0][0].context.item;
this.canHide = true;
if (item &&
(!layoutItem || (this.isItemType('subobject-view', layoutItem)))) {
domainObject = item;
} else {
domainObject = this.selection[0][1].context.item;
if (layoutItem) {
this.itemId = layoutItem.id;
}
}
} else {
domainObject = this.selection[0][0].context.item;
}
this.domainObject = domainObject;
this.initialStyles = getApplicableStylesForItem(domainObject, layoutItem);
this.$nextTick(() => {
this.removeListeners();
if (this.domainObject) {
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
this.stopObservingItems = this.openmct.objects.observe(this.domainObject, 'configuration.items', this.updateDomainObjectItemStyles);
}
});
},
removeListeners() {
if (this.stopObserving) {
this.stopObserving();
}
if (this.stopObservingItems) {
this.stopObservingItems();
}
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
},
initialize(conditionSetDomainObject) { initialize(conditionSetDomainObject) {
//If there are new conditions in the conditionSet we need to set those styles to default //If there are new conditions in the conditionSet we need to set those styles to default
this.conditionSetDomainObject = conditionSetDomainObject; this.conditionSetDomainObject = conditionSetDomainObject;
@ -170,6 +206,13 @@ export default {
}, },
setEditState(isEditing) { setEditState(isEditing) {
this.isEditing = isEditing; this.isEditing = isEditing;
if (this.isEditing) {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
} else {
this.subscribeToConditionSet();
}
}, },
addConditionSet() { addConditionSet() {
let conditionSetDomainObject; let conditionSetDomainObject;
@ -240,6 +283,8 @@ export default {
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {}; let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (this.itemId) { if (this.itemId) {
domainObjectStyles[this.itemId].conditionSetIdentifier = undefined; domainObjectStyles[this.itemId].conditionSetIdentifier = undefined;
domainObjectStyles[this.itemId].selectedConditionId = undefined;
domainObjectStyles[this.itemId].defaultConditionId = undefined;
delete domainObjectStyles[this.itemId].conditionSetIdentifier; delete domainObjectStyles[this.itemId].conditionSetIdentifier;
domainObjectStyles[this.itemId].styles = undefined; domainObjectStyles[this.itemId].styles = undefined;
delete domainObjectStyles[this.itemId].styles; delete domainObjectStyles[this.itemId].styles;
@ -248,6 +293,8 @@ export default {
} }
} else { } else {
domainObjectStyles.conditionSetIdentifier = undefined; domainObjectStyles.conditionSetIdentifier = undefined;
domainObjectStyles.selectedConditionId = undefined;
domainObjectStyles.defaultConditionId = undefined;
delete domainObjectStyles.conditionSetIdentifier; delete domainObjectStyles.conditionSetIdentifier;
domainObjectStyles.styles = undefined; domainObjectStyles.styles = undefined;
delete domainObjectStyles.styles; delete domainObjectStyles.styles;
@ -257,6 +304,43 @@ export default {
} }
this.persist(domainObjectStyles); this.persist(domainObjectStyles);
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
},
updateDomainObjectItemStyles(newItems) {
//check that all items that have been styles still exist. Otherwise delete those styles
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
let itemsToRemove = [];
let keys = Object.keys(domainObjectStyles);
//TODO: Need an easier way to find which properties are itemIds
keys.forEach((key) => {
const keyIsItemId = (key !== 'styles') &&
(key !== 'staticStyle') &&
(key !== 'defaultConditionId') &&
(key !== 'selectedConditionId') &&
(key !== 'conditionSetIdentifier');
if (keyIsItemId) {
if (!(newItems.find(item => item.id === key))) {
itemsToRemove.push(key);
}
}
});
if (itemsToRemove.length) {
this.removeItemStyles(itemsToRemove, domainObjectStyles);
}
},
removeItemStyles(itemIds, domainObjectStyles) {
itemIds.forEach(itemId => {
if (domainObjectStyles[itemId]) {
domainObjectStyles[itemId] = undefined;
delete domainObjectStyles[this.itemId];
}
});
if (_.isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
this.persist(domainObjectStyles);
}, },
initializeConditionalStyles() { initializeConditionalStyles() {
if (!this.conditions) { if (!this.conditions) {
@ -264,6 +348,9 @@ export default {
} }
let conditionalStyles = []; let conditionalStyles = [];
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => { this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
if (conditionConfiguration.isDefault) {
this.selectedConditionId = conditionConfiguration.id;
}
this.conditions[conditionConfiguration.id] = conditionConfiguration; this.conditions[conditionConfiguration.id] = conditionConfiguration;
let foundStyle = this.findStyleByConditionId(conditionConfiguration.id); let foundStyle = this.findStyleByConditionId(conditionConfiguration.id);
if (foundStyle) { if (foundStyle) {
@ -279,13 +366,39 @@ export default {
//we're doing this so that we remove styles for any conditions that have been removed from the condition set //we're doing this so that we remove styles for any conditions that have been removed from the condition set
this.conditionalStyles = conditionalStyles; this.conditionalStyles = conditionalStyles;
this.conditionsLoaded = true; this.conditionsLoaded = true;
this.persist(this.getDomainObjectConditionalStyle()); this.persist(this.getDomainObjectConditionalStyle(this.selectedConditionId));
if (!this.isEditing) {
this.subscribeToConditionSet();
}
},
subscribeToConditionSet() {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
if (this.conditionSetDomainObject) {
this.openmct.telemetry.request(this.conditionSetDomainObject)
.then(output => {
if (output && output.length) {
this.handleConditionSetResultUpdated(output[0]);
}
});
this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(this.conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this));
}
},
handleConditionSetResultUpdated(resultData) {
this.selectedConditionId = resultData ? resultData.conditionId : '';
}, },
initializeStaticStyle(objectStyles) { initializeStaticStyle(objectStyles) {
let staticStyle = objectStyles && objectStyles.staticStyle; let staticStyle = objectStyles && objectStyles.staticStyle;
this.staticStyle = staticStyle || { if (staticStyle) {
this.staticStyle = {
style: Object.assign({}, this.initialStyles, staticStyle.style)
};
} else {
this.staticStyle = {
style: Object.assign({}, this.initialStyles) style: Object.assign({}, this.initialStyles)
}; };
}
}, },
findStyleByConditionId(id) { findStyleByConditionId(id) {
return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id); return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id);
@ -298,14 +411,19 @@ export default {
let found = this.findStyleByConditionId(conditionStyle.conditionId); let found = this.findStyleByConditionId(conditionStyle.conditionId);
if (found) { if (found) {
found.style = conditionStyle.style; found.style = conditionStyle.style;
this.selectedConditionId = found.conditionId;
this.persist(this.getDomainObjectConditionalStyle()); this.persist(this.getDomainObjectConditionalStyle());
} }
}, },
getDomainObjectConditionalStyle() { getDomainObjectConditionalStyle(defaultConditionId) {
let objectStyle = { let objectStyle = {
styles: this.conditionalStyles, styles: this.conditionalStyles,
staticStyle: this.staticStyle staticStyle: this.staticStyle,
selectedConditionId: this.selectedConditionId
}; };
if (defaultConditionId) {
objectStyle.defaultConditionId = defaultConditionId;
}
if (this.conditionSetDomainObject) { if (this.conditionSetDomainObject) {
objectStyle.conditionSetIdentifier = this.conditionSetDomainObject.identifier; objectStyle.conditionSetIdentifier = this.conditionSetDomainObject.identifier;
} }
@ -327,6 +445,10 @@ export default {
getCondition(id) { getCondition(id) {
return this.conditions ? this.conditions[id] : {}; return this.conditions ? this.conditions[id] : {};
}, },
applySelectedConditionStyle(conditionId) {
this.selectedConditionId = conditionId;
this.persist(this.getDomainObjectConditionalStyle());
},
persist(style) { persist(style) {
this.openmct.objects.mutate(this.domainObject, 'configuration.objectStyles', style); this.openmct.objects.mutate(this.domainObject, 'configuration.objectStyles', style);
} }

View File

@ -0,0 +1,269 @@
/*****************************************************************************
* 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.
*****************************************************************************/
<template>
<div class="c-inspector__styles c-inspect-styles">
<div class="c-inspect-styles__header">
Object Style
</div>
<div class="c-inspect-styles__content">
<div v-if="isStaticAndConditionalStyles"
class="c-inspect-styles__mixed-static-and-conditional u-alert u-alert--block u-alert--with-icon"
>
Your selection includes one or more items that use Conditional Styling. Applying a static style below will replace any Conditional Styling with the new choice.
</div>
<div v-if="staticStyle"
class="c-inspect-styles__style"
>
<style-editor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
:mixed-styles="mixedStyles"
@persist="updateStaticStyle"
/>
</div>
</div>
</div>
</template>
<script>
import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionalStyleForItem } from "@/plugins/condition/utils/styleUtils";
export default {
name: 'MultiSelectStylesView',
components: {
StyleEditor
},
inject: [
'openmct',
'selection'
],
data() {
return {
staticStyle: undefined,
isEditing: this.openmct.editor.isEditing(),
mixedStyles: [],
isStaticAndConditionalStyles: false
}
},
destroyed() {
this.removeListeners();
},
mounted() {
this.items = [];
this.previewAction = new PreviewAction(this.openmct);
this.getObjectsAndItemsFromSelection();
this.initializeStaticStyle();
this.openmct.editor.on('isEditing', this.setEditState);
},
methods: {
isItemType(type, item) {
return item && (item.type === type);
},
hasConditionalStyles(domainObject, id) {
return getConditionalStyleForItem(domainObject, id) !== undefined;
},
getObjectsAndItemsFromSelection() {
let domainObject;
let subObjects = [];
//multiple selection
let itemInitialStyles = [];
let itemStyle;
this.selection.forEach((selectionItem) => {
const item = selectionItem[0].context.item;
const layoutItem = selectionItem[0].context.layoutItem;
if (item && this.isItemType('subobject-view', layoutItem)) {
subObjects.push(item);
itemStyle = getApplicableStylesForItem(item);
if (!this.isStaticAndConditionalStyles) {
this.isStaticAndConditionalStyles = this.hasConditionalStyles(item);
}
} else {
domainObject = selectionItem[1].context.item;
itemStyle = getApplicableStylesForItem(domainObject, layoutItem || item);
this.items.push({
id: layoutItem.id,
applicableStyles: itemStyle
});
if (!this.isStaticAndConditionalStyles) {
this.isStaticAndConditionalStyles = this.hasConditionalStyles(domainObject, layoutItem.id);
}
}
itemInitialStyles.push(itemStyle);
});
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
this.initialStyles = styles;
this.mixedStyles = mixedStyles;
this.domainObject = domainObject;
this.removeListeners();
if (this.domainObject) {
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
this.stopObservingItems = this.openmct.objects.observe(this.domainObject, 'configuration.items', this.updateDomainObjectItemStyles);
}
subObjects.forEach(this.registerListener);
},
updateDomainObjectItemStyles(newItems) {
//check that all items that have been styles still exist. Otherwise delete those styles
let keys = Object.keys(this.domainObject.configuration.objectStyles || {});
keys.forEach((key) => {
if ((key !== 'styles') &&
(key !== 'staticStyle') &&
(key !== 'conditionSetIdentifier')) {
if (!(newItems.find(item => item.id === key))) {
this.removeItemStyles(key);
}
}
});
},
registerListener(domainObject) {
let id = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.domainObjectsById) {
this.domainObjectsById = {};
}
if (!this.domainObjectsById[id]) {
this.domainObjectsById[id] = domainObject;
this.observeObject(domainObject, id);
}
},
observeObject(domainObject, id) {
let unobserveObject = this.openmct.objects.observe(domainObject, '*', function (newObject) {
this.domainObjectsById[id] = JSON.parse(JSON.stringify(newObject));
}.bind(this));
this.unObserveObjects.push(unobserveObject);
},
removeListeners() {
if (this.stopObserving) {
this.stopObserving();
}
if (this.stopObservingItems) {
this.stopObservingItems();
}
if (this.unObserveObjects) {
this.unObserveObjects.forEach((unObserveObject) => {
unObserveObject();
});
}
this.unObserveObjects = [];
},
removeItemStyles(itemId) {
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (itemId && domainObjectStyles[itemId]) {
domainObjectStyles[itemId] = undefined;
delete domainObjectStyles[this.itemId];
if (_.isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
this.persist(this.domainObject, domainObjectStyles);
}
},
removeConditionalStyles(domainObjectStyles, itemId) {
if (itemId) {
domainObjectStyles[itemId].conditionSetIdentifier = undefined;
delete domainObjectStyles[itemId].conditionSetIdentifier;
domainObjectStyles[itemId].styles = undefined;
delete domainObjectStyles[itemId].styles;
} else {
domainObjectStyles.conditionSetIdentifier = undefined;
delete domainObjectStyles.conditionSetIdentifier;
domainObjectStyles.styles = undefined;
delete domainObjectStyles.styles;
}
},
setEditState(isEditing) {
this.isEditing = isEditing;
},
initializeStaticStyle() {
this.staticStyle = {
style: Object.assign({}, this.initialStyles)
};
},
updateStaticStyle(staticStyle, property) {
//update the static style for each of the layoutItems as well as each sub object item
this.staticStyle = staticStyle;
this.persist(this.domainObject, this.getDomainObjectStyle(this.domainObject, property, this.items));
if (this.domainObjectsById) {
const keys = Object.keys(this.domainObjectsById);
keys.forEach(key => {
let domainObject = this.domainObjectsById[key];
this.persist(domainObject, this.getDomainObjectStyle(domainObject, property));
});
}
this.isStaticAndConditionalStyles = false;
let foundIndex = this.mixedStyles.indexOf(property);
if (foundIndex > -1) {
this.mixedStyles.splice(foundIndex, 1);
}
},
getDomainObjectStyle(domainObject, property, items) {
let domainObjectStyles = (domainObject.configuration && domainObject.configuration.objectStyles) || {};
if (items) {
items.forEach(item => {
let itemStaticStyle = {};
if (domainObjectStyles[item.id] && domainObjectStyles[item.id].staticStyle) {
itemStaticStyle = domainObjectStyles[item.id].staticStyle.style;
}
Object.keys(item.applicableStyles).forEach(key => {
if (property === key) {
itemStaticStyle[key] = this.staticStyle.style[key];
}
});
if (this.isStaticAndConditionalStyles) {
this.removeConditionalStyles(domainObjectStyles, item.id);
}
if (_.isEmpty(itemStaticStyle)) {
itemStaticStyle = undefined;
domainObjectStyles[item.id] = undefined;
} else {
domainObjectStyles[item.id] = Object.assign({}, { staticStyle: { style: itemStaticStyle } });
}
});
} else {
if (!domainObjectStyles.staticStyle) {
domainObjectStyles.staticStyle = {
style: {}
}
}
if (this.isStaticAndConditionalStyles) {
this.removeConditionalStyles(domainObjectStyles);
}
domainObjectStyles.staticStyle.style[property] = this.staticStyle.style[property];
}
return domainObjectStyles;
},
persist(domainObject, style) {
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
}
}
}
</script>

View File

@ -22,38 +22,41 @@
<template> <template>
<div class="c-style"> <div class="c-style">
<span class="c-style-thumb" <span :class="[
:class="{ 'is-style-invisible': styleItem.style.isStyleInvisible }" { 'is-style-invisible': styleItem.style.isStyleInvisible },
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : styleItem.style ]" { 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
class="c-style-thumb"
> >
<span class="c-style-thumb__text" <span class="c-style-thumb__text"
:class="{ 'hide-nice': !styleItem.style.color }" :class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
> >
ABC ABC
</span> </span>
</span> </span>
<span class="c-toolbar"> <span class="c-toolbar">
<toolbar-color-picker v-if="styleItem.style.border" <toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
class="c-style__toolbar-button--border-color u-menu-to--center" class="c-style__toolbar-button--border-color u-menu-to--center"
:options="borderColorOption" :options="borderColorOption"
@change="updateStyleValue" @change="updateStyleValue"
/> />
<toolbar-color-picker v-if="styleItem.style.backgroundColor" <toolbar-color-picker v-if="hasProperty(styleItem.style.backgroundColor)"
class="c-style__toolbar-button--background-color u-menu-to--center" class="c-style__toolbar-button--background-color u-menu-to--center"
:options="backgroundColorOption" :options="backgroundColorOption"
@change="updateStyleValue" @change="updateStyleValue"
/> />
<toolbar-color-picker v-if="styleItem.style.color" <toolbar-color-picker v-if="hasProperty(styleItem.style.color)"
class="c-style__toolbar-button--color u-menu-to--center" class="c-style__toolbar-button--color u-menu-to--center"
:options="colorOption" :options="colorOption"
@change="updateStyleValue" @change="updateStyleValue"
/> />
<toolbar-button v-if="styleItem.style.imageUrl !== undefined" <toolbar-button v-if="hasProperty(styleItem.style.imageUrl)"
class="c-style__toolbar-button--image-url" class="c-style__toolbar-button--image-url"
:options="imageUrlOption" :options="imageUrlOption"
@change="updateStyleValue" @change="updateStyleValue"
/> />
<toolbar-toggle-button v-if="styleItem.style.isStyleInvisible !== undefined" <toolbar-toggle-button v-if="hasProperty(styleItem.style.isStyleInvisible)"
class="c-style__toolbar-button--toggle-visible" class="c-style__toolbar-button--toggle-visible"
:options="isStyleInvisibleOption" :options="isStyleInvisibleOption"
@change="updateStyleValue" @change="updateStyleValue"
@ -68,6 +71,7 @@ import ToolbarColorPicker from "@/ui/toolbar/components/toolbar-color-picker.vue
import ToolbarButton from "@/ui/toolbar/components/toolbar-button.vue"; import ToolbarButton from "@/ui/toolbar/components/toolbar-button.vue";
import ToolbarToggleButton from "@/ui/toolbar/components/toolbar-toggle-button.vue"; import ToolbarToggleButton from "@/ui/toolbar/components/toolbar-toggle-button.vue";
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants"; import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
export default { export default {
name: 'StyleEditor', name: 'StyleEditor',
@ -83,37 +87,52 @@ export default {
isEditing: { isEditing: {
type: Boolean type: Boolean
}, },
mixedStyles: {
type: Array,
default() {
return [];
}
},
styleItem: { styleItem: {
type: Object, type: Object,
required: true required: true
} }
}, },
computed: { computed: {
itemStyle() {
return getStylesWithoutNoneValue(this.styleItem.style);
},
borderColorOption() { borderColorOption() {
let value = this.styleItem.style.border.replace('1px solid ', '');
return { return {
icon: 'icon-line-horz', icon: 'icon-line-horz',
title: STYLE_CONSTANTS.borderColorTitle, title: STYLE_CONSTANTS.borderColorTitle,
value: this.styleItem.style.border.replace('1px solid ', ''), value: this.normalizeValueForSwatch(value),
property: 'border', property: 'border',
isEditing: this.isEditing isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('border') > -1
} }
}, },
backgroundColorOption() { backgroundColorOption() {
let value = this.styleItem.style.backgroundColor;
return { return {
icon: 'icon-paint-bucket', icon: 'icon-paint-bucket',
title: STYLE_CONSTANTS.backgroundColorTitle, title: STYLE_CONSTANTS.backgroundColorTitle,
value: this.styleItem.style.backgroundColor, value: this.normalizeValueForSwatch(value),
property: 'backgroundColor', property: 'backgroundColor',
isEditing: this.isEditing isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('backgroundColor') > -1
} }
}, },
colorOption() { colorOption() {
let value = this.styleItem.style.color;
return { return {
icon: 'icon-font', icon: 'icon-font',
title: STYLE_CONSTANTS.textColorTitle, title: STYLE_CONSTANTS.textColorTitle,
value: this.styleItem.style.color, value: this.normalizeValueForSwatch(value),
property: 'color', property: 'color',
isEditing: this.isEditing isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('color') > -1
} }
}, },
imageUrlOption() { imageUrlOption() {
@ -138,7 +157,8 @@ export default {
property: 'imageUrl', property: 'imageUrl',
formKeys: ['url'], formKeys: ['url'],
value: {url: this.styleItem.style.imageUrl}, value: {url: this.styleItem.style.imageUrl},
isEditing: this.isEditing isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
} }
}, },
isStyleInvisibleOption() { isStyleInvisibleOption() {
@ -163,7 +183,23 @@ export default {
} }
}, },
methods: { methods: {
hasProperty(property) {
return property !== undefined;
},
normalizeValueForSwatch(value) {
if (value && value.indexOf('__no_value') > -1) {
return value.replace('__no_value', 'transparent');
}
return value;
},
normalizeValueForStyle(value) {
if (value && value === 'transparent') {
return '__no_value';
}
return value;
},
updateStyleValue(value, item) { updateStyleValue(value, item) {
value = this.normalizeValueForStyle(value);
if (item.property === 'border') { if (item.property === 'border') {
value = '1px solid ' + value; value = '1px solid ' + value;
} }
@ -172,7 +208,7 @@ export default {
} else { } else {
this.styleItem.style[item.property] = value; this.styleItem.style[item.property] = value;
} }
this.$emit('persist', this.styleItem); this.$emit('persist', this.styleItem, item.property);
} }
} }
} }

View File

@ -60,6 +60,31 @@
&__condition { &__condition {
@include discreteItem(); @include discreteItem();
border: 1px solid transparent;
pointer-events: none; // Prevent selecting when the object isn't being edited
&.is-current {
$c: $colorBodyFg;
border-color: rgba($c, 0.5);
background: rgba($c, 0.2);
}
.is-editing & {
cursor: pointer;
pointer-events: initial;
transition: $transOut;
&:hover {
background: rgba($colorBodyFg, 0.1);
transition: $transIn;
}
&.is-current {
$c: $editUIColorBg;
border-color: $c;
background: rgba($c, 0.1);
}
}
} }
.c-style { .c-style {

View File

@ -20,11 +20,11 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import EventEmitter from 'EventEmitter'; import TelemetryCriterion from './TelemetryCriterion';
import {OPERATIONS} from '../utils/operations'; import { evaluateResults } from "../utils/evaluator";
import {computeCondition} from "@/plugins/condition/utils/evaluator"; import { getLatestTimestamp } from '../utils/time';
export default class TelemetryCriterion extends EventEmitter { export default class AllTelemetryCriterion extends TelemetryCriterion {
/** /**
* Subscribes/Unsubscribes to telemetry and emits the result * Subscribes/Unsubscribes to telemetry and emits the result
@ -34,23 +34,35 @@ export default class TelemetryCriterion extends EventEmitter {
* @param openmct * @param openmct
*/ */
constructor(telemetryDomainObjectDefinition, openmct) { constructor(telemetryDomainObjectDefinition, openmct) {
super(); super(telemetryDomainObjectDefinition, openmct);
}
this.openmct = openmct; initialize() {
this.objectAPI = this.openmct.objects; this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
this.telemetryAPI = this.openmct.telemetry;
this.timeAPI = this.openmct.time;
this.id = telemetryDomainObjectDefinition.id;
this.telemetry = telemetryDomainObjectDefinition.telemetry;
this.operation = telemetryDomainObjectDefinition.operation;
this.telemetryObjects = Object.assign({}, telemetryDomainObjectDefinition.telemetryObjects);
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.telemetryDataCache = {}; this.telemetryDataCache = {};
} }
isValid() {
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
}
updateTelemetry(telemetryObjects) { updateTelemetry(telemetryObjects) {
this.telemetryObjects = Object.assign({}, telemetryObjects); this.telemetryObjects = { ...telemetryObjects };
this.removeTelemetryDataCache();
}
removeTelemetryDataCache() {
const telemetryCacheIds = Object.keys(this.telemetryDataCache);
Object.values(this.telemetryObjects).forEach(telemetryObject => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
const foundIndex = telemetryCacheIds.indexOf(id);
if (foundIndex > -1) {
telemetryCacheIds.splice(foundIndex, 1);
}
});
telemetryCacheIds.forEach(id => {
delete (this.telemetryDataCache[id]);
});
} }
formatData(data, telemetryObjects) { formatData(data, telemetryObjects) {
@ -68,105 +80,87 @@ export default class TelemetryCriterion extends EventEmitter {
}); });
const datum = { const datum = {
result: computeCondition(this.telemetryDataCache, this.telemetry === 'all') result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry)
}; };
if (data) { if (data) {
// TODO check back to see if we should format times here this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key] datum[timeSystem.key] = data[timeSystem.key]
}); });
} }
return datum; return datum;
} }
handleSubscription(data, telemetryObjects) { getResult(data, telemetryObjects) {
if(this.isValid()) { const validatedData = this.isValid() ? data : {};
this.emitEvent('criterionResultUpdated', this.formatData(data, telemetryObjects));
} else { if (validatedData) {
this.emitEvent('criterionResultUpdated', this.formatData({}, telemetryObjects)); this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
}
} }
findOperation(operation) { Object.values(telemetryObjects).forEach(telemetryObject => {
for (let i=0; i < OPERATIONS.length; i++) { const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (operation === OPERATIONS[i].name) { if (this.telemetryDataCache[id] === undefined) {
return OPERATIONS[i].operation; this.telemetryDataCache[id] = false;
} }
}
return null;
}
computeResult(data) {
let result = false;
if (data) {
let comparator = this.findOperation(this.operation);
let params = [];
params.push(data[this.metadata]);
if (this.input instanceof Array && this.input.length) {
this.input.forEach(input => params.push(input));
}
if (typeof comparator === 'function') {
result = comparator(params);
}
}
return result;
}
emitEvent(eventName, data) {
this.emit(eventName, {
id: this.id,
data: data
}); });
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
} }
isValid() { requestLAD(telemetryObjects) {
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation; const options = {
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest', strategy: 'latest',
size: 1 size: 1
} };
);
if (!this.isValid()) { if (!this.isValid()) {
return this.formatData({}, options.telemetryObjects); return this.formatData({}, telemetryObjects);
} }
let keys = Object.keys(Object.assign({}, options.telemetryObjects)); let keys = Object.keys(Object.assign({}, telemetryObjects));
const telemetryRequests = keys const telemetryRequests = keys
.map(key => this.telemetryAPI.request( .map(key => this.openmct.telemetry.request(
options.telemetryObjects[key], telemetryObjects[key],
options options
)); ));
let telemetryDataCache = {};
return Promise.all(telemetryRequests) return Promise.all(telemetryRequests)
.then(telemetryRequestsResults => { .then(telemetryRequestsResults => {
let latestDatum; let latestTimestamp;
const timeSystems = this.openmct.time.getAllTimeSystems();
const timeSystem = this.openmct.time.timeSystem();
telemetryRequestsResults.forEach((results, index) => { telemetryRequestsResults.forEach((results, index) => {
latestDatum = results.length ? results[results.length - 1] : {}; const latestDatum = results.length ? results[results.length - 1] : {};
if (index < telemetryRequestsResults.length-1) { const datumId = keys[index];
if (latestDatum) { const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);
this.telemetryDataCache[latestDatum.id] = this.computeResult(latestDatum);
} telemetryDataCache[datumId] = this.computeResult(normalizedDatum);
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
normalizedDatum,
timeSystems,
timeSystem
);
}); });
const datum = {
result: evaluateResults(Object.values(telemetryDataCache), this.telemetry),
...latestTimestamp
};
return { return {
id: this.id, id: this.id,
data: this.formatData(latestDatum, options.telemetryObjects) data: datum
}; };
}); });
} }
destroy() { destroy() {
this.emitEvent('criterionRemoved');
delete this.telemetryObjects; delete this.telemetryObjects;
delete this.telemetryDataCache; delete this.telemetryDataCache;
delete this.telemetryObjectIdAsString;
delete this.telemetryObject;
} }
} }

View File

@ -36,44 +36,89 @@ export default class TelemetryCriterion extends EventEmitter {
super(); super();
this.openmct = openmct; this.openmct = openmct;
this.objectAPI = this.openmct.objects; this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
this.telemetryAPI = this.openmct.telemetry;
this.timeAPI = this.openmct.time;
this.id = telemetryDomainObjectDefinition.id; this.id = telemetryDomainObjectDefinition.id;
this.telemetry = telemetryDomainObjectDefinition.telemetry; this.telemetry = telemetryDomainObjectDefinition.telemetry;
this.operation = telemetryDomainObjectDefinition.operation; this.operation = telemetryDomainObjectDefinition.operation;
this.input = telemetryDomainObjectDefinition.input; this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata; this.metadata = telemetryDomainObjectDefinition.metadata;
this.telemetryObject = telemetryDomainObjectDefinition.telemetryObject; this.result = undefined;
this.telemetryObjectIdAsString = this.objectAPI.makeKeyString(telemetryDomainObjectDefinition.telemetry);
this.on(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription); this.initialize();
this.emitEvent('criterionUpdated', this); this.emitEvent('criterionUpdated', this);
} }
initialize() {
this.telemetryObject = this.telemetryDomainObjectDefinition.telemetryObject;
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
updateTelemetry(telemetryObjects) { updateTelemetry(telemetryObjects) {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString]; this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
} }
createNormalizedDatum(telemetryDatum, endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
datum[metadatum.key] = formatter.parse(telemetryDatum[metadatum.source]);
return datum;
}, {});
normalizedDatum.id = id;
return normalizedDatum;
}
formatData(data) { formatData(data) {
const datum = { const datum = {
result: this.computeResult(data) result: this.computeResult(data)
}; };
if (data) { if (data) {
// TODO check back to see if we should format times here this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key] datum[timeSystem.key] = data[timeSystem.key]
}); });
} }
return datum; return datum;
} }
handleSubscription(data) { getResult(data) {
if(this.isValid()) { const validatedData = this.isValid() ? data : {};
this.emitEvent('criterionResultUpdated', this.formatData(data)); this.result = this.computeResult(validatedData);
} else {
this.emitEvent('criterionResultUpdated', this.formatData({}));
} }
requestLAD() {
const options = {
strategy: 'latest',
size: 1
};
if (!this.isValid()) {
return {
id: this.id,
data: this.formatData({})
};
}
return this.openmct.telemetry.request(
this.telemetryObject,
options
).then(results => {
const latestDatum = results.length ? results[results.length - 1] : {};
const normalizedDatum = this.createNormalizedDatum(latestDatum, this.telemetryObject);
return {
id: this.id,
data: this.formatData(normalizedDatum)
};
});
} }
findOperation(operation) { findOperation(operation) {
@ -95,7 +140,7 @@ export default class TelemetryCriterion extends EventEmitter {
this.input.forEach(input => params.push(input)); this.input.forEach(input => params.push(input));
} }
if (typeof comparator === 'function') { if (typeof comparator === 'function') {
result = comparator(params); result = !!comparator(params);
} }
} }
return result; return result;
@ -108,42 +153,9 @@ export default class TelemetryCriterion extends EventEmitter {
}); });
} }
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
if (!this.isValid()) {
return {
id: this.id,
data: this.formatData({})
};
}
return this.telemetryAPI.request(
this.telemetryObject,
options
).then(results => {
const latestDatum = results.length ? results[results.length - 1] : {};
return {
id: this.id,
data: this.formatData(latestDatum)
};
});
}
destroy() { destroy() {
this.off(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription);
this.emitEvent('criterionRemoved');
delete this.telemetryObjectIdAsString;
delete this.telemetryObject; delete this.telemetryObject;
delete this.telemetryObjectIdAsString;
} }
} }

View File

@ -54,7 +54,7 @@ describe("The telemetry criterion", function () {
key: "testSource", key: "testSource",
source: "value", source: "value",
name: "Test", name: "Test",
format: "enum" format: "string"
}] }]
} }
}; };
@ -80,8 +80,9 @@ describe("The telemetry criterion", function () {
testCriterionDefinition = { testCriterionDefinition = {
id: 'test-criterion-id', id: 'test-criterion-id',
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier), telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
operation: 'lessThan', operation: 'textContains',
metadata: 'sin', metadata: 'value',
input: ['Hell'],
telemetryObject: testTelemetryObject telemetryObject: testTelemetryObject
}; };
@ -100,12 +101,21 @@ describe("The telemetry criterion", function () {
expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key); expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key);
}); });
it("updates and emits event on new data from telemetry providers", function () { it("returns a result on new data from relevant telemetry providers", function () {
spyOn(telemetryCriterion, 'emitEvent').and.callThrough(); telemetryCriterion.getResult({
telemetryCriterion.handleSubscription({
value: 'Hello', value: 'Hello',
utc: 'Hi' utc: 'Hi',
id: testTelemetryObject.identifier.key
}); });
expect(telemetryCriterion.emitEvent).toHaveBeenCalled(); 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();
// });
}); });

View File

@ -19,36 +19,50 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { TRIGGER } from "./constants";
export const computeCondition = (resultMap, allMustBeTrue) => { export const evaluateResults = (results, trigger) => {
let result = false; if (trigger && trigger === TRIGGER.XOR) {
for (let key in resultMap) { return matchExact(results, 1);
if (resultMap.hasOwnProperty(key)) { } else if (trigger && trigger === TRIGGER.NOT) {
result = resultMap[key]; return matchExact(results, 0);
if (allMustBeTrue && !result) { } else if (trigger && trigger === TRIGGER.ALL) {
//If we want all conditions to be true, then even one negative result should break. return matchAll(results);
break; } else {
} else if (!allMustBeTrue && result) { return matchAny(results);
//If we want at least one condition to be true, then even one positive result should break.
break;
} }
} }
}
return result;
};
//Returns true only if limit number of results are satisfied function matchAll(results) {
export const computeConditionByLimit = (resultMap, limit) => { for (const result of results) {
let trueCount = 0; if (!result) {
for (let key in resultMap) { return false;
if (resultMap.hasOwnProperty(key)) {
if (resultMap[key]) {
trueCount++;
}
if (trueCount > limit) {
break;
} }
} }
return true;
}
function matchAny(results) {
for (const result of results) {
if (result) {
return true;
}
}
return false;
}
function matchExact(results, target) {
let matches = 0;
for (const result of results) {
if (result) {
matches++;
}
if (matches > target) {
return false;
}
}
return matches === target;
} }
return trueCount === limit;
};

View File

@ -20,47 +20,185 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { computeConditionByLimit } from "./evaluator"; import { evaluateResults } from './evaluator';
import { TRIGGER } from './constants';
describe('evaluate results based on trigger', function () { describe('evaluate results', () => {
// const allTrue = [true, true, true, true, true];
// const oneTrue = [false, false, false, false, true];
// const multipleTrue = [false, true, false, true, false];
// const noneTrue = [false, false, false, false, false];
// const allTrueWithUndefined = [true, true, true, undefined, true];
// const oneTrueWithUndefined = [undefined, undefined, undefined, undefined, true];
// const multipleTrueWithUndefined = [true, undefined, true, undefined, true];
// const allUndefined = [undefined, undefined, undefined, undefined, undefined];
// const singleTrue = [true];
// const singleFalse = [false];
// const singleUndefined = [undefined];
// const empty = [];
it('should evaluate to true if trigger is NOT', () => { const tests = [
const results = { {
result: false, name: 'allTrue',
result1: false, values: [true, true, true, true, true],
result2: false any: true,
}; all: true,
const result = computeConditionByLimit(results, 0); not: false,
expect(result).toBeTrue(); xor: false
}); }, {
name: 'oneTrue',
values: [false, false, false, false, true],
any: true,
all: false,
not: false,
xor: true
}, {
name: 'multipleTrue',
values: [false, true, false, true, false],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'noneTrue',
values: [false, false, false, false, false],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'allTrueWithUndefined',
values: [true, true, true, undefined, true],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'oneTrueWithUndefined',
values: [undefined, undefined, undefined, undefined, true],
any: true,
all: false,
not: false,
xor: true
}, {
name: 'multipleTrueWithUndefined',
values: [true, undefined, true, undefined, true],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'allUndefined',
values: [undefined, undefined, undefined, undefined, undefined],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'singleTrue',
values: [true],
any: true,
all: true,
not: false,
xor: true
}, {
name: 'singleFalse',
values: [false],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'singleUndefined',
values: [undefined],
any: false,
all: false,
not: true,
xor: false
}
// , {
// name: 'empty',
// values: [],
// any: false,
// all: false,
// not: true,
// xor: false
// }
];
it('should evaluate to false if trigger is NOT', () => { describe(`based on trigger ${TRIGGER.ANY}`, () => {
const results = { it('should evaluate to expected result', () => {
result: true, tests.forEach(test => {
result1: false, const result = evaluateResults(test.values, TRIGGER.ANY);
result2: false expect(result).toEqual(test[TRIGGER.ANY])
};
const result = computeConditionByLimit(results, 0);
expect(result).toBeFalse();
});
it('should evaluate to true if trigger is XOR', () => {
const results = {
result: false,
result1: true,
result2: false
};
const result = computeConditionByLimit(results, 1);
expect(result).toBeTrue();
});
it('should evaluate to false if trigger is XOR', () => {
const results = {
result: false,
result1: true,
result2: true
};
const result = computeConditionByLimit(results, 1);
expect(result).toBeFalse();
}); });
}); });
});
describe(`based on trigger ${TRIGGER.ALL}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.ALL);
expect(result).toEqual(test[TRIGGER.ALL])
});
});
});
describe(`based on trigger ${TRIGGER.NOT}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.NOT);
expect(result).toEqual(test[TRIGGER.NOT])
});
});
});
describe(`based on trigger ${TRIGGER.XOR}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.XOR);
expect(result).toEqual(test[TRIGGER.XOR])
});
});
});
// it('should evaluate to true if trigger is NOT', () => {
// const results = {
// result: false,
// result1: false,
// result2: false
// };
// const result = computeConditionByLimit(results, 0);
// expect(result).toBeTrue();
// });
// it('should evaluate to false if trigger is NOT', () => {
// const results = {
// result: true,
// result1: false,
// result2: false
// };
// const result = computeConditionByLimit(results, 0);
// expect(result).toBeFalse();
// });
// it('should evaluate to true if trigger is XOR', () => {
// const results = {
// result: false,
// result1: true,
// result2: false
// };
// const result = computeConditionByLimit(results, 1);
// expect(result).toBeTrue();
// });
// it('should evaluate to false if trigger is XOR', () => {
// const results = {
// result: false,
// result1: true,
// result2: true
// };
// const result = computeConditionByLimit(results, 1);
// expect(result).toBeFalse();
// });
});

View File

@ -22,6 +22,22 @@
import _ from 'lodash'; import _ from 'lodash';
const convertToNumbers = (input) => {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
return numberInputs;
};
const convertToStrings = (input) => {
let stringInputs = [];
input.forEach(inputValue => stringInputs.push(inputValue !== undefined ? inputValue.toString() : ''));
return stringInputs;
};
const joinValues = (values, length) => {
return values.slice(0, length).join(', ');
};
export const OPERATIONS = [ export const OPERATIONS = [
{ {
name: 'equalTo', name: 'equalTo',
@ -32,7 +48,7 @@ export const OPERATIONS = [
appliesTo: ['number'], appliesTo: ['number'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' is ' + values.join(', '); return ' is ' + joinValues(values, 1);
} }
}, },
{ {
@ -44,7 +60,7 @@ export const OPERATIONS = [
appliesTo: ['number'], appliesTo: ['number'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' is not ' + values.join(', '); return ' is not ' + joinValues(values, 1);
} }
}, },
{ {
@ -56,7 +72,7 @@ export const OPERATIONS = [
appliesTo: ['number'], appliesTo: ['number'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' > ' + values.join(', '); return ' > ' + joinValues(values, 1);
} }
}, },
{ {
@ -68,7 +84,7 @@ export const OPERATIONS = [
appliesTo: ['number'], appliesTo: ['number'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' < ' + values.join(', '); return ' < ' + joinValues(values, 1);
} }
}, },
{ {
@ -80,7 +96,7 @@ export const OPERATIONS = [
appliesTo: ['number'], appliesTo: ['number'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' >= ' + values.join(', '); return ' >= ' + joinValues(values, 1);
} }
}, },
{ {
@ -92,14 +108,13 @@ export const OPERATIONS = [
appliesTo: ['number'], appliesTo: ['number'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' <= ' + values.join(', '); return ' <= ' + joinValues(values, 1);
} }
}, },
{ {
name: 'between', name: 'between',
operation: function (input) { operation: function (input) {
let numberInputs = []; let numberInputs = convertToNumbers(input);
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
let larger = Math.max(...numberInputs.slice(1,3)); let larger = Math.max(...numberInputs.slice(1,3));
let smaller = Math.min(...numberInputs.slice(1,3)); let smaller = Math.min(...numberInputs.slice(1,3));
return (numberInputs[0] > smaller) && (numberInputs[0] < larger); return (numberInputs[0] > smaller) && (numberInputs[0] < larger);
@ -114,8 +129,7 @@ export const OPERATIONS = [
{ {
name: 'notBetween', name: 'notBetween',
operation: function (input) { operation: function (input) {
let numberInputs = []; let numberInputs = convertToNumbers(input);
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
let larger = Math.max(...numberInputs.slice(1,3)); let larger = Math.max(...numberInputs.slice(1,3));
let smaller = Math.min(...numberInputs.slice(1,3)); let smaller = Math.min(...numberInputs.slice(1,3));
return (numberInputs[0] < smaller) || (numberInputs[0] > larger); return (numberInputs[0] < smaller) || (numberInputs[0] > larger);
@ -136,7 +150,7 @@ export const OPERATIONS = [
appliesTo: ['string'], appliesTo: ['string'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' contains ' + values.join(', '); return ' contains ' + joinValues(values, 1);
} }
}, },
{ {
@ -148,7 +162,7 @@ export const OPERATIONS = [
appliesTo: ['string'], appliesTo: ['string'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' does not contain ' + values.join(', '); return ' does not contain ' + joinValues(values, 1);
} }
}, },
{ {
@ -160,7 +174,7 @@ export const OPERATIONS = [
appliesTo: ['string'], appliesTo: ['string'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' starts with ' + values.join(', '); return ' starts with ' + joinValues(values, 1);
} }
}, },
{ {
@ -172,7 +186,7 @@ export const OPERATIONS = [
appliesTo: ['string'], appliesTo: ['string'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' ends with ' + values.join(', '); return ' ends with ' + joinValues(values, 1);
} }
}, },
{ {
@ -184,7 +198,7 @@ export const OPERATIONS = [
appliesTo: ['string'], appliesTo: ['string'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' is exactly ' + values.join(', '); return ' is exactly ' + joinValues(values, 1);
} }
}, },
{ {
@ -214,33 +228,36 @@ export const OPERATIONS = [
{ {
name: 'enumValueIs', name: 'enumValueIs',
operation: function (input) { operation: function (input) {
return input[0] === input[1]; let stringInputs = convertToStrings(input);
return stringInputs[0] === stringInputs[1];
}, },
text: 'is', text: 'is',
appliesTo: ['enum'], appliesTo: ['enum'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' is ' + values.join(', '); return ' is ' + joinValues(values, 1);
} }
}, },
{ {
name: 'enumValueIsNot', name: 'enumValueIsNot',
operation: function (input) { operation: function (input) {
return input[0] !== input[1]; let stringInputs = convertToStrings(input);
return stringInputs[0] !== stringInputs[1];
}, },
text: 'is not', text: 'is not',
appliesTo: ['enum'], appliesTo: ['enum'],
inputCount: 1, inputCount: 1,
getDescription: function (values) { getDescription: function (values) {
return ' is not ' + values.join(', '); return ' is not ' + joinValues(values, 1);
} }
}, },
{ {
name: 'valueIs', name: 'valueIs',
operation: function (input) { operation: function (input) {
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
if (input[1]) { if (input[1]) {
const values = input[1].split(','); const values = input[1].split(',');
return values.find((value) => input[0].toString() === _.trim(value.toString())); return values.find((value) => lhsValue === _.trim(value.toString()));
} }
return false; return false;
}, },
@ -254,9 +271,10 @@ export const OPERATIONS = [
{ {
name: 'valueIsNot', name: 'valueIsNot',
operation: function (input) { operation: function (input) {
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
if (input[1]) { if (input[1]) {
const values = input[1].split(','); const values = input[1].split(',');
const found = values.find((value) => input[0].toString() === _.trim(value.toString())); const found = values.find((value) => lhsValue === _.trim(value.toString()));
return !found; return !found;
} }
return false; return false;

View File

@ -25,8 +25,10 @@ let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueI
let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIsNot'); let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIsNot');
let isBetween = OPERATIONS.find((operation) => operation.name === 'between'); let isBetween = OPERATIONS.find((operation) => operation.name === 'between');
let isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween'); let isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween');
let enumIsOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIs');
let enumIsNotOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIsNot');
describe('Is one of and is not one of operations', function () { describe('operations', function () {
it('should evaluate isOneOf to true for number inputs', () => { it('should evaluate isOneOf to true for number inputs', () => {
const inputs = [45, "5,6,45,8"]; const inputs = [45, "5,6,45,8"];
@ -87,4 +89,54 @@ describe('Is one of and is not one of operations', function () {
const inputs = ["45", "30", "50"]; const inputs = ["45", "30", "50"];
expect(!!isNotBetween.operation(inputs)).toBeFalse(); expect(!!isNotBetween.operation(inputs)).toBeFalse();
}); });
it('should evaluate enumValueIs to true for number inputs', () => {
const inputs = [1, "1"];
expect(!!enumIsOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIs to true for string inputs', () => {
const inputs = ["45", "45"];
expect(!!enumIsOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIsNot to true for number inputs', () => {
const inputs = [45, "46"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIsNot to true for string inputs', () => {
const inputs = ["45", "46"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIs to false for number inputs', () => {
const inputs = [1, "2"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIs to false for string inputs', () => {
const inputs = ["45", "46"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to false for number inputs', () => {
const inputs = [45, "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to false for string inputs', () => {
const inputs = ["45", "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIs to false for undefined input', () => {
const inputs = [undefined, "45"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to true for undefined input', () => {
const inputs = [undefined, "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
}); });

View File

@ -19,27 +19,154 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const NONE_VALUE = '__no_value';
export const getStyleProp = (key, defaultValue) => { const styleProps = {
let styleProp = undefined; backgroundColor: {
switch(key) { svgProperty: 'fill',
case 'fill': styleProp = { noneValue: NONE_VALUE,
backgroundColor: defaultValue || 'transparent' applicableForType: type => {
}; return !type ? true : (type === 'text-view' ||
break; type === 'telemetry-view' ||
case 'stroke': styleProp = { type === 'box-view' ||
border: '1px solid ' + (defaultValue || 'transparent') type === 'subobject-view');
}; }
break; },
case 'color': styleProp = { border: {
color: defaultValue || 'transparent' svgProperty: 'stroke',
}; noneValue: NONE_VALUE,
break; applicableForType: type => {
case 'url': styleProp = { return !type ? true : (type === 'text-view' ||
imageUrl: defaultValue || 'transparent' type === 'telemetry-view' ||
}; type === 'box-view' ||
break; type === 'image-view' ||
type === 'line-view'||
type === 'subobject-view');
}
},
color: {
svgProperty: 'color',
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view' ||
type === 'telemetry-view'||
type === 'subobject-view');
}
},
imageUrl: {
svgProperty: 'url',
noneValue: '',
applicableForType: type => {
return !type ? false : type === 'image-view';
}
} }
};
return styleProp;
const aggregateStyleValues = (accumulator, currentStyle) => {
const styleKeys = Object.keys(currentStyle);
const properties = Object.keys(styleProps);
properties.forEach((property) => {
if (!accumulator[property]) {
accumulator[property] = [];
}
const found = styleKeys.find(key => key === property);
if (found) {
accumulator[property].push(currentStyle[found]);
}
});
return accumulator;
};
// Returns a union of styles used by multiple items.
// Styles that are common to all items but don't have the same value are added to the mixedStyles list
export const getConsolidatedStyleValues = (multipleItemStyles) => {
let aggregatedStyleValues = multipleItemStyles.reduce(aggregateStyleValues, {});
let styleValues = {};
let mixedStyles = [];
const properties = Object.keys(styleProps);
properties.forEach((property) => {
const values = aggregatedStyleValues[property];
if (values.length) {
if (values.every(value => value === values[0])) {
styleValues[property] = values[0];
} else {
styleValues[property] = '';
mixedStyles.push(property);
}
}
});
return {
styles: styleValues,
mixedStyles
};
};
const getStaticStyleForItem = (domainObject, id) => {
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
if (domainObjectStyles) {
if (id) {
if(domainObjectStyles[id] && domainObjectStyles[id].staticStyle) {
return domainObjectStyles[id].staticStyle.style;
}
} else if (domainObjectStyles.staticStyle) {
return domainObjectStyles.staticStyle.style;
}
}
};
export const getConditionalStyleForItem = (domainObject, id) => {
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
if (domainObjectStyles) {
if (id) {
if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) {
return domainObjectStyles[id].styles;
}
} else if (domainObjectStyles.staticStyle) {
return domainObjectStyles.styles;
}
}
};
//Returns either existing static styles or uses SVG defaults if available
export const getApplicableStylesForItem = (domainObject, item) => {
const type = item && item.type;
const id = item && item.id;
let style = {};
let staticStyle = getStaticStyleForItem(domainObject, id);
const properties = Object.keys(styleProps);
properties.forEach(property => {
const styleProp = styleProps[property];
if (styleProp.applicableForType(type)) {
let defaultValue;
if (staticStyle) {
defaultValue = staticStyle[property];
} else if (item) {
defaultValue = item[styleProp.svgProperty];
}
style[property] = defaultValue === undefined ? styleProp.noneValue : defaultValue;
}
});
return style;
};
export const getStylesWithoutNoneValue = (style) => {
if (_.isEmpty(style) || !style) {
return;
}
let styleObj = {};
const keys = Object.keys(style);
keys.forEach(key => {
if ((typeof style[key] === 'string')) {
if (style[key].indexOf('__no_value') > -1) {
style[key] = '';
} else {
styleObj[key] = style[key];
}
}
});
return styleObj;
}; };

View File

@ -0,0 +1,52 @@
/*****************************************************************************
* 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.
*****************************************************************************/
export const getLatestTimestamp = (
currentTimestamp,
compareTimestamp,
timeSystems,
currentTimeSystem
) => {
let latest = { ...currentTimestamp };
const compare = { ...compareTimestamp };
const key = currentTimeSystem.key;
if (!latest || !latest[key]) {
latest = updateLatestTimeStamp(compare, timeSystems)
}
if (compare[key] > latest[key]) {
latest = updateLatestTimeStamp(compare, timeSystems)
}
return latest;
}
function updateLatestTimeStamp(timestamp, timeSystems) {
let latest = {};
timeSystems.forEach(timeSystem => {
latest[timeSystem.key] = timestamp[timeSystem.key];
});
return latest;
}

View File

@ -21,13 +21,14 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<a class="c-condition-widget" <component :is="urlDefined ? 'a' : 'span'"
:href="internalDomainObject.url" class="c-condition-widget"
:href="urlDefined ? internalDomainObject.url : null"
> >
<div class="c-condition-widget__label"> <div class="c-condition-widget__label">
{{ internalDomainObject.label }} {{ internalDomainObject.label }}
</div> </div>
</a> </component>
</template> </template>
<script> <script>
@ -38,6 +39,11 @@ export default {
internalDomainObject: this.domainObject internalDomainObject: this.domainObject
} }
}, },
computed: {
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
}
},
mounted() { mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject); this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
}, },

View File

@ -28,10 +28,12 @@
border: 1px solid transparent; border: 1px solid transparent;
display: inline-block; display: inline-block;
padding: $interiorMarginLg $interiorMarginLg * 2; padding: $interiorMarginLg $interiorMarginLg * 2;
cursor: inherit !important;
&[href] {
cursor: pointer !important;
} }
a.c-condition-widget {
// Widget is conditionally made into a <a> when URL property has been defined
cursor: pointer !important;
pointer-events: inherit;
} }
// Make Condition Widget expand when in a hidden frame Layout context // Make Condition Widget expand when in a hidden frame Layout context

View File

@ -43,7 +43,7 @@ export default {
makeDefinition() { makeDefinition() {
return { return {
fill: '#717171', fill: '#717171',
stroke: 'transparent', stroke: '',
x: 1, x: 1,
y: 1, y: 1,
width: 10, width: 10,
@ -74,13 +74,14 @@ export default {
}, },
computed: { computed: {
style() { style() {
return Object.assign({ if (this.itemStyle) {
return this.itemStyle;
} else {
return {
backgroundColor: this.item.fill, backgroundColor: this.item.fill,
border: '1px solid ' + this.item.stroke border: this.item.stroke ? '1px solid ' + this.item.stroke : ''
}, this.itemStyle); };
}, }
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
} }
}, },
watch: { watch: {

View File

@ -74,13 +74,18 @@ export default {
}, },
computed: { computed: {
style() { style() {
let backgroundImage = 'url(' + this.item.url + ')';
let border = '1px solid ' + this.item.stroke;
if (this.itemStyle) {
if (this.itemStyle.imageUrl !== undefined) {
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
}
border = this.itemStyle.border;
}
return { return {
backgroundImage: this.itemStyle ? ('url(' + this.itemStyle.imageUrl + ')') : 'url(' + this.item.url + ')', backgroundImage,
border: (this.itemStyle && this.itemStyle.border) ? this.itemStyle.border : ('1px solid ' + this.item.stroke) border
}; };
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
} }
}, },
watch: { watch: {

View File

@ -127,8 +127,11 @@ export default {
return {x, y, x2, y2}; return {x, y, x2, y2};
}, },
stroke() { stroke() {
if (this.itemStyle && this.itemStyle.border) { if (this.itemStyle) {
if (this.itemStyle.border) {
return this.itemStyle.border.replace('1px solid ', ''); return this.itemStyle.border.replace('1px solid ', '');
}
return '';
} else { } else {
return this.item.stroke; return this.item.stroke;
} }
@ -146,9 +149,6 @@ export default {
height: `${height}px` height: `${height}px`
}; };
}, },
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
},
startHandleClass() { startHandleClass() {
return START_HANDLE_QUADRANTS[this.vectorQuadrant]; return START_HANDLE_QUADRANTS[this.vectorQuadrant];
}, },

View File

@ -30,14 +30,13 @@
<div <div
v-if="domainObject" v-if="domainObject"
class="c-telemetry-view" class="c-telemetry-view"
:class="styleClass"
:style="styleObject" :style="styleObject"
@contextmenu.prevent="showContextMenu" @contextmenu.prevent="showContextMenu"
> >
<div <div
v-if="showLabel" v-if="showLabel"
class="c-telemetry-view__label" class="c-telemetry-view__label"
:class="[styleClass]"
:style="objectStyle"
> >
<div class="c-telemetry-view__label-text"> <div class="c-telemetry-view__label-text">
{{ domainObject.name }} {{ domainObject.name }}
@ -48,8 +47,7 @@
v-if="showValue" v-if="showValue"
:title="fieldName" :title="fieldName"
class="c-telemetry-view__value" class="c-telemetry-view__value"
:class="[telemetryClass, !telemetryClass && styleClass]" :class="[telemetryClass]"
:style="!telemetryClass && objectStyle"
> >
<div class="c-telemetry-view__value-text"> <div class="c-telemetry-view__value-text">
{{ telemetryValue }} {{ telemetryValue }}
@ -62,7 +60,7 @@
<script> <script>
import LayoutFrame from './LayoutFrame.vue' import LayoutFrame from './LayoutFrame.vue'
import printj from 'printj' import printj from 'printj'
import StyleRuleManager from "../../condition/StyleRuleManager"; import conditionalStylesMixin from "../mixins/objectStyles-mixin";
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5], const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5],
DEFAULT_POSITION = [1, 1], DEFAULT_POSITION = [1, 1],
@ -81,8 +79,8 @@ export default {
height: DEFAULT_TELEMETRY_DIMENSIONS[1], height: DEFAULT_TELEMETRY_DIMENSIONS[1],
displayMode: 'all', displayMode: 'all',
value: metadata.getDefaultDisplayValue(), value: metadata.getDefaultDisplayValue(),
stroke: "transparent", stroke: "",
fill: "transparent", fill: "",
color: "", color: "",
size: "13px" size: "13px"
}; };
@ -91,6 +89,7 @@ export default {
components: { components: {
LayoutFrame LayoutFrame
}, },
mixins: [conditionalStylesMixin],
props: { props: {
item: { item: {
type: Object, type: Object,
@ -113,8 +112,7 @@ export default {
datum: undefined, datum: undefined,
formats: undefined, formats: undefined,
domainObject: undefined, domainObject: undefined,
currentObjectPath: undefined, currentObjectPath: undefined
objectStyle: ''
} }
}, },
computed: { computed: {
@ -127,15 +125,10 @@ export default {
return displayMode === 'all' || displayMode === 'value'; return displayMode === 'all' || displayMode === 'value';
}, },
styleObject() { styleObject() {
return { return Object.assign({}, {
backgroundColor: this.item.fill,
borderColor: this.item.stroke,
color: this.item.color,
fontSize: this.item.size fontSize: this.item.size
} }, this.itemStyle);
},
styleClass() {
return this.objectStyle && this.objectStyle.isStyleInvisible;
}, },
fieldName() { fieldName() {
return this.valueMetadata && this.valueMetadata.name; return this.valueMetadata && this.valueMetadata.name;
@ -190,15 +183,6 @@ export default {
this.removeSelectable(); this.removeSelectable();
} }
if (this.unlistenStyles) {
this.unlistenStyles();
}
if (this.styleRuleManager) {
this.styleRuleManager.destroy();
delete this.styleRuleManager;
}
this.openmct.time.off("bounds", this.refreshData); this.openmct.time.off("bounds", this.refreshData);
}, },
methods: { methods: {
@ -241,7 +225,6 @@ export default {
}, },
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.initObjectStyles();
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
@ -266,30 +249,6 @@ export default {
}, },
showContextMenu(event) { showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
},
initObjectStyles() {
if (this.domainObject.configuration) {
this.styleRuleManager = new StyleRuleManager(this.domainObject.configuration.objectStyles, this.openmct, this.updateStyle.bind(this));
if (this.unlistenStyles) {
this.unlistenStyles();
}
this.unlistenStyles = this.openmct.objects.observe(this.domainObject, 'configuration.objectStyles', (newObjectStyle) => {
//Updating object styles in the inspector view will trigger this so that the changes are reflected immediately
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
});
}
},
updateStyle(styleObj) {
let keys = Object.keys(styleObj);
keys.forEach(key => {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('transparent') > -1)) {
if (styleObj[key]) {
styleObj[key] = '';
}
}
});
this.objectStyle = styleObj;
} }
} }
} }

View File

@ -32,7 +32,7 @@
:class="[styleClass]" :class="[styleClass]"
:style="style" :style="style"
> >
{{ item.text }} <div class="c-text-view__text">{{ item.text }}</div>
</div> </div>
</layout-frame> </layout-frame>
</template> </template>
@ -44,8 +44,8 @@ import conditionalStylesMixin from "../mixins/objectStyles-mixin";
export default { export default {
makeDefinition(openmct, gridSize, element) { makeDefinition(openmct, gridSize, element) {
return { return {
fill: 'transparent', fill: '',
stroke: 'transparent', stroke: '',
size: '13px', size: '13px',
color: '', color: '',
x: 1, x: 1,
@ -80,14 +80,8 @@ export default {
computed: { computed: {
style() { style() {
return Object.assign({ return Object.assign({
backgroundColor: this.item.fill,
border: '1px solid ' + this.item.stroke,
color: this.item.color,
fontSize: this.item.size fontSize: this.item.size
}, this.itemStyle); }, this.itemStyle);
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
} }
}, },
watch: { watch: {

View File

@ -16,6 +16,8 @@
.is-editing { .is-editing {
/******************* STYLES FOR C-FRAME WHILE EDITING */ /******************* STYLES FOR C-FRAME WHILE EDITING */
.c-frame { .c-frame {
border: 1px solid rgba($editFrameColorHov, 0.3);
&:not([s-selected]) { &:not([s-selected]) {
&:hover { &:hover {
border: $editFrameBorderHov; border: $editFrameBorderHov;

View File

@ -1,6 +1,8 @@
.c-text-view { .c-text-view {
display: flex; display: flex;
align-items: stretch; align-items: center; // Vertically center text
overflow: hidden;
padding: $interiorMargin;
.c-frame & { .c-frame & {
@include abs(); @include abs();

View File

@ -21,18 +21,20 @@
*****************************************************************************/ *****************************************************************************/
import StyleRuleManager from "@/plugins/condition/StyleRuleManager"; import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
export default { export default {
inject: ['openmct'], inject: ['openmct'],
data() { data() {
return { return {
itemStyle: this.itemStyle itemStyle: undefined,
styleClass: ''
} }
}, },
mounted() { mounted() {
this.domainObject = this.$parent.domainObject; this.parentDomainObject = this.$parent.domainObject;
this.itemId = this.item.id; this.itemId = this.item.id;
this.objectStyle = this.getObjectStyleForItem(this.domainObject.configuration.objectStyles); this.objectStyle = this.getObjectStyleForItem(this.parentDomainObject.configuration.objectStyles);
this.initObjectStyles(); this.initObjectStyles();
}, },
destroyed() { destroyed() {
@ -50,7 +52,7 @@ export default {
}, },
initObjectStyles() { initObjectStyles() {
if (!this.styleRuleManager) { if (!this.styleRuleManager) {
this.styleRuleManager = new StyleRuleManager(this.objectStyle, this.openmct, this.updateStyle.bind(this)); this.styleRuleManager = new StyleRuleManager(this.objectStyle, this.openmct, this.updateStyle.bind(this), true);
} else { } else {
this.styleRuleManager.updateObjectStyleConfig(this.objectStyle); this.styleRuleManager.updateObjectStyleConfig(this.objectStyle);
} }
@ -59,7 +61,7 @@ export default {
this.stopListeningObjectStyles(); this.stopListeningObjectStyles();
} }
this.stopListeningObjectStyles = this.openmct.objects.observe(this.domainObject, 'configuration.objectStyles', (newObjectStyle) => { this.stopListeningObjectStyles = this.openmct.objects.observe(this.parentDomainObject, 'configuration.objectStyles', (newObjectStyle) => {
//Updating object styles in the inspector view will trigger this so that the changes are reflected immediately //Updating object styles in the inspector view will trigger this so that the changes are reflected immediately
let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle); let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle);
if (this.objectStyle !== newItemObjectStyle) { if (this.objectStyle !== newItemObjectStyle) {
@ -69,13 +71,8 @@ export default {
}); });
}, },
updateStyle(style) { updateStyle(style) {
this.itemStyle = style; this.itemStyle = getStylesWithoutNoneValue(style);
let keys = Object.keys(this.itemStyle); this.styleClass = this.itemStyle && this.itemStyle.isStyleInvisible;
keys.forEach((key) => {
if ((typeof this.itemStyle[key] === 'string') && (this.itemStyle[key].indexOf('transparent') > -1)) {
delete this.itemStyle[key];
}
});
} }
} }
}; };

View File

@ -0,0 +1,21 @@
<template>
<div class="c-menu">
<ul>
<li
v-for="(item, index) in popupMenuItems"
:key="index"
:class="item.cssClass"
:title="item.name"
@click="item.callback"
>
{{ item.name }}
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['popupMenuItems']
}
</script>

View File

@ -12,24 +12,7 @@
:class="embed.cssClass" :class="embed.cssClass"
@click="changeLocation" @click="changeLocation"
>{{ embed.name }}</a> >{{ embed.name }}</a>
<a class="c-ne__embed__context-available icon-arrow-down" <PopupMenu :popup-menu-items="popupMenuItems" />
@click="toggleActionMenu"
></a>
</div>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(embed)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div> </div>
<div v-if="embed.snapshot" <div v-if="embed.snapshot"
class="c-ne__embed__time" class="c-ne__embed__time"
@ -42,15 +25,17 @@
<script> <script>
import Moment from 'moment'; import Moment from 'moment';
import PopupMenu from './popup-menu.vue';
import PreviewAction from '../../../ui/preview/PreviewAction'; import PreviewAction from '../../../ui/preview/PreviewAction';
import Painterro from 'painterro'; import Painterro from 'painterro';
import RemoveDialog from '../utils/removeDialog';
import SnapshotTemplate from './snapshot-template.html'; import SnapshotTemplate from './snapshot-template.html';
import { togglePopupMenu } from '../utils/popup-menu';
import Vue from 'vue'; import Vue from 'vue';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: { components: {
PopupMenu
}, },
props: { props: {
embed: { embed: {
@ -62,23 +47,35 @@ export default {
removeActionString: { removeActionString: {
type: String, type: String,
default() { default() {
return 'Remove Embed'; return 'Remove This Embed';
} }
} }
}, },
data() { data() {
return { return {
actions: [this.removeEmbedAction()], popupMenuItems: []
agentService: this.openmct.$injector.get('agentService'),
popupService: this.openmct.$injector.get('popupService')
} }
}, },
watch: { watch: {
}, },
beforeMount() { mounted() {
this.populateActionMenu(); this.addPopupMenuItems();
}, },
methods: { methods: {
addPopupMenuItems() {
const removeEmbed = {
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
}
const preview = {
cssClass: 'icon-eye-open',
name: 'Preview',
callback: this.previewEmbed.bind(this)
}
this.popupMenuItems = [removeEmbed, preview];
},
annotateSnapshot() { annotateSnapshot() {
const self = this; const self = this;
@ -165,24 +162,44 @@ export default {
}).show(this.embed.snapshot.src); }).show(this.embed.snapshot.src);
}, },
changeLocation() { changeLocation() {
this.openmct.time.stopClock();
this.openmct.time.bounds({
start: this.embed.bounds.start,
end: this.embed.bounds.end
});
const link = this.embed.historicLink; const link = this.embed.historicLink;
if (!link) { if (!link) {
return; return;
} }
const bounds = this.openmct.time.bounds();
const isTimeBoundChanged = this.embed.bounds.start !== bounds.start
&& this.embed.bounds.end !== bounds.end;
const isFixedTimespanMode = !this.openmct.time.clock();
window.location.href = link; window.location.href = link;
const message = 'Time bounds changed to fixed timespan mode';
let message = '';
if (isTimeBoundChanged) {
this.openmct.time.bounds({
start: this.embed.bounds.start,
end: this.embed.bounds.end
});
message = 'Time bound values changed';
}
if (!isFixedTimespanMode) {
message = 'Time bound values changed to fixed timespan mode';
}
this.openmct.notifications.alert(message); this.openmct.notifications.alert(message);
}, },
formatTime(unixTime, timeFormat) { formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat); return Moment.utc(unixTime).format(timeFormat);
}, },
getRemoveDialog() {
const options = {
name: this.removeActionString,
callback: this.removeEmbed.bind(this)
}
const removeDialog = new RemoveDialog(this.openmct, options);
removeDialog.show();
},
openSnapshot() { openSnapshot() {
const self = this; const self = this;
const snapshot = new Vue({ const snapshot = new Vue({
@ -214,53 +231,17 @@ export default {
] ]
}); });
}, },
populateActionMenu() { previewEmbed() {
const self = this; const self = this;
const actions = [new PreviewAction(self.openmct)]; const previewAction = new PreviewAction(self.openmct);
previewAction.invoke(JSON.parse(self.embed.objectPath));
},
removeEmbed(success) {
if (!success) {
return;
}
actions.forEach((action) => { this.$emit('removeEmbed', this.embed.id);
self.actions.push({
cssClass: action.cssClass,
name: action.name,
perform: () => {
action.invoke(JSON.parse(self.embed.objectPath));
}
});
});
},
removeEmbed(id) {
this.$emit('removeEmbed', id);
},
removeEmbedAction() {
const self = this;
return {
name: self.removeActionString,
cssClass: 'icon-trash',
perform: function (embed) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: `This action will permanently ${self.removeActionString.toLowerCase()}. Do you wish to continue?`,
buttons: [{
label: "No",
callback: function () {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: function () {
dialog.dismiss();
self.removeEmbed(embed.id);
}
}]
});
}
};
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
}, },
updateEmbed(embed) { updateEmbed(embed) {
this.$emit('updateEmbed', embed); this.$emit('updateEmbed', embed);

View File

@ -221,7 +221,7 @@ export default {
return position; return position;
}, },
formatTime(unixTime, timeFormat) { formatTime(unixTime, timeFormat) {
return Moment(unixTime).format(timeFormat); return Moment.utc(unixTime).format(timeFormat);
}, },
moveSnapshot(snapshotId) { moveSnapshot(snapshotId) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId); const snapshot = this.snapshotContainer.getSnapshot(snapshotId);

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left"> <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button <button
class="c-button--menu icon-notebook" class="c-button--menu icon-notebook"
title="Switch view type" title="Take a Notebook Snapshot"
@click="setNotebookTypes" @click="setNotebookTypes"
@click.stop="toggleMenu" @click.stop="toggleMenu"
> >
@ -40,6 +40,18 @@ export default {
default() { default() {
return {}; return {};
} }
},
ignoreLink: {
type: Boolean,
default() {
return false;
}
},
objectPath: {
type: Array,
default() {
return null;
}
} }
}, },
data() { data() {
@ -97,17 +109,27 @@ export default {
this.showMenu = false; this.showMenu = false;
}, },
snapshot(notebook) { snapshot(notebook) {
let element = document.getElementsByClassName("l-shell__main-container")[0]; this.hideMenu();
this.$nextTick(() => {
const element = document.querySelector('.c-overlay__contents')
|| document.getElementsByClassName('l-shell__main-container')[0];
const bounds = this.openmct.time.bounds(); const bounds = this.openmct.time.bounds();
const objectPath = this.openmct.router.path; const link = !this.ignoreLink
? window.location.href
: null;
const objectPath = this.objectPath || this.openmct.router.path;
const snapshotMeta = { const snapshotMeta = {
bounds, bounds,
link: window.location.href, link,
objectPath, objectPath,
openmct: this.openmct openmct: this.openmct
}; };
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element); this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
});
} }
} }
} }

View File

@ -10,24 +10,9 @@
>&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }} >&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
</span> </span>
</div> </div>
<a class="l-browse-bar__context-actions c-disclosure-button" <PopupMenu v-if="snapshots.length > 0"
@click="toggleActionMenu" :popup-menu-items="popupMenuItems"
></a> />
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform()"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -62,14 +47,16 @@
<script> <script>
import NotebookEmbed from './notebook-embed.vue'; import NotebookEmbed from './notebook-embed.vue';
import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container'; import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants'; import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
import { togglePopupMenu } from '../utils/popup-menu';
export default { export default {
inject: ['openmct', 'snapshotContainer'], inject: ['openmct', 'snapshotContainer'],
components: { components: {
NotebookEmbed NotebookEmbed,
PopupMenu
}, },
props: { props: {
toggleSnapshot: { toggleSnapshot: {
@ -81,54 +68,47 @@ export default {
}, },
data() { data() {
return { return {
actions: [this.removeAllSnapshotAction()], popupMenuItems: [],
removeActionString: 'Delete all snapshots',
snapshots: [] snapshots: []
} }
}, },
mounted() { mounted() {
this.addPopupMenuItems();
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated); this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.snapshots = this.snapshotContainer.getSnapshots(); this.snapshots = this.snapshotContainer.getSnapshots();
}, },
beforeDestory() { beforeDestory() {
}, },
methods: { methods: {
addPopupMenuItems() {
const removeSnapshot = {
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
}
this.popupMenuItems = [removeSnapshot];
},
close() { close() {
this.toggleSnapshot(); this.toggleSnapshot();
}, },
getNotebookSnapshotMaxCount() { getNotebookSnapshotMaxCount() {
return NOTEBOOK_SNAPSHOT_MAX_COUNT; return NOTEBOOK_SNAPSHOT_MAX_COUNT;
}, },
removeAllSnapshotAction() { getRemoveDialog() {
const self = this; const options = {
name: this.removeActionString,
callback: this.removeAllSnapshots.bind(this)
}
const removeDialog = new RemoveDialog(this.openmct, options);
removeDialog.show();
},
removeAllSnapshots(success) {
if (!success) {
return;
}
return {
name: 'Delete All Snapshots',
cssClass: 'icon-trash',
perform: function (embed) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete all notebook snapshots. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.removeAllSnapshots();
dialog.dismiss();
}
}
]
});
}
};
},
removeAllSnapshots() {
this.snapshotContainer.removeAllSnapshots(); this.snapshotContainer.removeAllSnapshots();
}, },
removeSnapshot(id) { removeSnapshot(id) {
@ -141,9 +121,6 @@ export default {
event.dataTransfer.setData('text/plain', snapshot.id); event.dataTransfer.setData('text/plain', snapshot.id);
event.dataTransfer.setData('snapshot/id', snapshot.id); event.dataTransfer.setData('snapshot/id', snapshot.id);
}, },
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
updateSnapshot(snapshot) { updateSnapshot(snapshot) {
this.snapshotContainer.updateSnapshot(snapshot); this.snapshotContainer.updateSnapshot(snapshot);
} }

View File

@ -49,13 +49,19 @@
class="c-notebook__controls__time" class="c-notebook__controls__time"
> >
<option value="0" <option value="0"
selected="selected" :selected="showTime === 0"
> >
Show all Show all
</option> </option>
<option value="1">Last hour</option> <option value="1"
<option value="8">Last 8 hours</option> :selected="showTime === 1"
<option value="24">Last 24 hours</option> >Last hour</option>
<option value="8"
:selected="showTime === 8"
>Last 8 hours</option>
<option value="24"
:selected="showTime === 24"
>Last 24 hours</option>
</select> </select>
<select v-model="defaultSort" <select v-model="defaultSort"
class="c-notebook__controls__time" class="c-notebook__controls__time"
@ -132,9 +138,17 @@ export default {
}, },
computed: { computed: {
filteredAndSortedEntries() { filteredAndSortedEntries() {
const filterTime = Date.now();
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || []; const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
return pageEntries.sort(this.sortEntries); const hours = parseInt(this.showTime, 10);
const filteredPageEntriesByTime = hours
? pageEntries.filter(entry => (filterTime - entry.createdOn) <= hours * 60 * 60 * 1000)
: pageEntries;
return this.defaultSort === 'oldest'
? filteredPageEntriesByTime
: [...filteredPageEntriesByTime].reverse();
}, },
pages() { pages() {
return this.getPages() || []; return this.getPages() || [];
@ -420,11 +434,6 @@ export default {
searchItem(input) { searchItem(input) {
this.search = input; this.search = input;
}, },
sortEntries(right, left) {
return this.defaultSort === 'newest'
? left.createdOn - right.createdOn
: right.createdOn - left.createdOn;
},
toggleNav() { toggleNav() {
this.showNav = !this.showNav; this.showNav = !this.showNav;
}, },
@ -486,7 +495,7 @@ export default {
return; return;
} }
if (section.id !== defaultNotebookSection.id) { if (id !== defaultNotebookSection.id) {
return; return;
} }

View File

@ -9,32 +9,19 @@
@keydown.enter="updateName" @keydown.enter="updateName"
@blur="updateName" @blur="updateName"
>{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span> >{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down" <PopupMenu :popup-menu-items="popupMenuItems" />
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(page.id)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import { togglePopupMenu } from '../utils/popup-menu'; import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: {
PopupMenu
},
props: { props: {
defaultPageId: { defaultPageId: {
type: String, type: String,
@ -55,7 +42,8 @@ export default {
}, },
data() { data() {
return { return {
actions: [this.deletePage()] popupMenuItems: [],
removeActionString: `Delete ${this.pageTitle}`
} }
}, },
watch: { watch: {
@ -64,40 +52,37 @@ export default {
} }
}, },
mounted() { mounted() {
this.addPopupMenuItems();
this.toggleContentEditable(); this.toggleContentEditable();
}, },
destroyed() { destroyed() {
}, },
methods: { methods: {
deletePage() { addPopupMenuItems() {
const self = this; const removePage = {
return {
name: `Delete ${this.pageTitle}`,
cssClass: 'icon-trash', cssClass: 'icon-trash',
perform: function (id) { name: this.removeActionString,
const dialog = self.openmct.overlays.dialog({ callback: this.getRemoveDialog.bind(this)
iconClass: "error",
message: 'This action will delete this page and all of its entries. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
} }
this.popupMenuItems = [removePage];
}, },
{ deletePage(success) {
label: "Yes", if (!success) {
emphasis: true, return;
callback: () => {
self.$emit('deletePage', id);
dialog.dismiss();
} }
this.$emit('deletePage', this.page.id);
},
getRemoveDialog() {
const message = 'This action will delete this page and all of its entries. Do you want to continue?';
const options = {
name: this.removeActionString,
callback: this.deletePage.bind(this),
message
} }
] const removeDialog = new RemoveDialog(this.openmct, options);
}); removeDialog.show();
}
};
}, },
selectPage(event) { selectPage(event) {
const target = event.target; const target = event.target;
@ -117,10 +102,6 @@ export default {
this.$emit('selectPage', id); this.$emit('selectPage', id);
}, },
toggleActionMenu(event) {
event.preventDefault();
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(page = this.page) { toggleContentEditable(page = this.page) {
const pageTitle = this.$el.querySelector('span'); const pageTitle = this.$el.querySelector('span');
pageTitle.contentEditable = page.isSelected; pageTitle.contentEditable = page.isSelected;

View File

@ -0,0 +1,93 @@
<template>
<button
class="c-popup-menu-button c-disclosure-button"
title="popup menu"
@click="showMenuItems"
>
</button>
</template>
<script>
import MenuItems from './menu-items.vue';
import Vue from 'vue';
export default {
inject: ['openmct'],
props: {
domainObject: {
type: Object,
default() {
return {};
}
},
popupMenuItems: {
type: Array,
default() {
return [];
}
}
},
data() {
return {
menuItems: null
}
},
mounted() {
},
methods: {
calculateMenuPosition(event, element) {
let eventPosX = event.clientX;
let eventPosY = event.clientY;
let menuDimensions = element.getBoundingClientRect();
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: eventPosX,
y: eventPosY
}
},
hideMenuItems() {
document.body.removeChild(this.menuItems.$el);
this.menuItems.$destroy();
this.menuItems = null;
document.removeEventListener('click', this.hideMenuItems);
return;
},
showMenuItems($event) {
const menuItems = new Vue({
components: {
MenuItems
},
provide: {
popupMenuItems: this.popupMenuItems
},
template: '<MenuItems />'
});
this.menuItems = menuItems;
menuItems.$mount();
const element = this.menuItems.$el;
document.body.appendChild(element);
const position = this.calculateMenuPosition($event, element);
element.style.left = `${position.x}px`;
element.style.top = `${position.y}px`;
setTimeout(() => {
document.addEventListener('click', this.hideMenuItems);
}, 0);
}
}
}
</script>

View File

@ -9,24 +9,7 @@
@keydown.enter="updateName" @keydown.enter="updateName"
@blur="updateName" @blur="updateName"
>{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span> >{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down" <PopupMenu :popup-menu-items="popupMenuItems" />
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(section.id)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div> </div>
</template> </template>
@ -34,10 +17,14 @@
</style> </style>
<script> <script>
import { togglePopupMenu } from '../utils/popup-menu'; import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: {
PopupMenu
},
props: { props: {
defaultSectionId: { defaultSectionId: {
type: String, type: String,
@ -58,7 +45,8 @@ export default {
}, },
data() { data() {
return { return {
actions: [this.deleteSectionAction()] popupMenuItems: [],
removeActionString: `Delete ${this.sectionTitle}`
} }
}, },
watch: { watch: {
@ -67,40 +55,38 @@ export default {
} }
}, },
mounted() { mounted() {
this.addPopupMenuItems();
this.toggleContentEditable(); this.toggleContentEditable();
}, },
destroyed() { destroyed() {
}, },
methods: { methods: {
deleteSectionAction() { addPopupMenuItems() {
const self = this; const removeSection = {
return {
name: `Delete ${this.sectionTitle}`,
cssClass: 'icon-trash', cssClass: 'icon-trash',
perform: function (id) { name: this.removeActionString,
const dialog = self.openmct.overlays.dialog({ callback: this.getRemoveDialog.bind(this)
iconClass: "error",
message: 'This action will delete this section and all of its pages and entries. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
} }
this.popupMenuItems = [removeSection];
}, },
{ deleteSection(success) {
label: "Yes", if (!success) {
emphasis: true, return;
callback: () => {
self.$emit('deleteSection', id);
dialog.dismiss();
} }
this.$emit('deleteSection', this.section.id);
},
getRemoveDialog() {
const message = 'This action will delete this section and all of its pages and entries. Do you want to continue?';
const options = {
name: this.removeActionString,
callback: this.deleteSection.bind(this),
message
} }
]
}); const removeDialog = new RemoveDialog(this.openmct, options);
} removeDialog.show();
};
}, },
selectSection(event) { selectSection(event) {
const target = event.target; const target = event.target;
@ -121,9 +107,6 @@ export default {
this.$emit('selectSection', id); this.$emit('selectSection', id);
}, },
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(section = this.section) { toggleContentEditable(section = this.section) {
const sectionTitle = this.$el.querySelector('span'); const sectionTitle = this.$el.querySelector('span');
sectionTitle.contentEditable = section.isSelected; sectionTitle.contentEditable = section.isSelected;

View File

@ -1,45 +0,0 @@
import $ from 'zepto';
export const togglePopupMenu = (event, openmct) => {
event.preventDefault();
const body = $(document.body);
const container = $(event.target.parentElement.parentElement);
const classList = document.querySelector('body').classList;
const isPhone = Array.from(classList).includes('phone');
const isTablet = Array.from(classList).includes('tablet');
const initiatingEvent = isPhone || isTablet
? 'touchstart'
: 'mousedown';
const menu = container.find('.menu-element');
let dismissExistingMenu;
function dismiss() {
container.find('.hide-menu').append(menu);
body.off(initiatingEvent, menuClickHandler);
dismissExistingMenu = undefined;
}
function menuClickHandler(e) {
window.setTimeout(() => {
dismiss();
}, 100);
}
// Dismiss any menu which was already showing
if (dismissExistingMenu) {
dismissExistingMenu();
}
// ...and record the presence of this menu.
dismissExistingMenu = dismiss;
const popupService = openmct.$injector.get('popupService');
popupService.display(menu, [event.pageX,event.pageY], {
marginX: 0,
marginY: -50
});
body.on(initiatingEvent, menuClickHandler);
}

View File

@ -0,0 +1,36 @@
export default class RemoveDialog {
constructor(openmct, options) {
this.name = options.name;
this.openmct = openmct;
this.callback = options.callback;
this.cssClass = options.cssClass || 'icon-trash';
this.description = options.description || 'Remove action dialog';
this.iconClass = "error";
this.key = 'remove';
this.message = options.message || `This action will permanently ${this.name.toLowerCase()}. Do you wish to continue?`;
}
show() {
const dialog = this.openmct.overlays.dialog({
iconClass: this.iconClass,
message: this.message,
buttons: [
{
label: "Ok",
callback: () => {
this.callback(true);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: () => {
this.callback(false);
dialog.dismiss();
}
}
]
});
}
}

View File

@ -21,7 +21,7 @@
--> -->
<div ng-controller="StackedPlotController as stackedPlot" <div ng-controller="StackedPlotController as stackedPlot"
class="c-plot c-plot--stacked holder holder-plot has-control-bar"> class="c-plot c-plot--stacked holder holder-plot has-control-bar">
<div class="l-control-bar" ng-show="!stackedPlot.hideExportButtons"> <div class="c-control-bar" ng-show="!stackedPlot.hideExportButtons">
<span class="c-button-set c-button-set--strip-h"> <span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download" <button class="c-button icon-download"
ng-click="stackedPlot.exportPNG()" ng-click="stackedPlot.exportPNG()"

View File

@ -54,6 +54,7 @@ function (
* @constructor * @constructor
*/ */
function MCTChartController($scope) { function MCTChartController($scope) {
this.$onInit = () => {
this.$scope = $scope; this.$scope = $scope;
this.isDestroyed = false; this.isDestroyed = false;
this.lines = []; this.lines = [];
@ -76,6 +77,7 @@ function (
this.$scope.$watch('rectangles', this.scheduleDraw); this.$scope.$watch('rectangles', this.scheduleDraw);
this.config.series.forEach(this.onSeriesAdd, this); this.config.series.forEach(this.onSeriesAdd, this);
} }
}
eventHelpers.extend(MCTChartController.prototype); eventHelpers.extend(MCTChartController.prototype);

View File

@ -34,6 +34,7 @@ define([
* values near the cursor. * values near the cursor.
*/ */
function MCTPlotController($scope, $element, $window) { function MCTPlotController($scope, $element, $window) {
this.$onInit = () => {
this.$scope = $scope; this.$scope = $scope;
this.$scope.config = this.config; this.$scope.config = this.config;
this.$scope.plot = this; this.$scope.plot = this;
@ -54,6 +55,7 @@ define([
this.initialize(); this.initialize();
} }
}
MCTPlotController.$inject = ['$scope', '$element', '$window']; MCTPlotController.$inject = ['$scope', '$element', '$window'];

View File

@ -114,6 +114,7 @@ define([
} }
function MCTTicksController($scope, $element) { function MCTTicksController($scope, $element) {
this.$onInit = () => {
this.$scope = $scope; this.$scope = $scope;
this.$element = $element; this.$element = $element;
@ -124,6 +125,7 @@ define([
this.listenTo(this.$scope, '$destroy', this.stopListening, this); this.listenTo(this.$scope, '$destroy', this.stopListening, this);
this.updateTicks(); this.updateTicks();
} }
}
MCTTicksController.$inject = ['$scope', '$element']; MCTTicksController.$inject = ['$scope', '$element'];

View File

@ -81,7 +81,8 @@ define(
clonedElement.classList.add(className); clonedElement.classList.add(className);
} }
element.id = oldId; element.id = oldId;
} },
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) { }).then(function (canvas) {
dialog.dismiss(); dialog.dismiss();
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {

View File

@ -0,0 +1,35 @@
export default class PlotlyTelemetryProvider {
constructor(openmct) {
this.openmct = openmct;
}
isTelemetryObject(domainObject) {
return domainObject.type === 'plotlyPlot';
}
supportsRequest(domainObject) {
return domainObject.type === 'plotlyPlot';
}
supportsSubscribe(domainObject) {
return domainObject.type === 'plotlyPlot';
}
request(domainObject) {
// let conditionManager = this.getConditionManager(domainObject);
// return conditionManager.requestLADConditionSetOutput()
// .then(latestOutput => {
// return latestOutput;
// });
}
subscribe(domainObject, callback) {
// let conditionManager = this.getConditionManager(domainObject);
// conditionManager.on('conditionSetResultUpdated', callback);
// return this.destroyConditionManager.bind(this, this.openmct.objects.makeKeyString(domainObject.identifier));
}
}

View File

@ -0,0 +1,73 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2019, 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 PlotlyViewLayout from './components/PlotlyViewLayout.vue';
import Vue from 'vue';
export default function PlotlyViewProvider(openmct) {
return {
key: 'plotlyPlot',
name: 'Plotly Plot',
cssClass: 'icon-plot-overlay',
canView: function (domainObject) {
return domainObject.type === 'plotlyPlot';
},
canEdit: function (domainObject) {
return domainObject.type === 'plotlyPlot';
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element, isEditing) {
component = new Vue({
provide: {
openmct,
domainObject,
objectPath
},
el: element,
components: {
PlotlyViewLayout
},
data() {
return {
isEditing
}
},
template: '<plotly-view-layout :isEditing="isEditing"></plotly-view-layout>'
});
},
onEditModeChange: function (isEditing) {
component.isEditing = isEditing;
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}

View File

@ -0,0 +1,138 @@
<template>
<div class="l-view-section"></div>
</template>
<script>
import Plotly from 'plotly.js-dist';
import moment from 'moment'
export default {
inject: ['openmct', 'domainObject', 'objectPath'],
data: function () {
return {
telemetryObjects: []
// currentDomainObject: this.domainObject
}
},
mounted() {
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addTelemetry);
this.composition.load();
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
console.log('this.metadata', this.metadata);
// this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);
// this.subscribe(this.domainObject);
this.plotElement = document.querySelector('.l-view-section');
// Plotly.newPlot(this.plotElement, [{
// x: [1, 2, 3, 4, 5],
// y: [1, 2, 4, 8, 16]
// }], this.getLayout(), {displayModeBar: false});
},
methods: {
getLayout() {
return {
hovermode: 'compare',
hoverdistance: -1,
autosize: "true",
showlegend: false,
font: {
family: "'Helvetica Neue', Helvetica, Arial, sans-serif",
size: "12px",
color: "#666"
},
xaxis: {
// title: this.plotAxisTitle.xAxisTitle,
zeroline: false
},
yaxis: {
// title: this.plotAxisTitle.yAxisTitle,
zeroline: false
},
margin: {
l: 20,
r: 10,
b: 20,
t: 10
},
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent'
}
},
addTelemetry(telemetryObject) {
return this.openmct.telemetry.request(telemetryObject)
.then(telemetryData => {
this.createPlot(telemetryData, telemetryObject);
}, () => {console.log(error)});
},
formatDatumX(datum) {
let timestamp = moment.utc(datum.utc).format('YYYY-MM-DD hh:mm:ss.ms');
return timestamp;
},
formatDatumY(datum) {
return datum.sin;
},
createPlot(telemetryData, telemetryObject) {
let x = [],
y = [];
telemetryData.forEach((datum, index) => {
x.push(this.formatDatumX(datum));
y.push(this.formatDatumY(datum));
})
let data = [{
x,
y,
mode: 'line'
}];
var layout = {
title:'Line and Scatter Plot'
};
Plotly.newPlot(
this.plotElement,
data,
this.getLayout()
)
this.subscribe(telemetryObject);
},
subscribe(domainObject) {
// this.date = ''
// this.openmct.objects.get(this.keystring)
// .then((object) => {
// const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
// console.log('metadata', metadata);
// // 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);
// // });
// // this.requestHistory(this.openmct.time.bounds());
// });
this.openmct.telemetry.subscribe(domainObject, (datum) => {
this.updateData(datum)
})
},
updateData(datum) {
Plotly.extendTraces(
this.plotElement,
{
x: [[this.formatDatumX(datum)]],
y: [[this.formatDatumY(datum)]]
},
[0]
);
}
}
}
</script>

View File

@ -0,0 +1,2 @@
.plot svg {
}

View File

@ -0,0 +1,20 @@
import PlotlyViewProvider from './PlotlyViewProvider.js';
import PlotlyTelemetryProvider from './PlotlyTelemetryProvider.js';
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new PlotlyViewProvider(openmct));
openmct.telemetry.addProvider(new PlotlyTelemetryProvider(openmct));
openmct.types.addType('plotlyPlot', {
name: "Plotly Plot",
description: "Simple plot rendered by plotly.js",
creatable: true,
cssClass: 'icon-plot-overlay',
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.telemetry = {};
}
});
};
}

View File

@ -34,6 +34,7 @@ define([
'./URLIndicatorPlugin/URLIndicatorPlugin', './URLIndicatorPlugin/URLIndicatorPlugin',
'./telemetryMean/plugin', './telemetryMean/plugin',
'./plot/plugin', './plot/plugin',
'./plotlyPlot/plugin',
'./telemetryTable/plugin', './telemetryTable/plugin',
'./staticRootPlugin/plugin', './staticRootPlugin/plugin',
'./notebook/plugin', './notebook/plugin',
@ -66,6 +67,7 @@ define([
URLIndicatorPlugin, URLIndicatorPlugin,
TelemetryMean, TelemetryMean,
PlotPlugin, PlotPlugin,
PlotlyPlotPlugin,
TelemetryTablePlugin, TelemetryTablePlugin,
StaticRootPlugin, StaticRootPlugin,
Notebook, Notebook,
@ -88,7 +90,8 @@ define([
var bundleMap = { var bundleMap = {
LocalStorage: 'platform/persistence/local', LocalStorage: 'platform/persistence/local',
MyItems: 'platform/features/my-items', MyItems: 'platform/features/my-items',
CouchDB: 'platform/persistence/couch' CouchDB: 'platform/persistence/couch',
Elasticsearch: 'platform/persistence/elastic'
}; };
var plugins = _.mapValues(bundleMap, function (bundleName, pluginName) { var plugins = _.mapValues(bundleMap, function (bundleName, pluginName) {
@ -170,8 +173,8 @@ define([
plugins.ExampleImagery = ExampleImagery; plugins.ExampleImagery = ExampleImagery;
plugins.ImageryPlugin = ImageryPlugin; plugins.ImageryPlugin = ImageryPlugin;
plugins.Plot = PlotPlugin; plugins.Plot = PlotPlugin;
plugins.PlotlyPlot = PlotlyPlotPlugin.default;
plugins.TelemetryTable = TelemetryTablePlugin; plugins.TelemetryTable = TelemetryTablePlugin;
plugins.SummaryWidget = SummaryWidget; plugins.SummaryWidget = SummaryWidget;
plugins.TelemetryMean = TelemetryMean; plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin; plugins.URLIndicator = URLIndicatorPlugin;

View File

@ -165,7 +165,16 @@
/******************************* LEGACY */ /******************************* LEGACY */
.s-status-taking-snapshot, .s-status-taking-snapshot,
.overlay.snapshot { .overlay.snapshot {
// Handle overflow-y issues with tables and html2canvas .c-table {
// Replaces .l-sticky-headers .l-tabular-body { overflow: auto; } &__body-w {
.c-table__body-w { overflow: auto; } overflow: auto; // Handle overflow-y issues with tables and html2canvas
}
&-control-bar {
display: none;
+ * {
margin-top: 0 !important;
}
}
}
} }

View File

@ -13,7 +13,6 @@
@import "~styles/controls"; @import "~styles/controls";
@import "~styles/forms"; @import "~styles/forms";
@import "~styles/table"; @import "~styles/table";
@import "~styles/layout";
@import "~styles/legacy"; @import "~styles/legacy";
@import "~styles/legacy-plots"; @import "~styles/legacy-plots";
@import "~styles/plotly"; @import "~styles/plotly";

View File

@ -13,7 +13,6 @@
@import "~styles/controls"; @import "~styles/controls";
@import "~styles/forms"; @import "~styles/forms";
@import "~styles/table"; @import "~styles/table";
@import "~styles/layout";
@import "~styles/legacy"; @import "~styles/legacy";
@import "~styles/legacy-plots"; @import "~styles/legacy-plots";
@import "~styles/plotly"; @import "~styles/plotly";

View File

@ -13,7 +13,6 @@
@import "~styles/controls"; @import "~styles/controls";
@import "~styles/forms"; @import "~styles/forms";
@import "~styles/table"; @import "~styles/table";
@import "~styles/layout";
@import "~styles/legacy"; @import "~styles/legacy";
@import "~styles/legacy-plots"; @import "~styles/legacy-plots";
@import "~styles/plotly"; @import "~styles/plotly";

View File

@ -122,13 +122,8 @@ button {
margin-left: $interiorMargin; margin-left: $interiorMargin;
} }
$c1: nth($mixedSettingBg, 1);
$c2: nth($mixedSettingBg, 2);
$mixedBgD: $mixedSettingBgSize $mixedSettingBgSize;
&--mixed { &--mixed {
// E.g. click-icons in toolbar that apply to multiple selected items with different settings @include mixedBg();
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
} }
&--swatched { &--swatched {
@ -151,13 +146,6 @@ button {
flex: 1 1 auto; flex: 1 1 auto;
font-size: 1.1em; font-size: 1.1em;
} }
&--mixed {
// Styling for swatched buttons when settings are mixed
> [class*='swatch'] {
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
}
}
} }
} }
@ -244,18 +232,10 @@ button {
/******************************************************** SECTION */ /******************************************************** SECTION */
section { section {
flex: 0 0 auto; flex: 0 1 auto;
overflow: hidden; overflow: hidden;
+ section { + section {
margin-top: $interiorMargin; margin-top: $interiorMargin;
&.is-expanded {
margin-bottom: $interiorMargin * 3;
}
}
&.is-expanded {
flex: 0 1 auto;
} }
.c-section__header { .c-section__header {
@ -585,6 +565,20 @@ select {
} }
} }
} }
/******************************************************** CONTROL BARS */
.c-control-bar {
display: flex;
align-items: center;
> * + * {
margin-left: $interiorMarginSm;
}
&__label {
display: inline-block;
white-space: nowrap;
}
}
/******************************************************** PALETTES */ /******************************************************** PALETTES */
.c-palette { .c-palette {
@ -829,6 +823,10 @@ select {
box-shadow: rgba($colorBodyFg, 0.4) 0 0 3px; box-shadow: rgba($colorBodyFg, 0.4) 0 0 3px;
flex: 0 0 auto; flex: 0 0 auto;
padding: $interiorMargin $interiorMarginLg; padding: $interiorMargin $interiorMarginLg;
&--mixed {
@include mixedBg();
}
} }
/******************************************************** SLIDERS */ /******************************************************** SLIDERS */

View File

@ -88,6 +88,12 @@ body.desktop {
background: $scrollbarThumbColorMenuHov; background: $scrollbarThumbColorMenuHov;
} }
} }
div, span {
// Firefox
scrollbar-color: $scrollbarThumbColor $scrollbarTrackColorBg;
scrollbar-width: thin;
}
} }
/******************************************************** HTML ENTITIES */ /******************************************************** HTML ENTITIES */

View File

@ -1,87 +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.
*****************************************************************************/
/************************** BROWSE BAR */
.l-browse-bar {
display: flex;
align-items: center;
justify-content: space-between;
[class*="__"] {
// Removes extraneous horizontal white space
display: inline-flex;
}
&__start {
display: flex;
align-items: center;
flex: 1 1 auto;
margin-right: $interiorMargin;
min-width: 0; // Forces interior to compress when pushed on
}
&__end {
display: flex;
align-items: center;
flex: 0 0 auto;
[class*="__"] + [class*="__"] {
margin-left: $interiorMarginSm;
}
}
&__nav-to-parent-button,
&__disclosure-button {
flex: 0 0 auto;
}
&__nav-to-parent-button {
// This is an icon-button
$p: $interiorMargin;
margin-right: $interiorMargin;
padding-left: $p;
padding-right: $p;
.is-editing & {
display: none;
}
}
&__object-name--w {
flex: 0 1 auto;
@include headerFont(1.4em);
min-width: 0;
&:before {
// Icon
margin-right: $interiorMargin;
}
}
&__object-name {
flex: 0 1 auto;
}
&__object-details {
opacity: 0.5;
}
}

View File

@ -40,17 +40,51 @@ mct-plot {
} }
} }
} }
.c-plot,
.gl-plot {
.s-status-taking-snapshot & {
.c-control-bar {
display: none;
}
.gl-plot-y-label__select {
display: none;
}
}
}
.c-plot { .c-plot {
$p: $mainViewPad; //$p: $mainViewPad;
position: absolute; @include abs($mainViewPad);
top: $p; right: $p; bottom: $p; left: $p; //position: absolute;
//top: $p; right: $p; bottom: $p; left: $p;
display: flex;
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
}
.l-control-bar {
flex: 0 0 auto;
}
.l-view-section {
display: flex;
flex: 1 1 auto;
flex-direction: column;
}
&--stacked { &--stacked {
.l-view-section { .child-frame {
// Make this a flex container .has-control-bar {
display: flex; .c-control-bar {
flex-flow: column nowrap; // Hides buttons per plot element in a stacked plot
.gl-plot.child-frame { display: none;
}
}
mct-plot { mct-plot {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
@ -58,10 +92,6 @@ mct-plot {
position: relative; position: relative;
} }
flex: 1 1 auto; flex: 1 1 auto;
&:not(:first-child) {
margin-top: $interiorMargin;
}
}
} }
.s-status-timeconductor-unsynced .holder-plot { .s-status-timeconductor-unsynced .holder-plot {
@ -70,7 +100,6 @@ mct-plot {
} }
} }
} }
} }
@ -186,7 +215,7 @@ mct-plot {
left: 0; top: 0; right: auto; bottom: 0; left: 0; top: 0; right: auto; bottom: 0;
padding-left: 5px; padding-left: 5px;
text-orientation: mixed; text-orientation: mixed;
overflow: hidden; //overflow: hidden;
writing-mode: vertical-lr; writing-mode: vertical-lr;
&:before { &:before {
// Icon denoting configurability // Icon denoting configurability
@ -339,11 +368,11 @@ mct-plot {
z-index: -10; z-index: -10;
.l-view-section { .l-view-section {
$m: $interiorMargin; //$m: $interiorMargin;
top: $m !important; //top: $m !important;
right: $m; //right: $m;
bottom: $m; //bottom: $m;
left: $m; //left: $m;
.s-status-timeconductor-unsynced .holder-plot { .s-status-timeconductor-unsynced .holder-plot {
.t-object-alert.t-alert-unsynced { .t-object-alert.t-alert-unsynced {

View File

@ -19,59 +19,13 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/******************************************************************* VIEWS */
// From _views.scss
// Legacy overlay and stacked plots depend on this for now
// Styles for sub-dividing views generically
.l-control-bar {
// Element that can be placed above l-view-section, holds controls, buttons, etc.
height: $controlBarH;
}
.c-control-bar {
display: flex;
align-items: center;
> * + * {
margin-left: $interiorMarginSm;
}
&__label {
display: inline-block;
white-space: nowrap;
}
}
.l-view-section {
@include abs();
overflow: auto;
}
.has-control-bar {
.l-view-section {
top: $controlBarH + $interiorMargin;
}
}
.child-frame {
.has-control-bar {
.l-control-bar,
.c-control-bar {
// Hides buttons per plot element in a stacked plot
display: none;
}
.l-view-section {
top: 0;
}
}
}
/*********************************************************************** CLOCKS AND TIMERS */ /*********************************************************************** CLOCKS AND TIMERS */
.c-clock, .c-clock,
.c-timer { .c-timer {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 1.25em; font-size: 1.25em;
overflow: hidden;
> * { > * {
flex: 0 0 auto; flex: 0 0 auto;
@ -82,6 +36,12 @@
&__value { &__value {
color: $colorBodyFgEm; color: $colorBodyFgEm;
} }
.c-frame & {
// When in a Display or Flexible Layout
@include abs();
padding: $interiorMargin;
}
} }
.c-clock { .c-clock {

View File

@ -50,6 +50,14 @@
} }
/************************** EFFECTS */ /************************** EFFECTS */
@mixin mixedBg() {
$c1: nth($mixedSettingBg, 1);
$c2: nth($mixedSettingBg, 2);
$mixedBgD: $mixedSettingBgSize $mixedSettingBgSize;
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
}
@mixin pulse($animName: pulse, $dur: 500ms, $iteration: infinite, $opacity0: 0.5, $opacity100: 1) { @mixin pulse($animName: pulse, $dur: 500ms, $iteration: infinite, $opacity0: 0.5, $opacity100: 1) {
@keyframes #{$animName} { @keyframes #{$animName} {
0% { opacity: $opacity0; } 0% { opacity: $opacity0; }

View File

@ -388,7 +388,21 @@
.s-status-taking-snapshot, .s-status-taking-snapshot,
.overlay.snapshot { .overlay.snapshot {
// Handle overflow-y issues with tables and html2canvas // Handle overflow-y issues with tables and html2canvas
background: $colorBodyBg; // Prevent html2canvas from using white background
color: $colorBodyFg;
padding: $interiorMarginSm !important; // Prevents items from going right to the edge of the image
.l-sticky-headers .l-tabular-body { overflow: auto; } .l-sticky-headers .l-tabular-body { overflow: auto; }
.l-browse-bar {
display: none; // Suppress browse-bar when snapshotting from view-large overlay
+ * {
margin-top: 0 !important; // Remove margin from any following elements
}
}
* {
box-shadow: none !important; // Prevent html2canvas problems with box-shadow
}
} }
.c-notebook-snapshot { .c-notebook-snapshot {

View File

@ -1,7 +1,6 @@
@import "../api/overlays/components/dialog-component.scss"; @import "../api/overlays/components/dialog-component.scss";
@import "../api/overlays/components/overlay-component.scss"; @import "../api/overlays/components/overlay-component.scss";
@import "../plugins/condition/components/condition.scss"; @import "../plugins/condition/components/conditionals.scss";
@import "../plugins/condition/components/condition-set.scss";
@import "../plugins/conditionWidget/components/condition-widget.scss"; @import "../plugins/conditionWidget/components/condition-widget.scss";
@import "../plugins/condition/components/inspector/conditional-styles.scss"; @import "../plugins/condition/components/inspector/conditional-styles.scss";
@import "../plugins/displayLayout/components/box-view.scss"; @import "../plugins/displayLayout/components/box-view.scss";
@ -19,6 +18,7 @@
@import "../plugins/folderView/components/list-item.scss"; @import "../plugins/folderView/components/list-item.scss";
@import "../plugins/folderView/components/list-view.scss"; @import "../plugins/folderView/components/list-view.scss";
@import "../plugins/imagery/components/imagery-view-layout.scss"; @import "../plugins/imagery/components/imagery-view-layout.scss";
@import "../plugins/plotlyPlot/components/plotly.scss";
@import "../plugins/telemetryTable/components/table-row.scss"; @import "../plugins/telemetryTable/components/table-row.scss";
@import "../plugins/telemetryTable/components/telemetry-filter-indicator.scss"; @import "../plugins/telemetryTable/components/telemetry-filter-indicator.scss";
@import "../plugins/tabs/components/tabs.scss"; @import "../plugins/tabs/components/tabs.scss";

View File

@ -59,6 +59,8 @@
<script> <script>
import ObjectView from './ObjectView.vue' import ObjectView from './ObjectView.vue'
import ContextMenuDropDown from './contextMenuDropDown.vue'; import ContextMenuDropDown from './contextMenuDropDown.vue';
import PreviewHeader from '@/ui/preview/preview-header.vue';
import Vue from 'vue';
const SIMPLE_CONTENT_TYPES = [ const SIMPLE_CONTENT_TYPES = [
'clock', 'clock',
@ -116,13 +118,41 @@ export default {
childElement = parentElement.children[0]; childElement = parentElement.children[0];
this.openmct.overlays.overlay({ this.openmct.overlays.overlay({
element: childElement, element: this.getOverlayElement(childElement),
size: 'large', size: 'large',
onDestroy() { onDestroy() {
parentElement.append(childElement); parentElement.append(childElement);
} }
}); });
}, },
getOverlayElement(childElement) {
const fragment = new DocumentFragment();
const header = this.getPreviewHeader();
fragment.append(header);
fragment.append(childElement);
return fragment;
},
getPreviewHeader() {
const domainObject = this.objectPath[0];
const preview = new Vue({
components: {
PreviewHeader
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath
},
data() {
return {
domainObject
}
},
template: '<PreviewHeader :domainObject="domainObject" :hideViewSwitcher="true" :showNotebookMenuSwitcher="true"></PreviewHeader>'
});
return preview.$mount().$el;
},
getSelectionContext() { getSelectionContext() {
return this.$refs.objectView.getSelectionContext(); return this.$refs.objectView.getSelectionContext();
} }

View File

@ -104,7 +104,7 @@ export default {
keys.forEach(key => { keys.forEach(key => {
let firstChild = this.$el.querySelector(':first-child'); let firstChild = this.$el.querySelector(':first-child');
if (firstChild) { if (firstChild) {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('transparent') > -1)) { if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
if (firstChild.style[key]) { if (firstChild.style[key]) {
firstChild.style[key] = ''; firstChild.style[key] = '';
} }
@ -201,7 +201,7 @@ export default {
}, },
initObjectStyles() { initObjectStyles() {
if (!this.styleRuleManager) { if (!this.styleRuleManager) {
this.styleRuleManager = new StyleRuleManager((this.currentObject.configuration && this.currentObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this)); this.styleRuleManager = new StyleRuleManager((this.currentObject.configuration && this.currentObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this), true);
} else { } else {
this.styleRuleManager.updateObjectStyleConfig(this.currentObject.configuration && this.currentObject.configuration.objectStyles); this.styleRuleManager.updateObjectStyleConfig(this.currentObject.configuration && this.currentObject.configuration.objectStyles);
} }

View File

@ -23,9 +23,12 @@
} }
&:not(.c-so-view--no-frame) { &:not(.c-so-view--no-frame) {
background: $colorBodyBg;
border: $browseFrameBorder; border: $browseFrameBorder;
padding: $interiorMargin; padding: $interiorMargin;
.is-editing & {
background: rgba($colorBodyBg, 0.8);
}
} }
&--no-frame { &--no-frame {

View File

@ -81,7 +81,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.excludeObjectTypes = ['folder', 'webPage', 'conditionSet']; this.excludeObjectTypes = ['folder', 'webPage', 'conditionSet', 'summary-widget', 'hyperlink'];
this.openmct.selection.on('change', this.updateInspectorViews); this.openmct.selection.on('change', this.updateInspectorViews);
}, },
destroyed() { destroyed() {
@ -108,7 +108,7 @@ export default {
let object = selection[0][0].context.item; let object = selection[0][0].context.item;
if (object) { if (object) {
let type = this.openmct.types.get(object.type); let type = this.openmct.types.get(object.type);
this.showStyles = (this.excludeObjectTypes.indexOf(object.type) < 0) && type.definition.creatable; this.showStyles = this.isLayoutObject(selection[0], object.type) || this.isCreatableObject(object, type);
} }
if (!this.currentTabbedView.key || (!this.showStyles && this.currentTabbedView.key === this.tabbedViews[1].key)) if (!this.currentTabbedView.key || (!this.showStyles && this.currentTabbedView.key === this.tabbedViews[1].key))
{ {
@ -116,6 +116,14 @@ export default {
} }
} }
}, },
isLayoutObject(selection, objectType) {
//we allow conditionSets to be styled if they're part of a layout
return selection.length > 1 &&
((objectType === 'conditionSet') || (this.excludeObjectTypes.indexOf(objectType) < 0));
},
isCreatableObject(object, type) {
return (this.excludeObjectTypes.indexOf(object.type) < 0) && type.definition.creatable;
},
updateCurrentTab(view) { updateCurrentTab(view) {
this.currentTabbedView = view; this.currentTabbedView = view;
}, },

View File

@ -26,8 +26,8 @@
<script> <script>
import ConditionalStylesView from '../../plugins/condition/components/inspector/ConditionalStylesView.vue'; import ConditionalStylesView from '../../plugins/condition/components/inspector/ConditionalStylesView.vue';
import MultiSelectStylesView from '../../plugins/condition/components/inspector/MultiSelectStylesView.vue';
import Vue from 'vue'; import Vue from 'vue';
import { getStyleProp } from "../../plugins/condition/utils/styleUtils";
export default { export default {
inject: ['openmct'], inject: ['openmct'],
@ -44,35 +44,9 @@ export default {
this.openmct.selection.off('change', this.updateSelection); this.openmct.selection.off('change', this.updateSelection);
}, },
methods: { methods: {
getStyleProperties(item) {
let styleProps = {};
Object.keys(item).forEach((key) => {
Object.assign(styleProps, getStyleProp(key, item[key]));
});
return styleProps;
},
updateSelection(selection) { updateSelection(selection) {
if (selection.length > 0 && selection[0].length > 0) { if (selection.length > 0 && selection[0].length > 0) {
let isChildItem = false; let template = selection.length > 1 ? '<multi-select-styles-view></multi-select-styles-view>' : '<conditional-styles-view></conditional-styles-view>';
let domainObject = selection[0][0].context.item;
let layoutItem = {};
let styleProps = this.getStyleProperties({
fill: 'transparent',
stroke: 'transparent',
color: 'transparent'
});
if (selection[0].length > 1) {
isChildItem = true;
//If there are more than 1 items in the selection[0] list, the first one could either be a sub domain object OR a layout drawing control.
//The second item in the selection[0] list is the container object (usually a layout)
if (!domainObject) {
styleProps = {};
layoutItem = selection[0][0].context.layoutItem;
styleProps = this.getStyleProperties(layoutItem);
domainObject = selection[0][1].context.item;
}
}
if (this.component) { if (this.component) {
this.component.$destroy(); this.component.$destroy();
this.component = undefined; this.component = undefined;
@ -83,20 +57,14 @@ export default {
this.component = new Vue({ this.component = new Vue({
provide: { provide: {
openmct: this.openmct, openmct: this.openmct,
domainObject: domainObject selection: selection
}, },
el: viewContainer, el: viewContainer,
components: { components: {
ConditionalStylesView ConditionalStylesView,
MultiSelectStylesView
}, },
data() { template: template
return {
layoutItem,
styleProps,
isChildItem
}
},
template: '<conditional-styles-view :can-hide="isChildItem" :item-id="layoutItem.id" :initial-styles="styleProps"></conditional-styles-view>'
}); });
} }
} }

View File

@ -28,6 +28,7 @@
<div class="l-browse-bar__end"> <div class="l-browse-bar__end">
<view-switcher <view-switcher
v-if="!isEditing"
:current-view="currentView" :current-view="currentView"
:views="views" :views="views"
@setView="setView" @setView="setView"
@ -35,6 +36,7 @@
<!-- Action buttons --> <!-- Action buttons -->
<NotebookMenuSwitcher v-if="notebookEnabled" <NotebookMenuSwitcher v-if="notebookEnabled"
:domain-object="domainObject" :domain-object="domainObject"
:object-path="openmct.router.path"
class="c-notebook-snapshot-menubutton" class="c-notebook-snapshot-menubutton"
/> />
<div class="l-browse-bar__actions"> <div class="l-browse-bar__actions">
@ -197,8 +199,6 @@ export default {
updateName(event) { updateName(event) {
if (event.target.innerText !== this.domainObject.name && event.target.innerText.match(/\S/)) { if (event.target.innerText !== this.domainObject.name && event.target.innerText.match(/\S/)) {
this.openmct.objects.mutate(this.domainObject, 'name', event.target.innerText); this.openmct.objects.mutate(this.domainObject, 'name', event.target.innerText);
} else {
event.target.innerText = this.domainObject.name;
} }
}, },
updateNameOnEnterKeyPress(event) { updateNameOnEnterKeyPress(event) {

View File

@ -6,7 +6,7 @@
<button <button
class="c-button--menu" class="c-button--menu"
:class="currentView.cssClass" :class="currentView.cssClass"
title="Switch view type" title="Change the current view"
@click.stop="toggleViewMenu" @click.stop="toggleViewMenu"
> >
<span class="c-button__label"> <span class="c-button__label">

View File

@ -1,3 +1,24 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/******************************* SHELL */ /******************************* SHELL */
.l-shell { .l-shell {
position: absolute; position: absolute;
@ -126,6 +147,9 @@
body.mobile & .l-shell__main-view-browse-bar { body.mobile & .l-shell__main-view-browse-bar {
margin-left: $mobileMenuIconD; // Make room for the hamburger! margin-left: $mobileMenuIconD; // Make room for the hamburger!
.c-button[class*='__actions__edit'] {
display: none; // Hide the main view edit button when in mobile context
}
} }
&__head { &__head {
@ -269,6 +293,79 @@
} }
} }
/************************** BROWSE BAR */
.l-browse-bar {
display: flex;
align-items: center;
justify-content: space-between;
[class*="__"] {
// Removes extraneous horizontal white space
display: inline-flex;
}
&__start,
&__end,
&__actions {
display: flex;
align-items: center;
}
&__actions,
&__end {
> * + * {
margin-left: $interiorMarginSm;
}
}
&__start {
flex: 1 1 auto;
margin-right: $interiorMargin;
min-width: 0; // Forces interior to compress when pushed on
}
&__end {
flex: 0 0 auto;
}
&__nav-to-parent-button,
&__disclosure-button {
flex: 0 0 auto;
}
&__nav-to-parent-button {
// This is an icon-button
$p: $interiorMargin;
margin-right: $interiorMargin;
padding-left: $p;
padding-right: $p;
.is-editing & {
display: none;
}
}
&__object-name--w,
&__object-name {
flex: 0 1 auto;
}
&__object-name--w {
@include headerFont(1.4em);
min-width: 0;
&:before {
// Icon
margin-right: $interiorMargin;
}
}
&__object-details {
opacity: 0.5;
}
}
/************************** DRAWER */
.c-drawer { .c-drawer {
/* New sliding overlay or push element to contain things /* New sliding overlay or push element to contain things
* Designed for mobile and compact desktop scenarios * Designed for mobile and compact desktop scenarios
@ -332,4 +429,3 @@
} }
} }
} }

View File

@ -21,28 +21,12 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div class="l-preview-window"> <div class="l-preview-window">
<div class="l-browse-bar"> <PreviewHeader
<div class="l-browse-bar__start">
<div
class="l-browse-bar__object-name--w"
:class="type.cssClass"
>
<span class="l-browse-bar__object-name">
{{ domainObject.name }}
</span>
<context-menu-drop-down :object-path="objectPath" />
</div>
</div>
<div class="l-browse-bar__end">
<div class="l-browse-bar__actions">
<view-switcher
:views="views"
:current-view="currentView" :current-view="currentView"
:domain-object="domainObject"
:views="views"
@setView="setView" @setView="setView"
/> />
</div>
</div>
</div>
<div class="l-preview-window__object-view"> <div class="l-preview-window__object-view">
<div ref="objectView"></div> <div ref="objectView"></div>
</div> </div>
@ -50,13 +34,11 @@
</template> </template>
<script> <script>
import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue'; import PreviewHeader from './preview-header.vue';
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
export default { export default {
components: { components: {
ContextMenuDropDown, PreviewHeader
ViewSwitcher
}, },
inject: [ inject: [
'openmct', 'openmct',
@ -64,12 +46,9 @@ export default {
], ],
data() { data() {
let domainObject = this.objectPath[0]; let domainObject = this.objectPath[0];
let type = this.openmct.types.get(domainObject.type);
return { return {
domainObject: domainObject, domainObject: domainObject,
type: type,
notebookEnabled: false,
viewKey: undefined viewKey: undefined
}; };
}, },
@ -97,6 +76,7 @@ export default {
this.view.destroy(); this.view.destroy();
this.$refs.objectView.innerHTML = ''; this.$refs.objectView.innerHTML = '';
} }
delete this.view; delete this.view;
delete this.viewContainer; delete this.viewContainer;
}, },

View File

@ -36,7 +36,12 @@ export default class PreviewAction {
* Dependencies * Dependencies
*/ */
this._openmct = openmct; this._openmct = openmct;
if (PreviewAction.isVisible === undefined) {
PreviewAction.isVisible = false;
} }
}
invoke(objectPath) { invoke(objectPath) {
let preview = new Vue({ let preview = new Vue({
components: { components: {
@ -59,16 +64,27 @@ export default class PreviewAction {
callback: () => overlay.dismiss() callback: () => overlay.dismiss()
} }
], ],
onDestroy: () => preview.$destroy() onDestroy: () => {
PreviewAction.isVisible = false;
preview.$destroy()
}
}); });
PreviewAction.isVisible = true;
} }
appliesTo(objectPath) { appliesTo(objectPath) {
return !this._isNavigatedObject(objectPath) return !PreviewAction.isVisible && !this._isNavigatedObject(objectPath);
} }
_isNavigatedObject(objectPath) { _isNavigatedObject(objectPath) {
let targetObject = objectPath[0]; let targetObject = objectPath[0];
let navigatedObject = this._openmct.router.path[0]; let navigatedObject = this._openmct.router.path[0];
return targetObject.identifier.namespace === navigatedObject.identifier.namespace && return targetObject.identifier.namespace === navigatedObject.identifier.namespace &&
targetObject.identifier.key === navigatedObject.identifier.key; targetObject.identifier.key === navigatedObject.identifier.key;
} }
_preventPreview(objectPath) {
const noPreviewTypes = ['folder'];
return noPreviewTypes.includes(objectPath[0].type);
}
} }

View File

@ -0,0 +1,92 @@
<template>
<div class="l-browse-bar">
<div class="l-browse-bar__start">
<div
class="l-browse-bar__object-name--w"
:class="type.cssClass"
>
<span class="l-browse-bar__object-name">
{{ domainObject.name }}
</span>
<context-menu-drop-down :object-path="objectPath" />
</div>
</div>
<div class="l-browse-bar__end">
<div class="l-browse-bar__actions">
<view-switcher
:v-if="!hideViewSwitcher"
:views="views"
:current-view="currentView"
@setView="setView"
/>
<NotebookMenuSwitcher v-if="showNotebookMenuSwitcher"
:domain-object="domainObject"
:ignore-link="true"
:object-path="objectPath"
class="c-notebook-snapshot-menubutton"
/>
</div>
</div>
</div>
</template>
<script>
import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue';
import NotebookMenuSwitcher from '@/plugins/notebook/components/notebook-menu-switcher.vue';
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
export default {
inject: [
'openmct',
'objectPath'
],
components: {
ContextMenuDropDown,
NotebookMenuSwitcher,
ViewSwitcher
},
props: {
currentView: {
type: Object,
default: () => {
return {};
}
},
domainObject: {
type: Object,
default: () => {
return {};
}
},
hideViewSwitcher: {
type: Boolean,
default: () => {
return false;
}
},
showNotebookMenuSwitcher: {
type: Boolean,
default: () => {
return false;
}
},
views: {
type: Array,
default: () => {
return [];
}
}
},
data() {
return {
type: this.openmct.types.get(this.domainObject.type)
};
},
methods: {
setView(view) {
this.$emit('setView', view);
}
}
}
</script>