Compare commits

...

202 Commits

Author SHA1 Message Date
195aa0a95b Remove composition listeners on destroy 2020-06-30 09:30:49 -07:00
c252d435bf Clear data on bounds change. Clean up on destroy. 2020-06-29 18:21:57 -07:00
87aa5a4342 Cleaning up unused code 2020-06-29 17:16:57 -07:00
bd98b81339 some code reorg 2020-06-29 16:47:47 -07:00
f49556adad wip: functional with single trace, but debugging problems with changing bounds with multiple traces 2020-06-23 12:32:47 -07:00
f726bfa31a wip: refactoring 2020-06-22 14:56:22 -07:00
e9c6c5760e resolve conflicts 2020-06-19 13:42:23 -07:00
07c3dd6cfa wip: changes for bounds events 2020-06-19 13:40:20 -07:00
771fb9c044 [Display Layout] Allow multiple selection, duplication, and changing types (#3083)
* enable multiple selection

* enable object duplication

* enable copy styles

* enable converting plots and tables to alpha numerics

* enable merging multiple alpha numerics

* change icon for viewSwitcher

* allow users to merge overlay plots into a stacked plot

* New glyph for alphanumeric

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-06-19 11:44:17 -07:00
651a369391 refactored for multiple telemetry types 2020-06-18 15:36:38 -07:00
ae67a2f438 wip: refactoring for multiple telemetry types 2020-06-18 08:53:24 -07:00
055cf2b118 Lad testing (#3045)
* Added tests for LAD Tables and LAD Table Sets

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2020-06-17 17:25:34 -07:00
67ebcf4749 Update testing plan document with description of testathon process (#3022)
* Update testing plan document with description of testathon process
* Add instructions on unverified issues + link to instructions on pull requests.

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2020-06-17 15:05:02 -07:00
38dbf2ccab Addresses review comments for conditionals code (#2978)
Conditionals code refactoring

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-06-17 14:56:36 -07:00
e9968e3649 Replace Angular code that synchronizes URL parameters with Time API (#3089)
* Added new test to telemetry tables to check that telemetry data is correctly rendered in rows

* Added test tools for mocking builtins

* Changed order that promises are resolved to address race condition

* Remove duplicate installation of UTC Time System

* Added additional test telemetry

* Start Open MCT headless

* Added headless mode start option. Fixes #3064

* Added new non-angular URL handler

* Removed legacy Angular TimeSettingsURLHandler

* Added function to testTools to reset application state

* Use resetApplicationState function from telemetry table spec

* Added new TimeSettingsURLHandler to plugins

* Added missing semicolons

* #2826 Refactored code into separate class

* Handling of hash-relative URLs

* Refactoring URL sync code

* Refactored to external class

* Moved utils to new 'utils' directory. Refactored location util functions from class to exported functions

* Added test specs for openmctLocation

* Added new function to destroy instances of Open MCT between test runs

* Ensure test specs are cleaning up after themselves

* Added test spec for new URLTimeSettingsSynchronizer

* Removed use of shell script as it doesn't work in windows

* Pushed test coverage to 100%

* Added missing copyright statement

* Removed debugging output

* Fixed linting error

* Upgrade node version

* Clear cache

* Re-enabled tests

Co-authored-by: Melanie Lean <melanielean@Melanies-MacBook-Pro.local>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-17 13:58:25 -07:00
d9fafd2956 Tweaked code standards for ternaries and return statements (#3082)
* Tweaked code standards for ternaries and return statements

* Tweaked language slightly

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-17 10:52:30 -07:00
1acda469a9 fixed value formatting, removed listeners 2020-06-16 15:51:12 -07:00
d5b0ef735c Merge branch 'master' of https://github.com/nasa/openmct into plotly-test 2020-06-16 12:25:58 -07:00
a6cac13dfc removed duplicate function 2020-06-16 12:25:30 -07:00
3d058151f2 imported and used telemetry table components, removed console logs from telemetryTable.js 2020-06-16 12:16:17 -07:00
76817193eb minor color changes 2020-06-15 12:33:25 -07:00
35ef4407be changed way index is tracked mostly, other small changes 2020-06-15 11:16:01 -07:00
f3526f9185 refactored to follow telemetry tables pattern 2020-06-12 16:24:10 -07:00
b5aba7ce8f Merge pull request #3096 from nasa/revert-3095-patch-1
Revert "Update API.md"
2020-06-12 10:12:56 -07:00
0db5648e10 Revert "Update API.md" 2020-06-12 10:07:49 -07:00
83325da738 Merge pull request #3095 from willmendil/patch-1
Update API.md
2020-06-12 08:50:23 -07:00
4d1b2f3456 Update API.md
typo
2020-06-12 17:42:39 +02:00
6137700c82 Merge pull request #3092 from nasa/contributor-guide-grammar
Fixes a grammatical issue in the wording of the contributor's guide
2020-06-10 11:06:59 -07:00
91a1b3f31d Fixes a grammatical issue in the wording of the contributor's guide 2020-06-10 10:29:47 -07:00
70649b0657 added js-plotly-container class 2020-06-10 10:29:36 -07:00
357b25a76b LAD views should respond to conductor bounds changes (#2946)
Added bounds listener, moved history request to function, checking for race conditions
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-06-09 16:51:32 -07:00
3b89bf0b8c Merge branch 'plotly-test' of https://github.com/nasa/openmct into plotly-test 2020-06-09 11:08:03 -07:00
a314aa3c95 Merge branch 'master' of https://github.com/nasa/openmct into plotly-test 2020-06-09 11:07:27 -07:00
dc5723c227 tweaked margins 2020-06-09 11:07:14 -07:00
61fd7d4e4e mostly styling changes, removed isFixed arg from getLayout 2020-06-09 09:31:54 -07:00
bab53ad9bd Fix static styles bug - adding null checks. (#3084)
* Fix static styles bug - adding null checks. Fixes #3076

* Adds tests for updating conditional and static styles for layout drawing items

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-05 15:30:35 -07:00
d0d4579f13 Give users the option to hide table headers (#3085)
* working hide header config

* Simplified Inspector UI layout

* fix lint errors

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-06-05 15:09:59 -07:00
32fc871e67 correct usage of node_options 2020-06-04 09:28:43 -07:00
6b74a133d8 Upgraded karma 2020-06-03 17:41:42 -07:00
ceaf4c2ef0 Cleared circle cache 2020-06-03 16:29:55 -07:00
982b69e7e1 Don't rebuild sass every time 2020-06-03 16:25:08 -07:00
8e5182732d Used cross-platform instead of export 2020-06-03 16:24:13 -07:00
2d0b92cc38 Removed no-sandbox flag 2020-06-03 16:23:17 -07:00
75e846ea3f Added missing new line 2020-06-03 16:22:08 -07:00
1b5dee981d Reverted resource class change 2020-06-03 16:20:49 -07:00
4fee7f73e7 Fixed circle config 2020-06-03 16:19:49 -07:00
4308a4c9cf Removed report files 2020-06-03 16:17:23 -07:00
abbffcfbf1 Merge branch 'plotly-test' of https://github.com/nasa/openmct into plotly-test 2020-06-03 16:16:00 -07:00
01710876fb Increase circle resource class 2020-06-03 16:15:52 -07:00
9e3d97c85d added no-sandbox flag 2020-06-03 13:33:59 -07:00
519075001e bumped docker config to node 13 2020-06-03 13:12:45 -07:00
96a48dd9ee moved rebuild node-sass later 2020-06-03 13:08:00 -07:00
550daae76f add rebuild node-sass to config 2020-06-03 13:03:36 -07:00
b8fcb8ff14 change circleci config to node12 2020-06-03 12:45:46 -07:00
373ddd0bf5 updated circleci to use node13 2020-06-03 11:47:00 -07:00
d62cc6b3ee bumped karma-jasmine to ^2.0.0 2020-06-03 11:11:07 -07:00
5116d38437 added test memory increase 2020-06-03 11:03:36 -07:00
29771f2722 Merge branch 'master' of https://github.com/nasa/openmct into plotly-test 2020-06-02 16:52:34 -07:00
7bfe4bb25c restored karma-jasmine to original 2020-06-02 16:31:04 -07:00
510c637081 locked karma-jasmine dependency 2020-06-02 16:01:02 -07:00
02b537580c Merge pull request #3057 from nasa/code-standards-update
Updated code standard
2020-06-02 13:31:21 -07:00
d7c65fec4c changed jasmine-core to ^2.0.0 2020-06-02 13:11:18 -07:00
7073b0717f Merge branch 'master' into code-standards-update 2020-06-02 13:05:32 -07:00
d6bb1b2a12 Added note about requiring a single return statement. 2020-06-02 13:05:15 -07:00
626e2d8e80 changed jasmine-core to 2.0.0 to test circle-ci specfilter issue 2020-06-02 12:56:51 -07:00
1fe673c1f5 Merge branch 'plotly-test' of https://github.com/nasa/openmct into plotly-test 2020-06-02 12:44:35 -07:00
d9b00574e7 Merge branch 'master' of https://github.com/nasa/openmct into plotly-test 2020-06-02 12:38:39 -07:00
7c48b3ba9a Merge branch 'master' into plotly-test 2020-06-02 12:29:46 -07:00
c256696790 Image view should react to time conductor changes - 2712 (#2934)
Image view reacts to time conductor changes
Fixes #2712
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-06-02 12:27:55 -07:00
b6e589eed4 removed telemetry from initialization config 2020-06-01 14:46:54 -07:00
d5480e7524 Multiple item conditional styles (#3017)
Conditional Styles for multiple items #3076

* Preview condition styles on selecting that condition or one of it's styles
Do not evaluate conditional styles in edit mode

* Conditions styling tweaked

- Condition match `is-current` styling for browse and edit modes;
- Disallowed pointer events on Conditions to prevent selection in
Inspector when not editing;

* Highlight current condition in conditionSet view

* Addresses review comments.

* Condition matching highlighting tweaked

- Enable match highlighting during Editing;
- Tweaks to `is-current` styling;

* Don't reset the callback on destroy

* Combine multiple and single selection styling of objects

* Fix issue with applying styles in edit mode

* Fix item styles bug

* Remove comment

* Adds back visibility toggle

* Set isEditing on initialization.

* Addresses review comments - removes use of lodash.

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-01 12:40:40 -07:00
ab463e93fe Refactor Notifications to use Vue, and add clear all option (#3068)
[Notifications] Need a clear all notifications option #3006

* create new notifications indicator in vue, and sunset old one

* add notifications list overlay

* working notifications

* add support for progress notifications

* update percentage on mounted

* Markup cleanups in new Vue Notifications files

- Removed unneeded markup and class wrappers;
- Removed unneeded inline styling;

* remove example notifications

* fix lint errors

* make reviewer requested changes, remove old not needed files, add test

* update testTools to testUtils

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-06-01 11:45:33 -07:00
fb1813c14b change in legend position, trace names 2020-06-01 07:40:24 -07:00
cce834f873 changed colors, added legend, fixed removeTelemetry 2020-05-29 15:14:05 -07:00
ca60d02614 added name to plugin function, changed colors 2020-05-29 13:38:52 -07:00
9520c09b49 fixed updateData so plot is extended in fixed timespan 2020-05-29 12:02:31 -07:00
0d70717b35 addressed review comments 2020-05-29 09:55:37 -07:00
f18da542d6 added updateRange function, removed length arg from updateData, fixed unsubscribe 2020-05-29 09:37:56 -07:00
e7c38d473b added unsubscribe, subscribeTo methods 2020-05-28 15:26:30 -07:00
2f8db31a33 added dynamic layout for fixed, local clock 2020-05-28 15:25:30 -07:00
1c58d8c85f implemented Plotly.relayout to set x-axis correctly for local clock 2020-05-28 15:25:05 -07:00
baf426055c added back if in updateData 2020-05-28 15:23:04 -07:00
aa3af520ea refactor of refreshData 2020-05-28 15:21:15 -07:00
4a43893ccc debugging local clock issues 2020-05-28 15:21:01 -07:00
77f50a41c9 added some comments 2020-05-28 15:20:38 -07:00
e4cd5f441f added requestHistory 2020-05-28 15:19:44 -07:00
a9cfb002fb added listeners and bounds methods 2020-05-28 15:15:39 -07:00
b603a5b722 WIP... 2020-05-28 15:13:35 -07:00
be165761c7 WIP: adding bounds functions 2020-05-28 15:13:10 -07:00
da88cf58cc add, remove multiple traces, remove hover effects 2020-05-28 15:12:48 -07:00
bc2bd53f9b WIP 2020-05-28 15:12:11 -07:00
6b7fd5f22a import RemoveAction 2020-05-28 15:09:01 -07:00
4ee214a142 add code to set Y-axis and plot multiple telemetry types 2020-05-28 15:07:35 -07:00
4af24db38a changed mode and type, dropped old data as data extends 2020-05-28 15:06:24 -07:00
8db27809ef rendering a plotly plot from SWG 2020-05-28 15:04:36 -07:00
3116b1addd wip: added telemetry provider 2020-05-28 14:58:24 -07:00
5f67c45b50 working on viewProvider 2020-05-28 14:57:52 -07:00
8158afc29a added hardcoded test plot 2020-05-28 14:57:13 -07:00
13b3acb7e0 basic plotly plugin structure 2020-05-28 14:55:06 -07:00
8363c65312 [Notebook] display bounds change notification only if bounds changed (#3062) Resolves (#3051)
* display bounds change notification only if bounds changed

* avoid empty notification messages.
2020-05-28 12:29:32 -07:00
04598b6cf1 Bug fixes for plots (#3019) (#3069)
* prevent plots from breaking when more than one NaN is received.

* fix y axis label issue

* dont emit a viewport change event when marquee doesnt happen
2020-05-28 09:58:22 -07:00
43628ad9d6 Lodash upgrade and cleanup (#2990)
* Upgrades lodash
* Replaces some usage of lodash with native functions.
* Adds linting to catch cases where native functions could be used instead of lodash functions
* Renamed testTools to testUtils

Co-authored-by: Joshi <simplyrender@gmail.com>
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-05-27 10:59:02 -07:00
67bea86bc8 Added headless mode start option (#3065)
* Added headless mode start option. Fixes #3064
2020-05-26 11:39:55 -07:00
4eb4cbfffc Merge pull request #3020 from nasa/updated-checklists
Add details about pull requests to contributing guide
2020-05-19 09:30:05 -07:00
eda01abcbc Merge branch 'master' into updated-checklists 2020-05-18 12:12:26 -07:00
2fa29124bf Merge branch 'master' into code-standards-update 2020-05-18 11:45:27 -07:00
694b8f4666 provide format for name metadata (#3054) 2020-05-18 11:40:05 -07:00
33faeafa98 New proposed ESLint rules that require no code changes 2020-05-16 16:19:36 -07:00
a40ff07353 Updated guidance on anonymous functions 2020-05-16 16:11:42 -07:00
41dc9c794d Fixed capitalization on CONST 2020-05-16 16:08:43 -07:00
f1faf3965d Changed reference to constructors to classes 2020-05-16 16:07:49 -07:00
da2ecbbcad Fixed formatting issues, removed outdated example code. 2020-05-16 16:06:59 -07:00
32c892fe98 Updated code standards 2020-05-16 15:56:30 -07:00
bbb271a678 clarify value hints (#2673)
remove confusing comments regarding domain - input and range - output
2020-05-15 14:48:25 -07:00
fec1438806 dont emit a viewport change event when marquee doesnt happen (#3036)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-05-15 14:35:13 -07:00
28f19ec310 [Notebook] Clicking on an embed in a notebook entry does not result in bounds change #3034 (#3042) 2020-05-14 14:56:41 -07:00
f934454c25 Fixes computation of result when telemetry is not used by an object (#3040)
* Resolves issue #30307
Don't compute the result of a condition if telemetry datum is not being used by that condition

* Adds tests for condition results
2020-05-13 14:30:45 -07:00
eb49ffae02 Update CONTRIBUTING.md 2020-05-13 09:21:00 -07:00
5751012872 Update CONTRIBUTING.md 2020-05-13 09:20:03 -07:00
aa041e04cf Merge remote-tracking branch 'origin/update-contributing-guidelines' into updated-checklists 2020-05-11 17:13:53 -07:00
24e7ea143a Added more details on the process around pull requests 2020-05-11 17:12:25 -07:00
79d5d9c4d0 Update CONTRIBUTING.md 2020-05-11 16:15:17 -07:00
b5bfdc4418 Update CONTRIBUTING.md 2020-05-11 15:56:49 -07:00
59730c60ec Update CONTRIBUTING.md 2020-05-11 15:54:25 -07:00
4a87a5d847 Show object styles in preview modal (#3018)
* Adds conditional styles to Preview window
2020-05-11 14:25:39 -07:00
421c09ec2c Allow users to lazy load Tabs (#2958)
* lazy load tabs

* remove listener on destroy

* fix lint error

* Store current tab position on domainObject

* remove lodash dependency and use keystring
2020-05-08 10:36:13 -07:00
0679b246b8 [Notebook]: Remove default section/page from localstorage on notebook delete (#2900)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-05-07 16:13:32 -07:00
83f9c6c528 improve plot gestures - and clean up (#3013) 2020-05-06 10:33:59 -07:00
a5f3ba6259 Use evalAsync instead of digest() (#3001)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-05-05 12:23:41 -07:00
a70facf0c8 Merge pull request #3000 from nasa/upgrade-moment
Upgrade moment to 2.25.3
2020-05-05 11:01:39 -07:00
447fe94325 Merge branch 'upgrade-moment' of https://github.com/nasa/openmct into upgrade-moment 2020-05-05 10:36:43 -07:00
8e2b666766 Upgraded moment version to 2.25.3 2020-05-05 10:36:33 -07:00
dcbfbdbb89 Merge branch 'master' into upgrade-moment 2020-05-05 10:12:50 -07:00
4c76bf34ab Highlight currently winning Condition in Condition Set Edit and Read-only views (#2936)
* Preview condition styles on selecting that condition or one of it's styles

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-05-05 09:55:42 -07:00
81b7a9d3e0 Merge branch 'master' into upgrade-moment 2020-05-01 16:47:26 -07:00
dc573c479c Upgrade moment to 2.24.0 2020-05-01 16:25:49 -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
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
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
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
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
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
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
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
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
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
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
3b6ef9b44b Refactor duplicate code into functions 2020-04-09 15:33:52 -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
212 changed files with 7781 additions and 3528 deletions

View File

@ -2,7 +2,7 @@ version: 2
jobs:
build:
docker:
- image: circleci/node:8-browsers
- image: circleci/node:13-browsers
environment:
CHROME_BIN: "/usr/bin/google-chrome"
steps:
@ -11,17 +11,17 @@ jobs:
name: Update npm
command: 'sudo npm install -g npm@latest'
- restore_cache:
key: dependency-cache-{{ checksum "package.json" }}
key: dependency-cache-13-{{ checksum "package.json" }}
- run:
name: Installing dependencies (npm install)
command: npm install
- save_cache:
key: dependency-cache-{{ checksum "package.json" }}
key: dependency-cache-13-{{ checksum "package.json" }}
paths:
- node_modules
- run:
name: npm run test
command: npm run test
name: npm run test:coverage
command: npm run test:coverage
- run:
name: npm run lint
command: npm run lint

View File

@ -1,3 +1,4 @@
const LEGACY_FILES = ["platform/**", "example/**"];
module.exports = {
"env": {
"browser": true,
@ -10,7 +11,8 @@ module.exports = {
},
"extends": [
"eslint:recommended",
"plugin:vue/recommended"
"plugin:vue/recommended",
"plugin:you-dont-need-lodash-underscore/compatible"
],
"parser": "vue-eslint-parser",
"parserOptions": {
@ -22,6 +24,9 @@ module.exports = {
}
},
"rules": {
"you-dont-need-lodash-underscore/omit": "off",
"you-dont-need-lodash-underscore/throttle": "off",
"you-dont-need-lodash-underscore/flatten": "off",
"no-bitwise": "error",
"curly": "error",
"eqeqeq": "error",
@ -66,6 +71,56 @@ module.exports = {
],
"dot-notation": "error",
"indent": ["error", 4],
// https://eslint.org/docs/rules/no-case-declarations
"no-case-declarations": "error",
// https://eslint.org/docs/rules/max-classes-per-file
"max-classes-per-file": ["error", 1],
// https://eslint.org/docs/rules/no-eq-null
"no-eq-null": "error",
// https://eslint.org/docs/rules/no-eval
"no-eval": "error",
// https://eslint.org/docs/rules/no-floating-decimal
"no-floating-decimal": "error",
// https://eslint.org/docs/rules/no-implicit-globals
"no-implicit-globals": "error",
// https://eslint.org/docs/rules/no-implied-eval
"no-implied-eval": "error",
// https://eslint.org/docs/rules/no-lone-blocks
"no-lone-blocks": "error",
// https://eslint.org/docs/rules/no-loop-func
"no-loop-func": "error",
// https://eslint.org/docs/rules/no-new-func
"no-new-func": "error",
// https://eslint.org/docs/rules/no-new-wrappers
"no-new-wrappers": "error",
// https://eslint.org/docs/rules/no-octal-escape
"no-octal-escape": "error",
// https://eslint.org/docs/rules/no-proto
"no-proto": "error",
// https://eslint.org/docs/rules/no-return-await
"no-return-await": "error",
// https://eslint.org/docs/rules/no-script-url
"no-script-url": "error",
// https://eslint.org/docs/rules/no-self-compare
"no-self-compare": "error",
// https://eslint.org/docs/rules/no-sequences
"no-sequences": "error",
// https://eslint.org/docs/rules/no-unmodified-loop-condition
"no-unmodified-loop-condition": "error",
// https://eslint.org/docs/rules/no-useless-call
"no-useless-call": "error",
// https://eslint.org/docs/rules/wrap-iife
"wrap-iife": "error",
// https://eslint.org/docs/rules/no-nested-ternary
"no-nested-ternary": "error",
// https://eslint.org/docs/rules/switch-colon-spacing
"switch-colon-spacing": "error",
// https://eslint.org/docs/rules/no-useless-computed-key
"no-useless-computed-key": "error",
// https://eslint.org/docs/rules/rest-spread-spacing
"rest-spread-spacing": ["error"],
"vue/html-indent": [
"error",
4,
@ -112,6 +167,13 @@ module.exports = {
}
]
}
}, {
"files": LEGACY_FILES,
"rules": {
// https://eslint.org/docs/rules/no-nested-ternary
"no-nested-ternary": "off",
"no-var": "off"
}
}
]
};

1
.gitignore vendored
View File

@ -38,3 +38,4 @@ protractor/logs
npm-debug.log
package-lock.json
report.*.json

4
API.md
View File

@ -427,8 +427,8 @@ Each telemetry value description has an object defining hints. Keys in this thi
Known hints:
* `domain`: Indicates that the value represents the "input" of a datum. Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first.
* `range`: Indicates that the value is the "output" of a datum. Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values.
* `domain`: Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first.
* `range`: Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values.
* `image`: Indicates that the value may be interpreted as the URL to an image file, in which case appropriate views will be made available.
##### The Time Conductor and Telemetry

View File

@ -103,7 +103,7 @@ the name chosen could not be mistaken for a topic or master branch.
### Merging
When development is complete on an issue, the first step toward merging it
back into the master branch is to file a Pull Request. The contributions
back into the master branch is to file a Pull Request (PR). The contributions
should meet code, test, and commit message standards as described below,
and the pull request should include a completed author checklist, also
as described below. Pull requests may be assigned to specific team
@ -114,6 +114,15 @@ request. When the reviewer is satisfied, they should add a comment to
the pull request containing the reviewer checklist (from below) and complete
the merge back to the master branch.
Additionally:
* Every pull request must link to the issue that it addresses. Eg. “Addresses #1234” or “Closes #1234”. This is the responsibility of the pull requests __author__. If no issue exists, create one.
* Every __author__ must include testing instructions. These instructions should identify the areas of code affected, and some minimal test steps. If addressing a bug, reproduction steps should be included, if they were not included in the original issue. If reproduction steps were included on the original issue, and are sufficient, refer to them.
* A pull request that closes an issue should say so in the description. Including the text “Closes #1234” will cause the linked issue to be automatically closed when the pull request is merged. This is the responsibility of the pull requests __author__.
* When a pull request is merged, and the corresponding issue closed, the __reviewer__ must add the tag “unverified” to the original issue. This will indicate that although the issue is closed, it has not been tested yet.
* Every PR must have two reviewers assigned, though only one approval is necessary for merge.
* Changes to API require approval by a senior developer.
* When creating a PR, it is the author's responsibility to apply any priority label from the issue to the PR as well. This helps with prioritization.
## Standards
Contributions to Open MCT are expected to meet the following standards.
@ -122,89 +131,96 @@ changes.
### Code Standards
JavaScript sources in Open MCT must satisfy JSLint under its default
settings. This is verified by the command line build.
JavaScript sources in Open MCT must satisfy the ESLint rules defined in
this repository. This is verified by the command line build.
#### Code Guidelines
JavaScript sources in Open MCT should:
* Use four spaces for indentation. Tabs should not be used.
* Include JSDoc for any exposed API (e.g. public methods, constructors).
* Include non-JSDoc comments as-needed for explaining private variables,
methods, or algorithms when they are non-obvious.
* Define one public class per script, expressed as a constructor function
returned from an AMD-style module.
* Follow “Java-like” naming conventions. These includes:
* Classes should use camel case, first letter capitalized
(e.g. SomeClassName).
* Methods, variables, fields, and function names should use camel case,
first letter lower-case (e.g. someVariableName).
* Constants (variables or fields which are meant to be declared and
initialized statically, and never changed) should use only capital
letters, with underscores between words (e.g. SOME_CONSTANT).
* File names should be the name of the exported class, plus a .js extension
(e.g. SomeClassName.js).
* Avoid anonymous functions, except when functions are short (a few lines)
and/or their inclusion makes sense within the flow of the code
(e.g. as arguments to a forEach call).
* Avoid deep nesting (especially of functions), except where necessary
(e.g. due to closure scope).
* End with a single new-line character.
* Expose public methods by declaring them on the class's prototype.
* Within a given function's scope, do not mix declarations and imperative
code, and present these in the following order:
* First, variable declarations and initialization.
* Second, function declarations.
* Third, imperative statements.
* Finally, the returned value.
The following guidelines are provided for anyone contributing source code to the Open MCT project:
1. Write clean code. Heres a good summary - https://github.com/ryanmcdermott/clean-code-javascript.
1. Include JSDoc for any exposed API (e.g. public methods, classes).
1. Include non-JSDoc comments as-needed for explaining private variables,
methods, or algorithms when they are non-obvious. Otherwise code
should be self-documenting.
1. Classes and Vue components should use camel case, first letter capitalized
(e.g. SomeClassName).
1. Methods, variables, fields, events, and function names should use camelCase,
first letter lower-case (e.g. someVariableName).
1. Source files that export functions should use camelCase, first letter lower-case (eg. testTools.js)
1. Constants (variables or fields which are meant to be declared and
initialized statically, and never changed) should use only capital
letters, with underscores between words (e.g. SOME_CONSTANT). They should always be declared as `const`s
1. File names should be the name of the exported class, plus a .js extension
(e.g. SomeClassName.js).
1. Avoid anonymous functions, except when functions are short (one or two lines)
and their inclusion makes sense within the flow of the code
(e.g. as arguments to a forEach call). Anonymous functions should always be arrow functions.
1. Named functions are preferred over functions assigned to variables.
eg.
```JavaScript
function renameObject(object, newName) {
Object.name = newName;
}
```
is preferable to
```JavaScript
const rename = (object, newName) => {
Object.name = newName;
}
```
1. Avoid deep nesting (especially of functions), except where necessary
(e.g. due to closure scope).
1. End with a single new-line character.
1. Always use ES6 `Class`es and inheritence rather than the pre-ES6 prototypal
pattern.
1. Within a given function's scope, do not mix declarations and imperative
code, and present these in the following order:
* First, variable declarations and initialization.
* Secondly, imperative statements.
* Finally, the returned value. A single return statement at the end of the function should be used, except where an early return would improve code clarity.
1. Avoid the use of "magic" values.
eg.
```JavaScript
Const UNAUTHORIZED = 401
if (responseCode === UNAUTHORIZED)
```
is preferable to
```JavaScript
if (responseCode === 401)
```
1. Use the ternary operator only for simple cases such as variable assignment. Nested ternaries should be avoided in all cases.
1. Test specs should reside alongside the source code they test, not in a separate directory.
1. Organize code by feature, not by type.
eg.
```
- telemetryTable
- row
TableRow.js
TableRowCollection.js
TableRow.vue
- column
TableColumn.js
TableColumn.vue
plugin.js
pluginSpec.js
```
is preferable to
```
- telemetryTable
- components
TableRow.vue
TableColumn.vue
- collections
TableRowCollection.js
TableColumn.js
TableRow.js
plugin.js
pluginSpec.js
```
Deviations from Open MCT code style guidelines require two-party agreement,
typically from the author of the change and its reviewer.
#### Code Example
```js
/*global define*/
/**
* Bundles should declare themselves as namespaces in whichever source
* file is most like the "main point of entry" to the bundle.
* @namespace some/bundle
*/
define(
['./OtherClass'],
function (OtherClass) {
"use strict";
/**
* A summary of how to use this class goes here.
*
* @constructor
* @memberof some/bundle
*/
function ExampleClass() {
}
// Methods which are not intended for external use should
// not have JSDoc (or should be marked @private)
ExampleClass.prototype.privateMethod = function () {
};
/**
* A summary of this method goes here.
* @param {number} n a parameter
* @returns {number} a return value
*/
ExampleClass.prototype.publicMethod = function (n) {
return n * 2;
}
return ExampleClass;
}
);
```
### Test Standards
Automated testing shall occur whenever changes are merged into the main
@ -292,6 +308,7 @@ checklist).
2. Unit tests included and/or updated with changes?
3. Command line build passes?
4. Changes have been smoke-tested?
5. Testing instructions included?
### Reviewer Checklist
@ -299,3 +316,4 @@ checklist).
2. Appropriate unit tests included?
3. Code style and in-line documentation are appropriate?
4. Commit messages meet standards?
5. Has associated issue been labelled `unverified`? (only applicable if this PR closes the issue)

View File

@ -125,3 +125,22 @@ A release is not closed until both categories have been performed on
the latest snapshot of the software, _and_ no issues labelled as
["blocker" or "critical"](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)
remain open.
### Testathons
Testathons can be used as a means of performing per-sprint and per-release testing.
#### Timing
For per-sprint testing, a testathon is typically performed at the beginning of the third week of a sprint, and again later that week to verify any fixes. For per-release testing, a testathon is typically performed prior to any formal testing processes that are applicable to that release.
#### Process
1. Prior to the scheduled testathon, a list will be compiled of all issues that are closed and unverified.
2. For each issue, testers should review the associated PR for testing instructions. See the contributing guide for instructions on [pull requests](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md#merging).
3. As each issue is verified via testing, any team members testing it should leave a comment on that issue indicating that it has been verified fixed.
4. If a bug is found that relates to an issue being tested, notes should be included on the associated issue, and the issue should be reopened. Bug notes should include reproduction steps.
5. For any bugs that are not obviously related to any of the issues under test, a new issue should be created with details about the bug, including reproduction steps. If unsure about whether a bug relates to an issue being tested, just create a new issue.
6. At the end of the testathon, triage will take place, where all tested issues will be reviewed.
7. If verified fixed, an issue will remain closed, and will have the “unverified” label removed.
8. For any bugs found, a severity will be assigned.
9. A second testathon will be scheduled for later in the week that will aim to address all issues identified as blockers, as well as any other issues scoped by the team during triage.
10. Any issues that were not tested will remain "unverified" and will be picked up in the next testathon.

View File

@ -50,7 +50,8 @@ define([
values: [
{
key: "name",
name: "Name"
name: "Name",
format: "string"
},
{
key: "utc",
@ -99,7 +100,7 @@ define([
};
GeneratorMetadataProvider.prototype.getMetadata = function (domainObject) {
return _.extend(
return Object.assign(
{},
domainObject.telemetry,
METADATA_BY_TYPE[domainObject.type]

View File

@ -52,6 +52,7 @@ define([
return {
name: name,
utc: Math.floor(timestamp / 5000) * 5000,
local: Math.floor(timestamp / 5000) * 5000,
url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length]
};
}
@ -78,7 +79,7 @@ define([
},
request: function (domainObject, options) {
var start = options.start;
var end = options.end;
var end = Math.min(options.end, Date.now());
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, domainObject.name));
@ -118,6 +119,14 @@ define([
name: 'Time',
key: 'utc',
format: 'utc',
hints: {
domain: 2
}
},
{
name: 'Local Time',
key: 'local',
format: 'local-format',
hints: {
domain: 1
}

View File

@ -1,14 +1,14 @@
<template>
<div class="example">{{ msg }}</div>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello world!'
data() {
return {
msg: 'Hello world!'
}
}
}
}
</script>

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
Administration. All rights reserved.
@ -34,6 +34,7 @@
<body>
</body>
<script>
const ONE_MINUTE = 60 * 1000;
const FIVE_MINUTES = 5 * 60 * 1000;
const THIRTY_MINUTES = 30 * 60 * 1000;
@ -43,9 +44,9 @@
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
);
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.Generator());
openmct.install(openmct.plugins.ExampleImagery());
openmct.install(openmct.plugins.UTCTimeSystem());
@ -70,8 +71,8 @@
timeSystem: 'utc',
clock: 'local',
clockOffsets: {
start: - THIRTY_MINUTES,
end: FIVE_MINUTES
start: - FIVE_MINUTES,
end: ONE_MINUTE
}
}
]

View File

@ -24,16 +24,27 @@
const devMode = process.env.NODE_ENV !== 'production';
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) => {
const webpackConfig = require('./webpack.config.js');
delete webpackConfig.output;
if (!devMode) {
if (!devMode || coverageEnabled) {
webpackConfig.module.rules.push({
test: /\.js$/,
exclude: /node_modules|example/,
use: 'istanbul-instrumenter-loader'
exclude: /node_modules|example|lib|dist/,
use: {
loader: 'istanbul-instrumenter-loader',
options: {
esModules: true
}
}
});
}
@ -45,11 +56,7 @@ module.exports = (config) => {
'src/**/*Spec.js'
],
port: 9876,
reporters: [
'progress',
'coverage',
'html'
],
reporters: reporters,
browsers: browsers,
customLaunchers: {
ChromeDebugging: {
@ -61,27 +68,27 @@ module.exports = (config) => {
colors: true,
logLevel: config.LOG_INFO,
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.
htmlReporter: {
outputDir: "dist/reports/tests",
preserveDescribeNesting: true,
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: {
// add webpack as preprocessor
'platform/**/*Spec.js': [ 'webpack', 'sourcemap' ],
'src/**/*Spec.js': [ 'webpack', 'sourcemap' ]
'platform/**/*Spec.js': ['webpack', 'sourcemap'],
'src/**/*Spec.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {

View File

@ -2,10 +2,8 @@
"name": "openmct",
"version": "1.0.0-snapshot",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {
"acorn": "6.2.0",
"angular": "1.4.14",
"angular": "1.7.9",
"angular-route": "1.4.14",
"babel-eslint": "8.2.6",
"comma-separated-values": "^3.6.4",
@ -25,6 +23,7 @@
"d3-time-format": "2.1.x",
"eslint": "5.2.0",
"eslint-plugin-vue": "^6.0.0",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
"eventemitter3": "^1.2.0",
"exports-loader": "^0.7.0",
"express": "^4.13.1",
@ -39,26 +38,28 @@
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.1.0",
"jsdoc": "^3.3.2",
"karma": "^2.0.3",
"karma-chrome-launcher": "^2.2.0",
"karma": "5.0.9",
"karma-chrome-launcher": "3.1.0",
"karma-cli": "^1.0.1",
"karma-coverage": "^1.1.2",
"karma-coverage-istanbul-reporter": "^2.1.1",
"karma-html-reporter": "^0.2.7",
"karma-jasmine": "^1.1.2",
"karma-jasmine": "^2.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^3.0.0",
"location-bar": "^3.0.1",
"lodash": "^3.10.1",
"lodash": "^4.17.12",
"markdown-toc": "^0.11.7",
"marked": "^0.3.5",
"mini-css-extract-plugin": "^0.4.1",
"minimist": "^1.1.1",
"moment": "^2.11.1",
"moment": "2.25.3",
"moment-duration-format": "^2.2.2",
"moment-timezone": "^0.5.21",
"moment-timezone": "0.5.28",
"node-bourbon": "^4.2.3",
"node-sass": "^4.9.2",
"painterro": "^0.2.65",
"plotly.js-dist": "^1.54.1",
"printj": "^1.2.1",
"raw-loader": "^0.5.1",
"request": "^2.69.0",
@ -76,15 +77,17 @@
"zepto": "^1.2.0"
},
"scripts": {
"clean": "rm -rf ./dist",
"start": "node app.js",
"lint": "eslint platform example src/**/*.{js,vue} openmct.js",
"lint:fix": "eslint platform example src/**/*.{js,vue} openmct.js --fix",
"lint": "eslint platform example src --ext .js,.vue openmct.js",
"lint:fix": "eslint platform example src --ext .js,.vue openmct.js --fix",
"build:prod": "cross-env NODE_ENV=production webpack",
"build:dev": "webpack",
"build:watch": "webpack --watch",
"test": "karma start --single-run",
"test-debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:watch": "karma start --no-single-run",
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"verify": "concurrently 'npm:test' 'npm:lint'",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
"otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",

View File

@ -6,6 +6,12 @@
ng-show="ngModel.dialog.messages.length > 1 ||
ngModel.dialog.messages.length == 0">s</span>
</div>
<button
ng-if="ngModel.dialog.topBarButton"
class="c-button c-button--major"
ng-click="ngModel.topBarButton.onClick">
{{ ngModel.topBarButton.label }}
</button>
</div>
<div class="w-messages c-overlay__messages">
<mct-include
@ -16,7 +22,7 @@
<button ng-repeat="dialogAction in ngModel.dialog.actions"
class="c-button c-button--major"
ng-click="dialogAction.action()">
{{dialogAction.label}}
{{ dialogAction.label }}
</button>
</div>
</div>

View File

@ -21,7 +21,7 @@
*****************************************************************************/
define(
['../../../../../src/api/objects/object-utils'],
['objectUtils'],
function (objectUtils) {
/**

View File

@ -21,44 +21,15 @@
*****************************************************************************/
define([
"./src/NotificationIndicatorController",
"./src/NotificationIndicator",
"./src/NotificationService",
"./res/notification-indicator.html"
"./src/NotificationService"
], function (
NotificationIndicatorController,
NotificationIndicator,
NotificationService,
notificationIndicatorTemplate
NotificationService
) {
return {
name:"platform/commonUI/notification",
definition: {
"extensions": {
"templates": [
{
"key": "notificationIndicatorTemplate",
"template": notificationIndicatorTemplate
}
],
"controllers": [
{
"key": "NotificationIndicatorController",
"implementation": NotificationIndicatorController,
"depends": [
"$scope",
"openmct",
"dialogService"
]
}
],
"indicators": [
{
"implementation": NotificationIndicator,
"priority": "fallback"
}
],
"services": [
{
"key": "notificationService",

View File

@ -1,8 +0,0 @@
<!-- DO NOT ADD SPACES BETWEEN THE SPANS - IT ADDS WHITE SPACE!! -->
<div ng-show="notifications.length > 0" class="c-indicator c-indicator--clickable s-status-{{highest.severity}} icon-bell"
ng-controller="NotificationIndicatorController">
<span class="label c-indicator__label">
<button ng-click="showNotificationsList()">
{{notifications.length}} Notification<span ng-show="notifications.length > 1">s</span></button>
</span><span class="c-indicator__count">{{notifications.length}}</span>
</div>

View File

@ -1,73 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
/**
* Provides an indicator that is visible when there are
* banner notifications that have been minimized. Will also indicate
* the number of notifications. Notifications can be viewed by
* clicking on the indicator to launch a dialog showing a list of
* notifications.
* @param $scope
* @param notificationService
* @param dialogService
* @constructor
*/
function NotificationIndicatorController($scope, openmct, dialogService) {
$scope.notifications = openmct.notifications.notifications;
$scope.highest = openmct.notifications.highest;
/**
* Launch a dialog showing a list of current notifications.
*/
$scope.showNotificationsList = function () {
let notificationsList = openmct.notifications.notifications.map(notification => {
if (notification.model.severity === 'alert' || notification.model.severity === 'info') {
notification.model.primaryOption = {
label: 'Dismiss',
callback: () => {
let currentIndex = notificationsList.indexOf(notification);
notification.dismiss();
notificationsList.splice(currentIndex, 1);
}
}
}
return notification;
})
dialogService.getDialogResponse('overlay-message-list', {
dialog: {
title: "Messages",
//Launch the message list dialog with the models
// from the notifications
messages: notificationsList
}
});
};
}
return NotificationIndicatorController;
}
);

View File

@ -1,60 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['../src/NotificationIndicatorController'],
function (NotificationIndicatorController) {
xdescribe("The notification indicator controller ", function () {
var mockNotificationService,
mockScope,
mockDialogService,
controller;
beforeEach(function () {
mockNotificationService = jasmine.createSpy("notificationService");
mockScope = jasmine.createSpy("$scope");
mockDialogService = jasmine.createSpyObj(
"dialogService",
["getDialogResponse","dismiss"]
);
mockNotificationService.highest = {
severity: "error"
};
controller = new NotificationIndicatorController(mockScope, mockNotificationService, mockDialogService);
});
it("exposes the highest notification severity to the template", function () {
expect(mockScope.highest).toBeTruthy();
expect(mockScope.highest.severity).toBe("error");
});
it("invokes the dialog service to show list of messages", function () {
expect(mockScope.showNotificationsList).toBeDefined();
mockScope.showNotificationsList();
expect(mockDialogService.getDialogResponse).toHaveBeenCalled();
expect(mockDialogService.getDialogResponse.calls.mostRecent().args[0]).toBe('overlay-message-list');
expect(mockDialogService.getDialogResponse.calls.mostRecent().args[1].dialog).toBeDefined();
});
});
}
);

View File

@ -26,7 +26,7 @@
* @namespace platform/containment
*/
define(
['../../../src/api/objects/object-utils'],
['objectUtils'],
function (objectUtils) {
function PersistableCompositionPolicy(openmct) {

View File

@ -81,7 +81,7 @@ define(
baseContext = context || {};
}
var actionContext = _.extend({}, baseContext);
var actionContext = Object.assign({}, baseContext);
actionContext.domainObject = this.domainObject;
return this.actionService.getActions(actionContext);

View File

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

View File

@ -121,7 +121,7 @@ define(['lodash'], function (_) {
*/
ExportAsJSONAction.prototype.rewriteLink = function (child, parent) {
this.externalIdentifiers.push(this.getId(child));
var index = _.findIndex(parent.composition, function (id) {
var index = parent.composition.findIndex(id => {
return _.isEqual(child.identifier, id);
});
var copyOfChild = this.copyObject(child);

View File

@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['zepto', '../../../../src/api/objects/object-utils.js'], function ($, objectUtils) {
define(['zepto', 'objectUtils'], function ($, objectUtils) {
/**
* The ImportAsJSONAction is available from context menus and allows a user

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@
* Module defining GenericSearchProvider. Created by shale on 07/16/2015.
*/
define([
'../../../../src/api/objects/object-utils',
'objectUtils',
'lodash'
], function (
objectUtils,
@ -191,7 +191,7 @@ define([
}
var domainObject = objectUtils.toNewFormat(model, id);
var composition = _.find(this.openmct.composition.registry, function (p) {
var composition = this.openmct.composition.registry.find(p => {
return p.appliesTo(domainObject);
});

View File

@ -25,7 +25,7 @@
*/
define(
[
'../../../src/api/objects/object-utils',
'objectUtils',
'lodash'
],
function (
@ -235,7 +235,7 @@ define(
var defaultRange = metadata.valuesForHints(['range'])[0];
defaultRange = defaultRange ? defaultRange.key : undefined;
var sourceMap = _.indexBy(metadata.values(), 'key');
var sourceMap = _.keyBy(metadata.values(), 'key');
var isLegacyProvider = telemetryAPI.findRequestProvider(domainObject) ===
telemetryAPI.legacyProvider;
@ -300,7 +300,7 @@ define(
var defaultRange = metadata.valuesForHints(['range'])[0];
defaultRange = defaultRange ? defaultRange.key : undefined;
var sourceMap = _.indexBy(metadata.values(), 'key');
var sourceMap = _.keyBy(metadata.values(), 'key');
var isLegacyProvider = telemetryAPI.findSubscriptionProvider(domainObject) ===
telemetryAPI.legacyProvider;

View File

@ -28,7 +28,7 @@ define([
'./api/api',
'./api/overlays/OverlayAPI',
'./selection/Selection',
'./api/objects/object-utils',
'objectUtils',
'./plugins/plugins',
'./adapter/indicators/legacy-indicators-plugin',
'./plugins/buildInfo/plugin',
@ -249,9 +249,10 @@ define([
this.legacyRegistry = new BundleRegistry();
installDefaultBundles(this.legacyRegistry);
// Plugin's that are installed by default
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.PlotlyPlot());
this.install(this.plugins.TelemetryTable());
this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin());
@ -266,6 +267,8 @@ define([
this.install(this.plugins.WebPage());
this.install(this.plugins.Condition());
this.install(this.plugins.ConditionWidget());
this.install(this.plugins.URLTimeSettingsSynchronizer());
this.install(this.plugins.NotificationIndicator());
}
MCT.prototype = Object.create(EventEmitter.prototype);
@ -350,17 +353,13 @@ define([
* @param {HTMLElement} [domElement] the DOM element in which to run
* MCT; if undefined, MCT will be run in the body of the document
*/
MCT.prototype.start = function (domElement) {
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
if (!this.plugins.DisplayLayout._installed) {
this.install(this.plugins.DisplayLayout({
showAsView: ['summary-widget']
}));
}
if (!domElement) {
domElement = document.body;
}
this.element = domElement;
this.legacyExtension('runs', {
@ -400,24 +399,31 @@ define([
// something has depended upon objectService. Cool, right?
this.$injector.get('objectService');
var appLayout = new Vue({
components: {
'Layout': Layout.default
},
provide: {
openmct: this
},
template: '<Layout ref="layout"></Layout>'
});
domElement.appendChild(appLayout.$mount().$el);
if (!isHeadlessMode) {
var appLayout = new Vue({
components: {
'Layout': Layout.default
},
provide: {
openmct: this
},
template: '<Layout ref="layout"></Layout>'
});
domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout.$refs.layout;
Browse(this);
this.layout = appLayout.$refs.layout;
Browse(this);
}
this.router.start();
this.emit('start');
}.bind(this));
};
MCT.prototype.startHeadless = function () {
let unreachableNode = document.createElement('div');
return this.start(unreachableNode, true);
}
/**
* Install a plugin in MCT.
*
@ -429,6 +435,10 @@ define([
plugin(this);
};
MCT.prototype.destroy = function () {
this.emit('destroy');
};
MCT.prototype.plugins = plugins;
return MCT;

View File

@ -21,24 +21,28 @@
*****************************************************************************/
define([
'./MCT',
'./plugins/plugins',
'legacyRegistry'
], function (MCT, plugins, legacyRegistry) {
xdescribe("MCT", function () {
'legacyRegistry',
'utils/testing'
], function (plugins, legacyRegistry, testUtils) {
describe("MCT", function () {
var openmct;
var mockPlugin;
var mockPlugin2;
var mockListener;
var oldBundles;
beforeAll(() => {
testUtils.resetApplicationState();
});
beforeEach(function () {
mockPlugin = jasmine.createSpy('plugin');
mockPlugin2 = jasmine.createSpy('plugin2');
mockListener = jasmine.createSpy('listener');
oldBundles = legacyRegistry.list();
openmct = new MCT();
openmct = testUtils.createOpenMct();
openmct.install(mockPlugin);
openmct.install(mockPlugin2);
@ -52,6 +56,7 @@ define([
legacyRegistry.delete(bundle);
}
});
testUtils.resetApplicationState(openmct);
});
it("exposes plugins", function () {
@ -63,8 +68,11 @@ define([
});
describe("start", function () {
beforeEach(function () {
openmct.start();
let appHolder;
beforeEach(function (done) {
appHolder = document.createElement("div");
openmct.on('start', done);
openmct.start(appHolder);
});
it("calls plugins for configuration", function () {
@ -75,25 +83,51 @@ define([
it("emits a start event", function () {
expect(mockListener).toHaveBeenCalled();
});
it("Renders the application into the provided container element", function () {
let openMctShellElements = appHolder.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(1);
});
});
describe("startHeadless", function () {
beforeEach(function (done) {
openmct.on('start', done);
openmct.startHeadless();
});
it("calls plugins for configuration", function () {
expect(mockPlugin).toHaveBeenCalledWith(openmct);
expect(mockPlugin2).toHaveBeenCalledWith(openmct);
});
it("emits a start event", function () {
expect(mockListener).toHaveBeenCalled();
});
it("Does not render Open MCT", function () {
let openMctShellElements = document.body.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(0);
});
});
describe("setAssetPath", function () {
var testAssetPath;
beforeEach(function () {
testAssetPath = "some/path";
openmct.legacyExtension = jasmine.createSpy('legacyExtension');
openmct.setAssetPath(testAssetPath);
});
it("internally configures the path for assets", function () {
expect(openmct.legacyExtension).toHaveBeenCalledWith(
'constants',
{
key: "ASSETS_PATH",
value: testAssetPath
}
);
it("configures the path for assets", function () {
testAssetPath = "some/path/";
openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath);
});
it("adds a trailing /", function () {
testAssetPath = "some/path";
openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath + "/");
});
});
});

View File

@ -21,7 +21,7 @@
*****************************************************************************/
define([
'../../api/objects/object-utils'
'objectUtils'
], function (objectUtils) {
function ActionDialogDecorator(mct, actionService) {
this.mct = mct;

View File

@ -29,7 +29,6 @@ define([
'./capabilities/APICapabilityDecorator',
'./policies/AdaptedViewPolicy',
'./runs/AlternateCompositionInitializer',
'./runs/TimeSettingsURLHandler',
'./runs/TypeDeprecationChecker',
'./runs/LegacyTelemetryProvider',
'./runs/RegisterLegacyTypes',
@ -46,7 +45,6 @@ define([
APICapabilityDecorator,
AdaptedViewPolicy,
AlternateCompositionInitializer,
TimeSettingsURLHandler,
TypeDeprecationChecker,
LegacyTelemetryProvider,
RegisterLegacyTypes,
@ -134,16 +132,6 @@ define([
implementation: AlternateCompositionInitializer,
depends: ["openmct"]
},
{
implementation: function (openmct, $location, $rootScope) {
return new TimeSettingsURLHandler(
openmct.time,
$location,
$rootScope
);
},
depends: ["openmct", "$location", "$rootScope"]
},
{
implementation: LegacyTelemetryProvider,
depends: [

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['../../api/objects/object-utils'], function (objectUtils) {
define(['objectUtils'], function (objectUtils) {
function AdapterCapability(domainObject) {
this.domainObject = domainObject;
}

View File

@ -24,7 +24,7 @@
* Module defining AlternateCompositionCapability. Created by vwoeltje on 11/7/14.
*/
define([
'../../api/objects/object-utils',
'objectUtils',
'../../../platform/core/src/capabilities/ContextualDomainObject'
], function (objectUtils, ContextualDomainObject) {
function AlternateCompositionCapability($injector, domainObject) {

View File

@ -31,6 +31,7 @@ define([
var capability = viewConstructor(domainObject);
var oldInvoke = capability.invoke.bind(capability);
/* eslint-disable you-dont-need-lodash-underscore/map */
capability.invoke = function () {
var availableViews = oldInvoke();
var newDomainObject = capability
@ -52,6 +53,8 @@ define([
.map('view')
.value();
};
/* eslint-enable you-dont-need-lodash-underscore/map */
return capability;
};
}

View File

@ -22,7 +22,7 @@
define([
'../capabilities/AlternateCompositionCapability',
'../../api/objects/object-utils'
'objectUtils'
], function (
AlternateCompositionCapability,
objectUtils

View File

@ -21,7 +21,7 @@
*****************************************************************************/
define([
'../../api/objects/object-utils'
'objectUtils'
], function (
utils
) {

View File

@ -1,150 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash'
], function (
_
) {
// Parameter names in query string
var SEARCH = {
MODE: 'tc.mode',
TIME_SYSTEM: 'tc.timeSystem',
START_BOUND: 'tc.startBound',
END_BOUND: 'tc.endBound',
START_DELTA: 'tc.startDelta',
END_DELTA: 'tc.endDelta'
};
var TIME_EVENTS = ['bounds', 'timeSystem', 'clock', 'clockOffsets'];
// Used to shorthand calls to $location, which clears null parameters
var NULL_PARAMETERS = { key: null, start: null, end: null };
/**
* Communicates settings from the URL to the time API,
* and vice versa.
*/
function TimeSettingsURLHandler(time, $location, $rootScope) {
this.time = time;
this.$location = $location;
$rootScope.$on('$locationChangeSuccess', this.updateTime.bind(this));
TIME_EVENTS.forEach(function (event) {
this.time.on(event, this.updateQueryParams.bind(this));
}, this);
this.updateTime(); // Initialize
}
TimeSettingsURLHandler.prototype.updateQueryParams = function () {
var clock = this.time.clock();
var fixed = !clock;
var mode = fixed ? 'fixed' : clock.key;
var timeSystem = this.time.timeSystem() || NULL_PARAMETERS;
var bounds = fixed ? this.time.bounds() : NULL_PARAMETERS;
var deltas = fixed ? NULL_PARAMETERS : this.time.clockOffsets();
bounds = bounds || NULL_PARAMETERS;
deltas = deltas || NULL_PARAMETERS;
if (deltas.start) {
deltas = { start: -deltas.start, end: deltas.end };
}
this.$location.search(SEARCH.MODE, mode);
this.$location.search(SEARCH.TIME_SYSTEM, timeSystem.key);
this.$location.search(SEARCH.START_BOUND, bounds.start);
this.$location.search(SEARCH.END_BOUND, bounds.end);
this.$location.search(SEARCH.START_DELTA, deltas.start);
this.$location.search(SEARCH.END_DELTA, deltas.end);
};
TimeSettingsURLHandler.prototype.parseQueryParams = function () {
var searchParams = _.pick(this.$location.search(), _.values(SEARCH));
var parsedParams = {
clock: searchParams[SEARCH.MODE],
timeSystem: searchParams[SEARCH.TIME_SYSTEM]
};
if (!isNaN(parseInt(searchParams[SEARCH.START_DELTA], 0xA)) &&
!isNaN(parseInt(searchParams[SEARCH.END_DELTA], 0xA))) {
parsedParams.clockOffsets = {
start: -searchParams[SEARCH.START_DELTA],
end: +searchParams[SEARCH.END_DELTA]
};
}
if (!isNaN(parseInt(searchParams[SEARCH.START_BOUND], 0xA)) &&
!isNaN(parseInt(searchParams[SEARCH.END_BOUND], 0xA))) {
parsedParams.bounds = {
start: +searchParams[SEARCH.START_BOUND],
end: +searchParams[SEARCH.END_BOUND]
};
}
return parsedParams;
};
TimeSettingsURLHandler.prototype.updateTime = function () {
var params = this.parseQueryParams();
if (_.isEqual(params, this.last)) {
return; // Do nothing;
}
this.last = params;
if (!params.timeSystem) {
this.updateQueryParams();
} else if (params.clock === 'fixed' && params.bounds) {
if (!this.time.timeSystem() ||
this.time.timeSystem().key !== params.timeSystem) {
this.time.timeSystem(
params.timeSystem,
params.bounds
);
} else if (!_.isEqual(this.time.bounds(), params.bounds)) {
this.time.bounds(params.bounds);
}
if (this.time.clock()) {
this.time.stopClock();
}
} else if (params.clockOffsets) {
if (params.clock === 'fixed') {
this.time.stopClock();
return;
}
if (!this.time.clock() ||
this.time.clock().key !== params.clock) {
this.time.clock(params.clock, params.clockOffsets);
} else if (!_.isEqual(this.time.clockOffsets(), params.clockOffsets)) {
this.time.clockOffsets(params.clockOffsets);
}
if (!this.time.timeSystem() ||
this.time.timeSystem().key !== params.timeSystem) {
this.time.timeSystem(params.timeSystem);
}
} else {
// Neither found, update from timeSystem.
this.updateQueryParams();
}
};
return TimeSettingsURLHandler;
});

View File

@ -1,576 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./TimeSettingsURLHandler',
'../../api/time/TimeAPI'
], function (
TimeSettingsURLHandler,
TimeAPI
) {
describe("TimeSettingsURLHandler", function () {
var time;
var $location;
var $rootScope;
var search;
var handler; // eslint-disable-line
var clockA;
var clockB;
var timeSystemA;
var timeSystemB;
var boundsA;
var boundsB;
var offsetsA;
var offsetsB;
var initialize;
var triggerLocationChange;
beforeEach(function () {
clockA = jasmine.createSpyObj('clockA', ['on', 'off']);
clockA.key = 'clockA';
clockA.currentValue = function () {
return 1000;
};
clockB = jasmine.createSpyObj('clockB', ['on', 'off']);
clockB.key = 'clockB';
clockB.currentValue = function () {
return 2000;
};
timeSystemA = {key: 'timeSystemA'};
timeSystemB = {key: 'timeSystemB'};
boundsA = {
start: 10,
end: 20
};
boundsB = {
start: 120,
end: 360
};
offsetsA = {
start: -100,
end: 0
};
offsetsB = {
start: -50,
end: 50
};
time = new TimeAPI();
[
'on',
'bounds',
'clockOffsets',
'timeSystem',
'clock',
'stopClock'
].forEach(function (method) {
spyOn(time, method).and.callThrough();
});
time.addTimeSystem(timeSystemA);
time.addTimeSystem(timeSystemB);
time.addClock(clockA);
time.addClock(clockB);
$location = jasmine.createSpyObj('$location', [
'search'
]);
$rootScope = jasmine.createSpyObj('$rootScope', [
'$on'
]);
search = {};
$location.search.and.callFake(function (key, value) {
if (arguments.length === 0) {
return search;
}
if (value === null) {
delete search[key];
} else {
search[key] = String(value);
}
return this;
});
expect(time.timeSystem()).toBeUndefined();
expect(time.bounds()).toEqual({});
expect(time.clockOffsets()).toBeUndefined();
expect(time.clock()).toBeUndefined();
initialize = function () {
handler = new TimeSettingsURLHandler(
time,
$location,
$rootScope
);
expect($rootScope.$on).toHaveBeenCalledWith(
'$locationChangeSuccess',
jasmine.any(Function)
);
triggerLocationChange = $rootScope.$on.calls.mostRecent().args[1];
};
});
it("initializes with missing time system", function () {
// This handles an odd transitory case where a url does not include
// a timeSystem. It's generally only experienced by those who
// based their code on the tutorial before it specified a time
// system.
search['tc.mode'] = 'clockA';
search['tc.timeSystem'] = undefined;
search['tc.startDelta'] = '123';
search['tc.endDelta'] = '456';
// We don't specify behavior right now other than "don't break."
expect(initialize).not.toThrow();
});
it("can initalize fixed mode from location", function () {
search['tc.mode'] = 'fixed';
search['tc.timeSystem'] = 'timeSystemA';
search['tc.startBound'] = '123';
search['tc.endBound'] = '456';
initialize();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemA',
{
start: 123,
end: 456
}
);
});
it("can initialize clock mode from location", function () {
search['tc.mode'] = 'clockA';
search['tc.timeSystem'] = 'timeSystemA';
search['tc.startDelta'] = '123';
search['tc.endDelta'] = '456';
initialize();
expect(time.clock).toHaveBeenCalledWith(
'clockA',
{
start: -123,
end: 456
}
);
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemA'
);
});
it("can initialize fixed mode from time API", function () {
time.timeSystem(timeSystemA.key, boundsA);
initialize();
expect($location.search)
.toHaveBeenCalledWith('tc.mode', 'fixed');
expect($location.search)
.toHaveBeenCalledWith('tc.timeSystem', 'timeSystemA');
expect($location.search)
.toHaveBeenCalledWith('tc.startBound', 10);
expect($location.search)
.toHaveBeenCalledWith('tc.endBound', 20);
expect($location.search)
.toHaveBeenCalledWith('tc.startDelta', null);
expect($location.search)
.toHaveBeenCalledWith('tc.endDelta', null);
});
it("can initialize clock mode from time API", function () {
time.clock(clockA.key, offsetsA);
time.timeSystem(timeSystemA.key);
initialize();
expect($location.search)
.toHaveBeenCalledWith('tc.mode', 'clockA');
expect($location.search)
.toHaveBeenCalledWith('tc.timeSystem', 'timeSystemA');
expect($location.search)
.toHaveBeenCalledWith('tc.startBound', null);
expect($location.search)
.toHaveBeenCalledWith('tc.endBound', null);
expect($location.search)
.toHaveBeenCalledWith('tc.startDelta', 100);
expect($location.search)
.toHaveBeenCalledWith('tc.endDelta', 0);
});
describe('location changes in fixed mode', function () {
beforeEach(function () {
time.timeSystem(timeSystemA.key, boundsA);
initialize();
time.timeSystem.calls.reset();
time.bounds.calls.reset();
time.clock.calls.reset();
time.stopClock.calls.reset();
});
it("does not change on spurious location change", function () {
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
'timeSystemA',
jasmine.any(Object)
);
expect(time.bounds).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.stopClock).not.toHaveBeenCalled();
});
it("updates timeSystem changes", function () {
search['tc.timeSystem'] = 'timeSystemB';
triggerLocationChange();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB',
{
start: 10,
end: 20
}
);
});
it("updates bounds changes", function () {
search['tc.startBound'] = '100';
search['tc.endBound'] = '200';
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.anything(), jasmine.anything()
);
expect(time.bounds).toHaveBeenCalledWith({
start: 100,
end: 200
});
search['tc.endBound'] = '300';
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.anything(), jasmine.anything()
);
expect(time.bounds).toHaveBeenCalledWith({
start: 100,
end: 300
});
});
it("updates clock mode w/o timeSystem change", function () {
search['tc.mode'] = 'clockA';
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
delete search['tc.endBound'];
delete search['tc.startBound'];
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockA',
{
start: -50,
end: 50
}
);
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.anything(), jasmine.anything()
);
});
it("updates clock mode and timeSystem", function () {
search['tc.mode'] = 'clockA';
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
search['tc.timeSystem'] = 'timeSystemB';
delete search['tc.endBound'];
delete search['tc.startBound'];
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockA',
{
start: -50,
end: 50
}
);
expect(time.timeSystem).toHaveBeenCalledWith('timeSystemB');
});
});
describe('location changes in clock mode', function () {
beforeEach(function () {
time.clock(clockA.key, offsetsA);
time.timeSystem(timeSystemA.key);
initialize();
time.timeSystem.calls.reset();
time.bounds.calls.reset();
time.clock.calls.reset();
time.clockOffsets.calls.reset();
time.stopClock.calls.reset();
});
it("does not change on spurious location change", function () {
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
'timeSystemA',
jasmine.any(Object)
);
expect(time.clockOffsets).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.clock).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.bounds).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("changes time system", function () {
search['tc.timeSystem'] = 'timeSystemB';
triggerLocationChange();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB'
);
expect(time.clockOffsets).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.clock).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.stopClock).not.toHaveBeenCalled();
expect(time.bounds).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("changes offsets", function () {
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
'timeSystemA',
jasmine.any(Object)
);
expect(time.clockOffsets).toHaveBeenCalledWith(
{
start: -50,
end: 50
}
);
expect(time.clock).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("updates to fixed w/o timeSystem change", function () {
search['tc.mode'] = 'fixed';
search['tc.startBound'] = '234';
search['tc.endBound'] = '567';
delete search['tc.endDelta'];
delete search['tc.startDelta'];
triggerLocationChange();
expect(time.stopClock).toHaveBeenCalled();
expect(time.bounds).toHaveBeenCalledWith({
start: 234,
end: 567
});
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.anything(), jasmine.anything()
);
});
it("updates fixed and timeSystem", function () {
search['tc.mode'] = 'fixed';
search['tc.startBound'] = '234';
search['tc.endBound'] = '567';
search['tc.timeSystem'] = 'timeSystemB';
delete search['tc.endDelta'];
delete search['tc.startDelta'];
triggerLocationChange();
expect(time.stopClock).toHaveBeenCalled();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB',
{
start: 234,
end: 567
}
);
});
it("updates clock", function () {
search['tc.mode'] = 'clockB';
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockB',
{
start: -100,
end: 0
}
);
expect(time.timeSystem).not.toHaveBeenCalledWith(jasmine.anything());
});
it("updates clock and timeSystem", function () {
search['tc.mode'] = 'clockB';
search['tc.timeSystem'] = 'timeSystemB';
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockB',
{
start: -100,
end: 0
}
);
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB'
);
});
it("updates clock and timeSystem and offsets", function () {
search['tc.mode'] = 'clockB';
search['tc.timeSystem'] = 'timeSystemB';
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockB',
{
start: -50,
end: 50
}
);
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB'
);
});
it("stops the clock", function () {
// this is a robustness test, unsure if desired, requires
// user to be manually editing location strings.
search['tc.mode'] = 'fixed';
triggerLocationChange();
expect(time.stopClock).toHaveBeenCalled();
});
});
describe("location updates from time API in fixed", function () {
beforeEach(function () {
time.timeSystem(timeSystemA.key, boundsA);
initialize();
});
it("updates on bounds change", function () {
time.bounds(boundsB);
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '120',
'tc.endBound': '360',
'tc.timeSystem': 'timeSystemA'
});
});
it("updates on timeSystem change", function () {
time.timeSystem(timeSystemB, boundsA);
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '10',
'tc.endBound': '20',
'tc.timeSystem': 'timeSystemB'
});
time.timeSystem(timeSystemA, boundsB);
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '120',
'tc.endBound': '360',
'tc.timeSystem': 'timeSystemA'
});
});
it("Updates to clock", function () {
time.clock(clockA, offsetsA);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '100',
'tc.endDelta': '0',
'tc.timeSystem': 'timeSystemA'
});
});
});
describe("location updates from time API in fixed", function () {
beforeEach(function () {
time.clock(clockA.key, offsetsA);
time.timeSystem(timeSystemA.key);
initialize();
});
it("updates offsets", function () {
time.clockOffsets(offsetsB);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '50',
'tc.endDelta': '50',
'tc.timeSystem': 'timeSystemA'
});
});
it("updates clocks", function () {
time.clock(clockB, offsetsA);
expect(search).toEqual({
'tc.mode': 'clockB',
'tc.startDelta': '100',
'tc.endDelta': '0',
'tc.timeSystem': 'timeSystemA'
});
time.clock(clockA, offsetsB);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '50',
'tc.endDelta': '50',
'tc.timeSystem': 'timeSystemA'
});
});
it("updates timesystems", function () {
time.timeSystem(timeSystemB);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '100',
'tc.endDelta': '0',
'tc.timeSystem': 'timeSystemB'
});
});
it("stops the clock", function () {
time.stopClock();
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '900',
'tc.endBound': '1000',
'tc.timeSystem': 'timeSystemA'
});
});
});
});
});

View File

@ -21,7 +21,7 @@
*****************************************************************************/
define([
'../../api/objects/object-utils'
'objectUtils'
], function (
utils
) {

View File

@ -21,7 +21,7 @@
*****************************************************************************/
define([
'../../api/objects/object-utils'
'objectUtils'
], function (
objectUtils
) {

View File

@ -1,7 +1,7 @@
define([
'./LegacyViewProvider',
'./TypeInspectorViewProvider',
'../../api/objects/object-utils'
'objectUtils'
], function (
LegacyViewProvider,
TypeInspectorViewProvider,

View File

@ -70,7 +70,7 @@ define([
* @memberof module:openmct.CompositionAPI#
*/
CompositionAPI.prototype.get = function (domainObject) {
var provider = _.find(this.registry, function (p) {
var provider = this.registry.find(p => {
return p.appliesTo(domainObject);
});

View File

@ -122,7 +122,7 @@ define([
throw new Error('Event not supported by composition: ' + event);
}
var index = _.findIndex(this.listeners[event], function (l) {
var index = this.listeners[event].findIndex(l => {
return l.callback === callback && l.context === context;
});

View File

@ -22,7 +22,7 @@
define([
'lodash',
'../objects/object-utils'
'objectUtils'
], function (
_,
objectUtils
@ -143,7 +143,7 @@ define([
var keyString = objectUtils.makeKeyString(domainObject.identifier);
var objectListeners = this.listeningTo[keyString];
var index = _.findIndex(objectListeners[event], function (l) {
var index = objectListeners[event].findIndex(l => {
return l.callback === callback && l.context === context;
});
@ -196,8 +196,8 @@ define([
* @private
*/
DefaultCompositionProvider.prototype.includes = function (parent, childId) {
return parent.composition.findIndex(composee =>
this.publicAPI.objects.areIdsEqual(composee, childId)) !== -1;
return parent.composition.some(composee =>
this.publicAPI.objects.areIdsEqual(composee, childId));
};
DefaultCompositionProvider.prototype.reorder = function (domainObject, oldIndex, newIndex) {

View File

@ -128,6 +128,11 @@ export default class NotificationAPI extends EventEmitter {
return this._notify(notificationModel);
}
dismissAllNotifications() {
this.notifications = [];
this.emit('dismiss-all');
}
/**
* Minimize a notification. The notification will still be available
* from the notification list. Typically notifications with a

View File

@ -21,7 +21,7 @@
*****************************************************************************/
define([
'./object-utils.js',
'objectUtils',
'lodash'
], function (
utils,

View File

@ -22,7 +22,7 @@
define([
'lodash',
'./object-utils',
'objectUtils',
'./MutableObject',
'./RootRegistry',
'./RootObjectProvider',

View File

@ -43,7 +43,7 @@ define([
}
RootRegistry.prototype.addRoot = function (key) {
if (isKey(key) || (_.isArray(key) && _.every(key, isKey))) {
if (isKey(key) || (Array.isArray(key) && key.every(isKey))) {
this.providers.push(function () {
return key;
});

View File

@ -1,5 +1,5 @@
define([
'../object-utils'
'objectUtils'
], function (
objectUtils
) {

View File

@ -85,9 +85,9 @@ define([
value: +e.value
};
}), 'e.value');
valueMetadata.values = _.pluck(valueMetadata.enumerations, 'value');
valueMetadata.max = _.max(valueMetadata.values);
valueMetadata.min = _.min(valueMetadata.values);
valueMetadata.values = valueMetadata.enumerations.map(e => e.value);
valueMetadata.max = Math.max(valueMetadata.values);
valueMetadata.min = Math.min(valueMetadata.values);
}
valueMetadatas.push(valueMetadata);
@ -103,7 +103,7 @@ define([
var metadata = domainObject.telemetry || {};
if (this.typeHasTelemetry(domainObject)) {
var typeMetadata = this.typeService.getType(domainObject.type).typeDef.telemetry;
_.extend(metadata, typeMetadata);
Object.assign(metadata, typeMetadata);
if (!metadata.values) {
metadata.values = valueMetadatasFromOldFormat(metadata);
}

View File

@ -24,7 +24,7 @@ define([
'./TelemetryMetadataManager',
'./TelemetryValueFormatter',
'./DefaultMetadataProvider',
'../objects/object-utils',
'objectUtils',
'lodash'
], function (
TelemetryMetadataManager,
@ -370,7 +370,7 @@ define([
TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) {
var options = metadatas.map(function (metadata) {
var values = metadata.valuesForHints(hints);
return _.indexBy(values, 'key');
return _.keyBy(values, 'key');
}).reduce(function (a, b) {
var results = {};
Object.keys(a).forEach(function (key) {
@ -383,7 +383,7 @@ define([
var sortKeys = hints.map(function (h) {
return 'hints.' + h;
});
return _.sortByAll(options, sortKeys);
return _.sortBy(options, sortKeys);
};
/**

View File

@ -57,13 +57,13 @@ define([
if (valueMetadata.format === 'enum') {
if (!valueMetadata.values) {
valueMetadata.values = _.pluck(valueMetadata.enumerations, 'value');
valueMetadata.values = valueMetadata.enumerations.map(e => e.value);
}
if (!valueMetadata.hasOwnProperty('max')) {
valueMetadata.max = _.max(valueMetadata.values) + 1;
valueMetadata.max = Math.max(valueMetadata.values) + 1;
}
if (!valueMetadata.hasOwnProperty('min')) {
valueMetadata.min = _.min(valueMetadata.values) - 1;
valueMetadata.min = Math.min(valueMetadata.values) - 1;
}
}
@ -121,7 +121,7 @@ define([
return metadata.hints[hint];
}
});
return _.sortByAll(matchingMetadata, ...iteratees);
return _.sortBy(matchingMetadata, ...iteratees);
};
TelemetryMetadataManager.prototype.getFilterableValues = function () {

View File

@ -0,0 +1,39 @@
/*****************************************************************************
* 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 default class LADTableCompositionPolicy {
constructor(openmct) {
this.openmct = openmct;
return this.allow.bind(this);
}
allow(parent, child) {
if(parent.type === 'LadTable') {
return this.openmct.telemetry.isTelemetryObject(child);
} else if(parent.type === 'LadTableSet') {
return child.type === 'LadTable';
}
return true;
}
}

View File

@ -19,53 +19,46 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import LadTableSet from './components/LadTableSet.vue';
import Vue from 'vue';
define([
'./components/LadTableSet.vue',
'vue'
], function (
LadTableSet,
Vue
) {
function LADTableSetViewProvider(openmct) {
return {
key: 'LadTableSet',
name: 'LAD Table Set',
cssClass: 'icon-tabular-lad-set',
canView: function (domainObject) {
return domainObject.type === 'LadTableSet';
},
canEdit: function (domainObject) {
return domainObject.type === 'LadTableSet';
},
view: function (domainObject, objectPath) {
let component;
export default function LADTableSetViewProvider(openmct) {
return {
key: 'LadTableSet',
name: 'LAD Table Set',
cssClass: 'icon-tabular-lad-set',
canView: function (domainObject) {
return domainObject.type === 'LadTableSet';
},
canEdit: function (domainObject) {
return domainObject.type === 'LadTableSet';
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
LadTableSet: LadTableSet.default
},
provide: {
openmct,
domainObject,
objectPath
},
template: '<lad-table-set></lad-table-set>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}
return LADTableSetViewProvider;
});
return {
show: function (element) {
component = new Vue({
el: element,
components: {
LadTableSet: LadTableSet
},
provide: {
openmct,
domainObject,
objectPath
},
template: '<lad-table-set></lad-table-set>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}

View File

@ -19,53 +19,46 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import LadTable from './components/LADTable.vue';
import Vue from 'vue';
define([
'./components/LADTable.vue',
'vue'
], function (
LadTableComponent,
Vue
) {
function LADTableViewProvider(openmct) {
return {
key: 'LadTable',
name: 'LAD Table',
cssClass: 'icon-tabular-lad',
canView: function (domainObject) {
return domainObject.type === 'LadTable';
},
canEdit: function (domainObject) {
return domainObject.type === 'LadTable';
},
view: function (domainObject, objectPath) {
let component;
export default function LADTableViewProvider(openmct) {
return {
key: 'LadTable',
name: 'LAD Table',
cssClass: 'icon-tabular-lad',
canView: function (domainObject) {
return domainObject.type === 'LadTable';
},
canEdit: function (domainObject) {
return domainObject.type === 'LadTable';
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
LadTableComponent: LadTableComponent.default
},
provide: {
openmct,
domainObject,
objectPath
},
template: '<lad-table-component></lad-table-component>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}
return LADTableViewProvider;
});
return {
show: function (element) {
component = new Vue({
el: element,
components: {
LadTableComponent: LadTable
},
provide: {
openmct,
domainObject,
objectPath
},
template: '<lad-table-component></lad-table-component>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}

View File

@ -1,6 +1,6 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@ -22,12 +22,16 @@
*****************************************************************************/
<template>
<tr @contextmenu.prevent="showContextMenu">
<td>{{ name }}</td>
<td>{{ timestamp }}</td>
<td :class="valueClass">
{{ value }}
</td>
<tr
class="js-lad-table__body__row"
@contextmenu.prevent="showContextMenu"
>
<td class="js-first-data">{{ name }}</td>
<td class="js-second-data">{{ formattedTimestamp }}</td>
<td
class="js-third-data"
:class="valueClass"
>{{ value }}</td>
</tr>
</template>
@ -52,16 +56,22 @@ export default {
return {
name: this.domainObject.name,
timestamp: '---',
timestamp: undefined,
value: '---',
valueClass: '',
currentObjectPath
}
},
computed: {
formattedTimestamp() {
return this.timestamp !== undefined ? this.formats[this.timestampKey].format(this.timestamp) : '---';
}
},
mounted() {
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.bounds = this.openmct.time.bounds();
this.limitEvaluator = this.openmct
.telemetry
@ -76,6 +86,7 @@ export default {
);
this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.openmct.time.on('bounds', this.updateBounds);
this.timestampKey = this.openmct.time.timeSystem().key;
@ -89,43 +100,63 @@ export default {
.telemetry
.subscribe(this.domainObject, this.updateValues);
this.openmct
.telemetry
.request(this.domainObject, {strategy: 'latest'})
.then((array) => this.updateValues(array[array.length - 1]));
this.requestHistory();
},
destroyed() {
this.stopWatchingMutation();
this.unsubscribe();
this.openmct.off('timeSystem', this.updateTimeSystem);
this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.openmct.time.off('bounds', this.updateBounds);
},
methods: {
updateValues(datum) {
this.timestamp = this.formats[this.timestampKey].format(datum);
this.value = this.formats[this.valueKey].format(datum);
let newTimestamp = this.formats[this.timestampKey].parse(datum),
limit;
var limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
if (limit) {
this.valueClass = limit.cssClass;
} else {
this.valueClass = '';
if(this.shouldUpdate(newTimestamp)) {
this.timestamp = this.formats[this.timestampKey].parse(datum);
this.value = this.formats[this.valueKey].format(datum);
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
if (limit) {
this.valueClass = limit.cssClass;
} else {
this.valueClass = '';
}
}
},
shouldUpdate(newTimestamp) {
return (this.timestamp === undefined) ||
(this.inBounds(newTimestamp) &&
newTimestamp > this.timestamp);
},
requestHistory() {
this.openmct
.telemetry
.request(this.domainObject, {
start: this.bounds.start,
end: this.bounds.end,
size: 1,
strategy: 'latest'
})
.then((array) => this.updateValues(array[array.length - 1]));
},
updateName(name) {
this.name = name;
},
updateBounds(bounds, isTick) {
this.bounds = bounds;
if(!isTick) {
this.requestHistory();
}
},
inBounds(timestamp) {
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
},
updateTimeSystem(timeSystem) {
this.value = '---';
this.timestamp = '---';
this.valueClass = '';
this.timestampKey = timeSystem.key;
this.openmct
.telemetry
.request(this.domainObject, {strategy: 'latest'})
.then((array) => this.updateValues(array[array.length - 1]));
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);

View File

@ -75,7 +75,7 @@ export default {
this.items.push(item);
},
removeItem(identifier) {
let index = _.findIndex(this.items, (item) => this.openmct.objects.makeKeyString(identifier) === item.key);
let index = this.items.findIndex(item => this.openmct.objects.makeKeyString(identifier) === item.key);
this.items.splice(index, 1);
},
@ -88,4 +88,3 @@ export default {
}
}
</script>

View File

@ -35,7 +35,7 @@
>
<tr
:key="primary.key"
class="c-table__group-header"
class="c-table__group-header js-lad-table-set__table-headers"
>
<td colspan="10">
{{ primary.domainObject.name }}
@ -102,7 +102,7 @@ export default {
this.compositions.push({composition, addCallback, removeCallback});
},
removePrimary(identifier) {
let index = _.findIndex(this.primaryTelemetryObjects, (primary) => this.openmct.objects.makeKeyString(identifier) === primary.key),
let index = this.primaryTelemetryObjects.findIndex(primary => this.openmct.objects.makeKeyString(identifier) === primary.key),
primary = this.primaryTelemetryObjects[index];
this.$set(this.secondaryTelemetryObjects, primary.key, undefined);
@ -130,7 +130,7 @@ export default {
removeSecondary(primary) {
return (identifier) => {
let array = this.secondaryTelemetryObjects[primary.key],
index = _.findIndex(array, (secondary) => this.openmct.objects.makeKeyString(identifier) === secondary.key);
index = array.findIndex(secondary => this.openmct.objects.makeKeyString(identifier) === secondary.key);
array.splice(index, 1);

View File

@ -19,38 +19,36 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import LADTableViewProvider from './LADTableViewProvider';
import LADTableSetViewProvider from './LADTableSetViewProvider';
import LADTableCompositionPolicy from './LADTableCompositionPolicy';
define([
'./LADTableViewProvider',
'./LADTableSetViewProvider'
], function (
LADTableViewProvider,
LADTableSetViewProvider
) {
return function plugin() {
return function install(openmct) {
openmct.objectViews.addProvider(new LADTableViewProvider(openmct));
openmct.objectViews.addProvider(new LADTableSetViewProvider(openmct));
export default function plugin() {
return function install(openmct) {
openmct.types.addType('LadTable', {
name: "LAD Table",
creatable: true,
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
cssClass: 'icon-tabular-lad',
initialize(domainObject) {
domainObject.composition = [];
}
});
openmct.objectViews.addProvider(new LADTableViewProvider(openmct));
openmct.objectViews.addProvider(new LADTableSetViewProvider(openmct));
openmct.types.addType('LadTableSet', {
name: "LAD Table Set",
creatable: true,
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
cssClass: 'icon-tabular-lad-set',
initialize(domainObject) {
domainObject.composition = [];
}
});
};
openmct.types.addType('LadTable', {
name: "LAD Table",
creatable: true,
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
cssClass: 'icon-tabular-lad',
initialize(domainObject) {
domainObject.composition = [];
}
});
openmct.types.addType('LadTableSet', {
name: "LAD Table Set",
creatable: true,
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
cssClass: 'icon-tabular-lad-set',
initialize(domainObject) {
domainObject.composition = [];
}
});
openmct.composition.addPolicy(new LADTableCompositionPolicy(openmct));
};
});
}

View File

@ -0,0 +1,365 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import LadPlugin from './plugin.js';
import Vue from 'vue';
import {
createOpenMct,
getMockObjects,
getMockTelemetry,
getLatestTelemetry,
resetApplicationState
} from 'utils/testing';
const TABLE_BODY_ROWS = '.js-lad-table__body__row';
const TABLE_BODY_FIRST_ROW = TABLE_BODY_ROWS + ':first-child';
const TABLE_BODY_FIRST_ROW_FIRST_DATA = TABLE_BODY_FIRST_ROW + ' .js-first-data';
const TABLE_BODY_FIRST_ROW_SECOND_DATA = TABLE_BODY_FIRST_ROW + ' .js-second-data';
const TABLE_BODY_FIRST_ROW_THIRD_DATA = TABLE_BODY_FIRST_ROW + ' .js-third-data';
const LAD_SET_TABLE_HEADERS = '.js-lad-table-set__table-headers';
function utcTimeFormat(value) {
return new Date(value).toISOString().replace('T', ' ')
}
describe("The LAD Table", () => {
const ladTableKey = 'LadTable';
let openmct,
ladPlugin,
parent,
child,
telemetryCount = 3,
timeFormat = 'utc',
mockTelemetry = getMockTelemetry({ count: telemetryCount, format: timeFormat }),
mockObj = getMockObjects({
objectKeyStrings: ['ladTable', 'telemetry'],
format: timeFormat
}),
bounds = {
start: 0,
end: 4
};
// add telemetry object as composition in lad table
mockObj.ladTable.composition.push(mockObj.telemetry.identifier);
// this setups up the app
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
parent = document.createElement('div');
child = document.createElement('div');
parent.appendChild(child);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
ladPlugin = new LadPlugin();
openmct.install(ladPlugin);
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
openmct.time.bounds({ start: bounds.start, end: bounds.end });
openmct.on('start', done);
openmct.startHeadless(appHolder);
});
afterEach(() => {
resetApplicationState(openmct);
});
it("should provide a table view only for lad table objects", () => {
let applicableViews = openmct.objectViews.get(mockObj.ladTable),
ladTableView = applicableViews.find(
(viewProvider) => viewProvider.key === ladTableKey
);
expect(applicableViews.length).toEqual(1);
expect(ladTableView).toBeDefined();
});
describe('composition', () => {
let ladTableCompositionCollection;
beforeEach(() => {
ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable);
ladTableCompositionCollection.load();
});
it("should accept telemetry producing objects", () => {
expect(() => {
ladTableCompositionCollection.add(mockObj.telemetry);
}).not.toThrow();
});
it("should reject non-telemtry producing objects", () => {
expect(()=> {
ladTableCompositionCollection.add(mockObj.ladTable);
}).toThrow();
});
});
describe("table view", () => {
let applicableViews,
ladTableViewProvider,
ladTableView,
anotherTelemetryObj = getMockObjects({
objectKeyStrings: ['telemetry'],
overwrite: {
telemetry: {
name: "New Telemetry Object",
identifier: { namespace: "", key: "another-telemetry-object" }
}
}
}).telemetry;
// add another telemetry object as composition in lad table to test multi rows
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
beforeEach(async () => {
let telemetryRequestResolve,
telemetryObjectResolve,
anotherTelemetryObjectResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
}),
telemetryObjectPromise = new Promise((resolve) => {
telemetryObjectResolve = resolve;
}),
anotherTelemetryObjectPromise = new Promise((resolve) => {
anotherTelemetryObjectResolve = resolve;
})
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise;
});
openmct.objects.get.and.callFake((obj) => {
if(obj.key === 'telemetry-object') {
telemetryObjectResolve(mockObj.telemetry);
return telemetryObjectPromise;
} else {
anotherTelemetryObjectResolve(anotherTelemetryObj);
return anotherTelemetryObjectPromise;
}
});
openmct.time.bounds({ start: bounds.start, end: bounds.end });
applicableViews = openmct.objectViews.get(mockObj.ladTable);
ladTableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableKey);
ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]);
ladTableView.show(child, true);
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick();
return;
});
it("should show one row per object in the composition", () => {
const rowCount = parent.querySelectorAll(TABLE_BODY_ROWS).length;
expect(rowCount).toBe(mockObj.ladTable.composition.length);
});
it("should show the most recent datum from the telemetry producing object", async () => {
const latestDatum = getLatestTelemetry(mockTelemetry, { timeFormat });
const expectedDate = utcTimeFormat(latestDatum[timeFormat]);
await Vue.nextTick();
const latestDate = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText;
expect(latestDate).toBe(expectedDate);
});
it("should show the name provided for the the telemetry producing object", () => {
const rowName = parent.querySelector(TABLE_BODY_FIRST_ROW_FIRST_DATA).innerText,
expectedName = mockObj.telemetry.name;
expect(rowName).toBe(expectedName);
});
it("should show the correct values for the datum based on domain and range hints", async () => {
const range = mockObj.telemetry.telemetry.values.find((val) => {
return val.hints && val.hints.range !== undefined;
}).key;
const domain = mockObj.telemetry.telemetry.values.find((val) => {
return val.hints && val.hints.domain !== undefined;
}).key;
const mostRecentTelemetry = getLatestTelemetry(mockTelemetry, { timeFormat });
const rangeValue = mostRecentTelemetry[range];
const domainValue = utcTimeFormat(mostRecentTelemetry[domain]);
await Vue.nextTick();
const actualDomainValue = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText;
const actualRangeValue = parent.querySelector(TABLE_BODY_FIRST_ROW_THIRD_DATA).innerText;
expect(actualRangeValue).toBe(rangeValue);
expect(actualDomainValue).toBe(domainValue);
});
});
});
describe("The LAD Table Set", () => {
const ladTableSetKey = 'LadTableSet';
let openmct,
ladPlugin,
parent,
child,
telemetryCount = 3,
timeFormat = 'utc',
mockTelemetry = getMockTelemetry({ count: telemetryCount, format: timeFormat }),
mockObj = getMockObjects({
objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry']
}),
bounds = {
start: 0,
end: 4
};
// add mock telemetry to lad table and lad table to lad table set (composition)
mockObj.ladTable.composition.push(mockObj.telemetry.identifier);
mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier);
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
parent = document.createElement('div');
child = document.createElement('div');
parent.appendChild(child);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
ladPlugin = new LadPlugin();
openmct.install(ladPlugin);
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
openmct.time.bounds({ start: bounds.start, end: bounds.end });
openmct.on('start', done);
openmct.start(appHolder);
});
afterEach(() => {
resetApplicationState(openmct);
});
it("should provide a lad table set view only for lad table set objects", () => {
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet),
ladTableSetView = applicableViews.find(
(viewProvider) => viewProvider.key === ladTableSetKey
);
expect(applicableViews.length).toEqual(1);
expect(ladTableSetView).toBeDefined();
});
describe('composition', () => {
let ladTableSetCompositionCollection;
beforeEach(() => {
ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);
ladTableSetCompositionCollection.load();
});
it("should accept lad table objects", () => {
expect(() => {
ladTableSetCompositionCollection.add(mockObj.ladTable);
}).not.toThrow();
});
it("should reject non lad table objects", () => {
expect(()=> {
ladTableSetCompositionCollection.add(mockObj.telemetry);
}).toThrow();
});
});
describe("table view", () => {
let applicableViews,
ladTableSetViewProvider,
ladTableSetView,
otherObj = getMockObjects({
objectKeyStrings: ['ladTable'],
overwrite: {
ladTable: {
name: "New LAD Table Object",
identifier: { namespace: "", key: "another-lad-object" }
}
}
});
// add another lad table (with telemetry object) object to the lad table set for multi row test
otherObj.ladTable.composition.push(mockObj.telemetry.identifier);
mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier);
beforeEach(async () => {
let telemetryRequestResolve,
ladObjectResolve,
anotherLadObjectResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
}),
ladObjectPromise = new Promise((resolve) => {
ladObjectResolve = resolve;
}),
anotherLadObjectPromise = new Promise((resolve) => {
anotherLadObjectResolve = resolve;
})
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise;
});
openmct.objects.get.and.callFake((obj) => {
if(obj.key === 'lad-object') {
ladObjectResolve(mockObj.ladObject);
return ladObjectPromise;
} else if(obj.key === 'another-lad-object') {
anotherLadObjectResolve(otherObj.ladObject);
return anotherLadObjectPromise;
}
return Promise.resolve({});
});
openmct.time.bounds({ start: bounds.start, end: bounds.end });
applicableViews = openmct.objectViews.get(mockObj.ladTableSet);
ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey);
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
ladTableSetView.show(child, true);
await Promise.all([telemetryRequestPromise, ladObjectPromise, anotherLadObjectPromise]);
await Vue.nextTick();
return;
});
it("should show one row per lad table object in the composition", () => {
const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length;
expect(rowCount).toBe(mockObj.ladTableSet.composition.length);
pending();
});
});
});

View File

@ -0,0 +1,230 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
getAllSearchParams,
setAllSearchParams
} from 'utils/openmctLocation';
const TIME_EVENTS = ['bounds', 'timeSystem', 'clock', 'clockOffsets'];
const SEARCH_MODE = 'tc.mode';
const SEARCH_TIME_SYSTEM = 'tc.timeSystem';
const SEARCH_START_BOUND = 'tc.startBound';
const SEARCH_END_BOUND = 'tc.endBound';
const SEARCH_START_DELTA = 'tc.startDelta';
const SEARCH_END_DELTA = 'tc.endDelta';
const MODE_FIXED = 'fixed';
export default class URLTimeSettingsSynchronizer {
constructor(openmct) {
this.openmct = openmct;
this.isUrlUpdateInProgress = false;
this.initialize = this.initialize.bind(this);
this.destroy = this.destroy.bind(this);
this.updateTimeSettings = this.updateTimeSettings.bind(this);
this.setUrlFromTimeApi = this.setUrlFromTimeApi.bind(this);
openmct.on('start', this.initialize);
openmct.on('destroy', this.destroy);
}
initialize() {
this.updateTimeSettings();
window.addEventListener('hashchange', this.updateTimeSettings);
TIME_EVENTS.forEach(event => {
this.openmct.time.on(event, this.setUrlFromTimeApi);
});
}
destroy() {
window.removeEventListener('hashchange', this.updateTimeSettings);
this.openmct.off('start', this.initialize);
this.openmct.off('destroy', this.destroy);
TIME_EVENTS.forEach(event => {
this.openmct.time.off(event, this.setUrlFromTimeApi);
});
}
updateTimeSettings() {
// Prevent from triggering self
if (!this.isUrlUpdateInProgress) {
let timeParameters = this.parseParametersFromUrl();
if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters);
} else {
this.setUrlFromTimeApi();
}
} else {
this.isUrlUpdateInProgress = false;
}
}
parseParametersFromUrl() {
let searchParams = getAllSearchParams();
let mode = searchParams.get(SEARCH_MODE);
let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
let startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);
let endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
let bounds = {
start: startBound,
end: endBound
};
let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
let clockOffsets = {
start: 0 - startOffset,
end: endOffset
};
return {
mode,
timeSystem,
bounds,
clockOffsets
};
}
setTimeApiFromUrl(timeParameters) {
if (timeParameters.mode === 'fixed') {
if (this.openmct.time.timeSystem().key !== timeParameters.timeSystem) {
this.openmct.time.timeSystem(
timeParameters.timeSystem,
timeParameters.bounds
);
} else if (!this.areStartAndEndEqual(this.openmct.time.bounds(), timeParameters.bounds)) {
this.openmct.time.bounds(timeParameters.bounds);
}
if (this.openmct.time.clock()) {
this.openmct.time.stopClock();
}
} else {
if (!this.openmct.time.clock() ||
this.openmct.time.clock().key !== timeParameters.mode) {
this.openmct.time.clock(timeParameters.mode, timeParameters.clockOffsets);
} else if (!this.areStartAndEndEqual(this.openmct.time.clockOffsets(), timeParameters.clockOffsets)) {
this.openmct.time.clockOffsets(timeParameters.clockOffsets);
}
if (!this.openmct.time.timeSystem() ||
this.openmct.time.timeSystem().key !== timeParameters.timeSystem) {
this.openmct.time.timeSystem(timeParameters.timeSystem);
}
}
}
setUrlFromTimeApi() {
let searchParams = getAllSearchParams();
let clock = this.openmct.time.clock();
let bounds = this.openmct.time.bounds();
let clockOffsets = this.openmct.time.clockOffsets();
if (clock === undefined) {
searchParams.set(SEARCH_MODE, MODE_FIXED);
searchParams.set(SEARCH_START_BOUND, bounds.start);
searchParams.set(SEARCH_END_BOUND, bounds.end);
searchParams.delete(SEARCH_START_DELTA);
searchParams.delete(SEARCH_END_DELTA);
} else {
searchParams.set(SEARCH_MODE, clock.key);
if (clockOffsets !== undefined) {
searchParams.set(SEARCH_START_DELTA, 0 - clockOffsets.start);
searchParams.set(SEARCH_END_DELTA, clockOffsets.end);
} else {
searchParams.delete(SEARCH_START_DELTA);
searchParams.delete(SEARCH_END_DELTA);
}
searchParams.delete(SEARCH_START_BOUND);
searchParams.delete(SEARCH_END_BOUND);
}
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key);
this.isUrlUpdateInProgress = true;
setAllSearchParams(searchParams);
}
areTimeParametersValid(timeParameters) {
let isValid = false;
if (this.isModeValid(timeParameters.mode) &&
this.isTimeSystemValid(timeParameters.timeSystem)) {
if (timeParameters.mode === 'fixed') {
isValid = this.areStartAndEndValid(timeParameters.bounds);
} else {
isValid = this.areStartAndEndValid(timeParameters.clockOffsets);
}
}
return isValid;
}
areStartAndEndValid(bounds) {
return bounds !== undefined &&
bounds.start !== undefined &&
bounds.start !== null &&
bounds.end !== undefined &&
bounds.start !== null &&
!isNaN(bounds.start) &&
!isNaN(bounds.end);
}
isTimeSystemValid(timeSystem) {
let isValid = timeSystem !== undefined;
if (isValid) {
let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
isValid = timeSystemObject !== undefined;
}
return isValid;
}
isModeValid(mode) {
let isValid = false;
if (mode !== undefined &&
mode !== null) {
isValid = true;
}
if (isValid) {
if (mode.toLowerCase() === MODE_FIXED) {
isValid = true;
} else {
isValid = this.openmct.time.clocks.get(mode) !== undefined;
}
}
return isValid;
}
areStartAndEndEqual(firstBounds, secondBounds) {
return firstBounds.start === secondBounds.start &&
firstBounds.end === secondBounds.end;
}
}

View File

@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@ -19,15 +19,10 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import URLTimeSettingsSynchronizer from "./URLTimeSettingsSynchronizer.js";
define(
[],
function () {
function NotificationIndicator() {}
NotificationIndicator.template = 'notificationIndicatorTemplate';
return NotificationIndicator;
export default function () {
return function install(openmct) {
return new URLTimeSettingsSynchronizer(openmct);
}
);
}

View File

@ -0,0 +1,307 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe("The URLTimeSettingsSynchronizer", () => {
let openmct;
let testClock;
beforeAll(() => resetApplicationState());
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(openmct.plugins.LocalTimeSystem());
testClock = jasmine.createSpyObj("testClock", ["start", "stop", "tick", "currentValue", "on", "off"]);
testClock.key = "test-clock";
testClock.currentValue.and.returnValue(0);
openmct.time.addClock(testClock);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => resetApplicationState(openmct));
describe("realtime mode", () => {
it("when the clock is set via the time API, it is immediately reflected in the URL", () => {
//Test expected initial conditions
expect(window.location.hash.includes('tc.mode=fixed')).toBe(true);
openmct.time.clock('local', {start: -1000, end: 100});
expect(window.location.hash.includes('tc.mode=local')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.mode=fixed')).toBe(false);
});
it("when offsets are set via the time API, they are immediately reflected in the URL", () => {
//Test expected initial conditions
expect(window.location.hash.includes('tc.startDelta')).toBe(false);
expect(window.location.hash.includes('tc.endDelta')).toBe(false);
openmct.time.clock('local', {start: -1000, end: 100});
expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true);
expect(window.location.hash.includes('tc.endDelta=100')).toBe(true);
openmct.time.clockOffsets({start: -2000, end: 200});
expect(window.location.hash.includes('tc.startDelta=2000')).toBe(true);
expect(window.location.hash.includes('tc.endDelta=200')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.mode=fixed')).toBe(false);
});
describe("when set in the url", () => {
it("will change from fixed to realtime mode when the mode changes", () => {
expectLocationToBeInFixedMode();
return switchToRealtimeMode().then(() => {
let clock = openmct.time.clock();
expect(clock).toBeDefined();
expect(clock.key).toBe('local');
});
});
it("the clock is correctly set in the API from the URL parameters", () => {
return switchToRealtimeMode().then(() => {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('clock', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.mode=local', 'tc.mode=test-clock');
window.location.hash = hash;
}).then(() => {
let clock = openmct.time.clock();
expect(clock).toBeDefined();
expect(clock.key).toBe('test-clock');
openmct.time.off('clock', resolveFunction);
});
});
});
it("the clock offsets are correctly set in the API from the URL parameters", () => {
return switchToRealtimeMode().then(() => {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('clockOffsets', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.startDelta=1000', 'tc.startDelta=2000');
hash = hash.replace('tc.endDelta=100', 'tc.endDelta=200');
window.location.hash = hash;
}).then(() => {
let clockOffsets = openmct.time.clockOffsets();
expect(clockOffsets).toBeDefined();
expect(clockOffsets.start).toBe(-2000);
expect(clockOffsets.end).toBe(200);
openmct.time.off('clockOffsets', resolveFunction);
});
});
});
it("the time system is correctly set in the API from the URL parameters", () => {
return switchToRealtimeMode().then(() => {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('timeSystem', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local');
window.location.hash = hash;
}).then(() => {
let timeSystem = openmct.time.timeSystem();
expect(timeSystem).toBeDefined();
expect(timeSystem.key).toBe('local');
openmct.time.off('timeSystem', resolveFunction);
});
});
});
});
});
describe("fixed timespan mode", () => {
beforeEach(() => {
openmct.time.stopClock();
openmct.time.timeSystem('utc', {start: 0, end: 1});
});
it("when bounds are set via the time API, they are immediately reflected in the URL", ()=>{
//Test expected initial conditions
expect(window.location.hash.includes('tc.startBound=0')).toBe(true);
expect(window.location.hash.includes('tc.endBound=1')).toBe(true);
openmct.time.bounds({start: 10, end: 20});
expect(window.location.hash.includes('tc.startBound=10')).toBe(true);
expect(window.location.hash.includes('tc.endBound=20')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.startBound=0')).toBe(false);
expect(window.location.hash.includes('tc.endBound=1')).toBe(false);
});
it("when time system is set via the time API, it is immediately reflected in the URL", ()=>{
//Test expected initial conditions
expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(true);
openmct.time.timeSystem('local', {start: 20, end: 30});
expect(window.location.hash.includes('tc.timeSystem=local')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(false);
});
describe("when set in the url", () => {
it("time system changes are reflected in the API", () => {
let resolveFunction;
return new Promise((resolve) => {
let timeSystem = openmct.time.timeSystem();
resolveFunction = resolve;
expect(timeSystem.key).toBe('utc');
window.location.hash = window.location.hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local');
openmct.time.on('timeSystem', resolveFunction);
}).then(() => {
let timeSystem = openmct.time.timeSystem();
expect(timeSystem.key).toBe('local');
openmct.time.off('timeSystem', resolveFunction);
});
});
it("mode can be changed from realtime to fixed", () => {
return switchToRealtimeMode().then(() => {
expectLocationToBeInRealtimeMode();
expect(openmct.time.clock()).toBeDefined();
}).then(switchToFixedMode).then(() => {
let clock = openmct.time.clock();
expect(clock).not.toBeDefined();
});
});
it("bounds are correctly set in the API from the URL parameters", () => {
let resolveFunction;
expectLocationToBeInFixedMode();
return new Promise((resolve) => {
resolveFunction = resolve;
openmct.time.on('bounds', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.startBound=0', 'tc.startBound=222')
.replace('tc.endBound=1', 'tc.endBound=333');
window.location.hash = hash;
}).then(() => {
let bounds = openmct.time.bounds();
expect(bounds).toBeDefined();
expect(bounds.start).toBe(222);
expect(bounds.end).toBe(333);
});
});
it("bounds are correctly set in the API from the URL parameters where only the end bound changes", () => {
let resolveFunction;
expectLocationToBeInFixedMode();
return new Promise((resolve) => {
resolveFunction = resolve;
openmct.time.on('bounds', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.endBound=1', 'tc.endBound=333');
window.location.hash = hash;
}).then(() => {
let bounds = openmct.time.bounds();
expect(bounds).toBeDefined();
expect(bounds.start).toBe(0);
expect(bounds.end).toBe(333);
});
});
});
});
function setRealtimeLocationParameters() {
let hash = window.location.hash.toString()
.replace('tc.mode=fixed', 'tc.mode=local')
.replace('tc.startBound=0', 'tc.startDelta=1000')
.replace('tc.endBound=1', 'tc.endDelta=100');
window.location.hash = hash;
}
function setFixedLocationParameters() {
let hash = window.location.hash.toString()
.replace('tc.mode=local', 'tc.mode=fixed')
.replace('tc.timeSystem=utc', 'tc.timeSystem=local')
.replace('tc.startDelta=1000', 'tc.startBound=50')
.replace('tc.endDelta=100', 'tc.endBound=60');
window.location.hash = hash;
}
function switchToRealtimeMode() {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
openmct.time.on('clock', resolveFunction);
setRealtimeLocationParameters();
}).then(() => {
openmct.time.off('clock', resolveFunction);
});
}
function switchToFixedMode() {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('clock', resolveFunction);
setFixedLocationParameters();
}).then(() => {
openmct.time.off('clock', resolveFunction);
});
}
function expectLocationToBeInRealtimeMode() {
expect(window.location.hash.includes('tc.mode=local')).toBe(true);
expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true);
expect(window.location.hash.includes('tc.endDelta=100')).toBe(true);
expect(window.location.hash.includes('tc.mode=fixed')).toBe(false);
}
function expectLocationToBeInFixedMode() {
expect(window.location.hash.includes('tc.mode=fixed')).toBe(true);
expect(window.location.hash.includes('tc.startBound=0')).toBe(true);
expect(window.location.hash.includes('tc.endBound=1')).toBe(true);
expect(window.location.hash.includes('tc.mode=local')).toBe(false);
}
});

View File

@ -23,9 +23,10 @@
import EventEmitter from 'EventEmitter';
import uuid from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { TRIGGER } from "./utils/constants";
import {computeCondition, computeConditionByLimit} from "./utils/evaluator";
import { evaluateResults } from './utils/evaluator';
import { getLatestTimestamp } from './utils/time';
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
import {TRIGGER_CONJUNCTION, TRIGGER_LABEL} from "./utils/constants";
/*
* conditionConfiguration = {
@ -41,13 +42,14 @@ import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
* ]
* }
*/
export default class ConditionClass extends EventEmitter {
export default class Condition extends EventEmitter {
/**
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
* @constructor
* @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
* @param openmct
* @param conditionManager
*/
constructor(conditionConfiguration, openmct, conditionManager) {
super();
@ -56,28 +58,42 @@ export default class ConditionClass extends EventEmitter {
this.conditionManager = conditionManager;
this.id = conditionConfiguration.id;
this.criteria = [];
this.criteriaResults = {};
this.result = undefined;
this.latestTimestamp = {};
this.timeSystems = this.openmct.time.getAllTimeSystems();
if (conditionConfiguration.configuration.criteria) {
this.createCriteria(conditionConfiguration.configuration.criteria);
}
this.trigger = conditionConfiguration.configuration.trigger;
this.conditionManager.on('broadcastTelemetry', this.handleBroadcastTelemetry, this);
this.description = '';
}
handleBroadcastTelemetry(datum) {
getResult(datum) {
if (!datum || !datum.id) {
console.log('no data received');
return;
}
this.criteria.forEach(criterion => {
if (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')) {
criterion.handleSubscription(datum, this.conditionManager.telemetryObjects);
} else {
criterion.emit(`subscription:${datum.id}`, datum);
}
if (this.isTelemetryUsed(datum.id)) {
this.criteria.forEach(criterion => {
if (this.isAnyOrAllTelemetry(criterion)) {
criterion.getResult(datum, this.conditionManager.telemetryObjects);
} else {
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;
});
}
@ -89,7 +105,6 @@ export default class ConditionClass extends EventEmitter {
updateTrigger(trigger) {
if (this.trigger !== trigger) {
this.trigger = trigger;
this.handleConditionUpdated();
}
}
@ -97,7 +112,7 @@ export default class ConditionClass extends EventEmitter {
return {
id: criterionConfiguration.id || uuid(),
telemetry: criterionConfiguration.telemetry || '',
telemetryObject: this.conditionManager.telemetryObjects[this.openmct.objects.makeKeyString(criterionConfiguration.telemetry)],
telemetryObjects: this.conditionManager.telemetryObjects,
operation: criterionConfiguration.operation || '',
input: criterionConfiguration.input === undefined ? [] : criterionConfiguration.input,
metadata: criterionConfiguration.metadata || ''
@ -108,6 +123,7 @@ export default class ConditionClass extends EventEmitter {
criterionConfigurations.forEach((criterionConfiguration) => {
this.addCriterion(criterionConfiguration);
});
this.updateDescription();
}
updateCriteria(criterionConfigurations) {
@ -115,10 +131,11 @@ export default class ConditionClass extends EventEmitter {
this.createCriteria(criterionConfigurations);
}
updateTelemetry() {
updateTelemetryObjects() {
this.criteria.forEach((criterion) => {
criterion.updateTelemetry(this.conditionManager.telemetryObjects);
criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects);
});
this.updateDescription();
}
/**
@ -133,7 +150,6 @@ export default class ConditionClass extends EventEmitter {
criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);
}
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
if (!this.criteria) {
this.criteria = [];
}
@ -162,22 +178,12 @@ export default class ConditionClass extends EventEmitter {
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
newCriterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
let criterion = found.item;
criterion.unsubscribe();
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
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();
this.updateDescription();
}
}
@ -185,15 +191,13 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(id);
if (found) {
let criterion = found.item;
criterion.destroy();
// TODO this is passing the wrong args
criterion.off('criterionUpdated', (result) => {
this.handleCriterionUpdated(id, result);
criterion.off('criterionUpdated', (obj) => {
this.handleCriterionUpdated(obj);
});
criterion.destroy();
this.criteria.splice(found.index, 1);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id];
}
this.updateDescription();
return true;
}
return false;
@ -203,59 +207,61 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(criterion.id);
if (found) {
this.criteria[found.index] = criterion.data;
// TODO nothing is listening to this
this.emitEvent('conditionUpdated', {
trigger: this.trigger,
criteria: this.criteria
});
this.updateDescription();
}
}
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;
}
updateDescription() {
const triggerDescription = this.getTriggerDescription();
let description = '';
this.criteria.forEach((criterion, index) => {
if (!index) {
description = `Match if ${triggerDescription.prefix}`;
}
description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`;
});
this.description = description;
this.conditionManager.updateConditionDescription(this);
}
handleCriterionResult(eventData) {
this.updateCriteriaResults(eventData);
this.handleConditionUpdated(eventData.data);
getTriggerDescription() {
return {
conjunction: TRIGGER_CONJUNCTION[this.trigger],
prefix: `${TRIGGER_LABEL[this.trigger]}: `
};
}
requestLADConditionResult() {
const criteriaResults = this.criteria
.map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects}));
let latestTimestamp;
let criteriaResults = {};
const criteriaRequests = this.criteria
.map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects));
return Promise.all(criteriaResults)
return Promise.all(criteriaRequests)
.then(results => {
results.forEach(result => {
this.updateCriteriaResults(result);
this.latestTimestamp = this.getLatestTimestamp(this.latestTimestamp, result.data)
results.forEach(resultObj => {
const { id, data, data: { result } } = resultObj;
if (this.findCriterion(id)) {
criteriaResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
this.evaluate();
return {
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() {
return this.criteria;
}
@ -269,41 +275,7 @@ export default class ConditionClass extends EventEmitter {
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() {
this.conditionManager.off('broadcastTelemetry', this.handleBroadcastTelemetry, this);
if (typeof this.stopObservingForChanges === 'function') {
this.stopObservingForChanges();
}
this.destroyCriteria();
}
}

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import Condition from "./Condition";
import { getLatestTimestamp } from './utils/time';
import uuid from "uuid";
import EventEmitter from 'EventEmitter';
@ -29,8 +30,7 @@ export default class ConditionManager extends EventEmitter {
super();
this.openmct = openmct;
this.conditionSetDomainObject = conditionSetDomainObject;
this.timeAPI = this.openmct.time;
this.latestTimestamp = {};
this.timeSystems = this.openmct.time.getAllTimeSystems();
this.composition = this.openmct.composition.get(conditionSetDomainObject);
this.composition.on('add', this.subscribeToTelemetry, this);
this.composition.on('remove', this.unsubscribeFromTelemetry, this);
@ -55,9 +55,9 @@ export default class ConditionManager extends EventEmitter {
this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas});
this.subscriptions[id] = this.openmct.telemetry.subscribe(
endpoint,
this.broadcastTelemetry.bind(this, id)
this.telemetryReceived.bind(this, endpoint)
);
this.updateConditionTelemetry();
this.updateConditionTelemetryObjects();
}
unsubscribeFromTelemetry(endpointIdentifier) {
@ -70,11 +70,11 @@ export default class ConditionManager extends EventEmitter {
this.subscriptions[id]();
delete this.subscriptions[id];
delete this.telemetryObjects[id];
this.removeConditionTelemetryObjects();
}
initialize() {
this.conditionResults = {};
this.conditionClassCollection = [];
this.conditions = [];
if (this.conditionSetDomainObject.configuration.conditionCollection.length) {
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
this.initCondition(conditionConfiguration, index);
@ -82,24 +82,58 @@ export default class ConditionManager extends EventEmitter {
}
}
updateConditionTelemetry() {
this.conditionClassCollection.forEach((condition) => condition.updateTelemetry());
updateConditionTelemetryObjects() {
this.conditions.forEach((condition) => condition.updateTelemetryObjects());
}
removeConditionTelemetryObjects() {
let conditionsChanged = false;
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, conditionIndex) => {
let conditionChanged = false;
conditionConfiguration.configuration.criteria.forEach((criterion, index) => {
const isAnyAllTelemetry = criterion.telemetry && (criterion.telemetry === 'any' || criterion.telemetry === 'all');
if (!isAnyAllTelemetry) {
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 = '';
conditionChanged = true;
}
}
});
if (conditionChanged) {
this.updateCondition(conditionConfiguration, conditionIndex);
conditionsChanged = true;
}
});
if (conditionsChanged) {
this.persistConditions();
}
}
updateCondition(conditionConfiguration, index) {
let condition = this.conditionClassCollection[index];
condition.update(conditionConfiguration);
let condition = this.conditions[index];
this.conditionSetDomainObject.configuration.conditionCollection[index] = conditionConfiguration;
condition.update(conditionConfiguration);
this.persistConditions();
}
updateConditionDescription(condition) {
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
found.summary = condition.description;
this.persistConditions();
}
initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this);
condition.on('conditionResultUpdated', this.handleConditionResult.bind(this));
if (index !== undefined) {
this.conditionClassCollection.splice(index + 1, 0, condition);
this.conditions.splice(index + 1, 0, condition);
} else {
this.conditionClassCollection.unshift(condition);
this.conditions.unshift(condition);
}
}
@ -158,23 +192,17 @@ export default class ConditionManager extends EventEmitter {
}
removeCondition(index) {
let condition = this.conditionClassCollection[index];
condition.destroyCriteria();
condition.off('conditionResultUpdated', this.handleConditionResult.bind(this));
this.conditionClassCollection.splice(index, 1);
let condition = this.conditions[index];
condition.destroy();
this.conditions.splice(index, 1);
this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1);
if (this.conditionResults[condition.id] !== undefined) {
delete this.conditionResults[condition.id];
}
this.persistConditions();
this.handleConditionResult();
}
findConditionById(id) {
return this.conditionClassCollection.find(conditionClass => conditionClass.id === id);
return this.conditions.find(condition => condition.id === id);
}
//this.$set(this.conditionClassCollection, reorderEvent.newIndex, oldConditions[reorderEvent.oldIndex]);
reorderConditions(reorderPlan) {
let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection);
let newCollection = [];
@ -191,7 +219,23 @@ export default class ConditionManager extends EventEmitter {
let currentCondition = conditionCollection[conditionCollection.length-1];
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
currentCondition = conditionCollection[i];
break;
@ -200,24 +244,79 @@ export default class ConditionManager extends EventEmitter {
return currentCondition;
}
updateConditionResults(resultObj) {
if (!resultObj) {
requestLADConditionSetOutput() {
if (!this.conditions.length) {
return Promise.resolve([]);
}
return this.compositionLoad.then(() => {
let latestTimestamp;
let conditionResults = {};
const conditionRequests = this.conditions
.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.conditions) {
if (condition.isTelemetryUsed(id)) {
return true;
}
}
return false;
}
telemetryReceived(endpoint, datum) {
if (!this.isTelemetryUsed(endpoint)) {
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.conditionResults[id] = resultObj.data.result;
}
this.conditions.forEach(condition => {
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();
this.emit('conditionSetResultUpdated',
Object.assign(
{
@ -225,51 +324,11 @@ export default class ConditionManager extends EventEmitter {
id: this.conditionSetDomainObject.identifier,
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) {
let data = undefined;
if (this.testData.applied) {
@ -281,13 +340,20 @@ export default class ConditionManager extends EventEmitter {
return data;
}
createNormalizedDatum(telemetryDatum, id) {
return Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((normalizedDatum, metadatum) => {
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 testValue = this.getTestData(metadatum);
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
normalizedDatum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
return normalizedDatum;
datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
return datum;
}, {});
normalizedDatum.id = id;
return normalizedDatum;
}
updateTestData(testData) {
@ -303,14 +369,13 @@ export default class ConditionManager extends EventEmitter {
this.composition.off('add', this.subscribeToTelemetry, this);
this.composition.off('remove', this.unsubscribeFromTelemetry, this);
Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe());
this.subscriptions = undefined;
delete this.subscriptions;
if(this.stopObservingForChanges) {
this.stopObservingForChanges();
}
this.conditionClassCollection.forEach((condition) => {
condition.off('conditionResultUpdated', this.handleConditionResult);
this.conditions.forEach((condition) => {
condition.destroy();
})
}

View File

@ -49,6 +49,7 @@ describe('ConditionManager', () => {
};
let mockComposition;
let loader;
let mockTimeSystems;
function mockAngularComponents() {
let mockInjector = jasmine.createSpyObj('$injector', ['get']);
@ -111,15 +112,21 @@ describe('ConditionManager', () => {
openmct.objects.observe.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.on('conditionSetResultUpdated', mockListener);
conditionMgr.on('broadcastTelemetry', mockListener);
conditionMgr.on('telemetryReceived', mockListener);
});
it('creates a conditionCollection with a default condition', function () {
expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(1);
let defaultConditionId = conditionMgr.conditionClassCollection[0].id;
let defaultConditionId = conditionMgr.conditions[0].id;
expect(defaultConditionId).toEqual(mockCondition.id);
});

View File

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

View File

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

View File

@ -25,40 +25,47 @@ import {TRIGGER} from "./utils/constants";
import TelemetryCriterion from "./criterion/TelemetryCriterion";
let openmct = {},
mockListener,
testConditionDefinition,
testTelemetryObject,
conditionObj,
conditionManager,
mockBroadcastTelemetry;
mockTelemetryReceived,
mockTimeSystems;
describe("The condition", function () {
beforeEach (() => {
conditionManager = jasmine.createSpyObj('conditionManager',
['on']
['on', 'updateConditionDescription']
);
mockBroadcastTelemetry = jasmine.createSpy('listener');
conditionManager.on('broadcastTelemetry', mockBroadcastTelemetry);
mockTelemetryReceived = jasmine.createSpy('listener');
conditionManager.on('telemetryReceived', mockTelemetryReceived);
conditionManager.updateConditionDescription.and.returnValue(function () {});
mockListener = jasmine.createSpy('listener');
testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
valueMetadatas: [{
key: "some-key",
name: "Some attribute",
hints: {
range: 2
}
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
key: "testSource",
source: "value",
name: "Test",
format: "string"
}]
}
};
@ -72,7 +79,13 @@ describe("The condition", function () {
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', 'subscribe', 'getMetadata']);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values);
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
testConditionDefinition = {
id: '123-456',
@ -97,8 +110,6 @@ describe("The condition", function () {
openmct,
conditionManager
);
conditionObj.on('conditionUpdated', mockListener);
});
it("generates criteria with the correct properties", function () {
@ -133,4 +144,38 @@ describe("The condition", function () {
expect(result).toBeTrue();
expect(conditionObj.criteria.length).toEqual(0);
});
it("gets the result of a condition when new telemetry data is received", function () {
conditionObj.getResult({
value: '0',
utc: 'Hi',
id: testTelemetryObject.identifier.key
});
expect(conditionObj.result).toBeTrue();
});
it("gets the result of a condition when new telemetry data is received", function () {
conditionObj.getResult({
value: '1',
utc: 'Hi',
id: testTelemetryObject.identifier.key
});
expect(conditionObj.result).toBeFalse();
});
it("keeps the old result new telemetry data is not used by it", function () {
conditionObj.getResult({
value: '0',
utc: 'Hi',
id: testTelemetryObject.identifier.key
});
expect(conditionObj.result).toBeTrue();
conditionObj.getResult({
value: '1',
utc: 'Hi',
id: '1234'
});
expect(conditionObj.result).toBeTrue();
});
});

View File

@ -23,10 +23,14 @@
import EventEmitter from 'EventEmitter';
export default class StyleRuleManager extends EventEmitter {
constructor(styleConfiguration, openmct, callback) {
constructor(styleConfiguration, openmct, callback, suppressSubscriptionOnEdit) {
super();
this.openmct = openmct;
this.callback = callback;
if (suppressSubscriptionOnEdit) {
this.openmct.editor.on('isEditing', this.toggleSubscription.bind(this));
this.isEditing = this.openmct.editor.editing;
}
if (styleConfiguration) {
this.initialize(styleConfiguration);
if (styleConfiguration.conditionSetIdentifier) {
@ -37,9 +41,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) {
this.conditionSetIdentifier = styleConfiguration.conditionSetIdentifier;
this.staticStyle = styleConfiguration.staticStyle;
this.selectedConditionId = styleConfiguration.selectedConditionId;
this.defaultConditionId = styleConfiguration.defaultConditionId;
this.updateConditionStylesMap(styleConfiguration.styles || []);
}
@ -54,7 +74,7 @@ export default class StyleRuleManager extends EventEmitter {
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,9 +86,13 @@ export default class StyleRuleManager extends EventEmitter {
let isNewConditionSet = !this.conditionSetIdentifier ||
!this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier);
this.initialize(styleConfiguration);
//Only resubscribe if the conditionSet has changed.
if (isNewConditionSet) {
this.subscribeToConditionSet();
if (this.isEditing) {
this.applySelectedConditionStyle();
} else {
//Only resubscribe if the conditionSet has changed.
if (isNewConditionSet) {
this.subscribeToConditionSet();
}
}
}
}
@ -103,13 +127,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() {
if (this.staticStyle) {
this.currentStyle = this.staticStyle.style;
} else {
if (this.currentStyle) {
Object.keys(this.currentStyle).forEach(key => {
this.currentStyle[key] = 'transparent';
this.currentStyle[key] = '__no_value';
});
}
}

View File

@ -21,159 +21,169 @@
*****************************************************************************/
<template>
<div v-if="isEditing"
class="c-condition c-condition--edit js-condition-drag-wrapper"
<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)"
>
<!-- Edit view -->
<div class="c-condition__header">
<span class="c-condition__drag-grippy c-grippy c-grippy--vertical-drag"
title="Drag to reorder conditions"
:class="[{ 'is-enabled': !condition.isDefault }, { 'hide-nice': condition.isDefault }]"
:draggable="!condition.isDefault"
@dragstart="dragStart"
@dragstop="dragStop"
@dragover.stop
></span>
<span class="c-condition__disclosure c-disclosure-triangle c-tree__item__view-control is-enabled"
:class="{ 'c-disclosure-triangle--expanded': expanded }"
@click="expanded = !expanded"
></span>
<span class="c-condition__name">{{ condition.configuration.name }}</span>
<span class="c-condition__summary">
<template v-if="!canEvaluateCriteria">
Define criteria
</template>
<span v-else>
<condition-description :show-label="false"
:condition="condition"
/>
</span>
</span>
<div class="c-condition__buttons">
<button v-if="!condition.isDefault"
class="c-click-icon c-condition__duplicate-button icon-duplicate"
title="Duplicate this condition"
@click="cloneCondition"
></button>
<button v-if="!condition.isDefault"
class="c-click-icon c-condition__delete-button icon-trash"
title="Delete this condition"
@click="removeCondition"
></button>
</div>
</div>
<div v-if="expanded"
class="c-condition__definition c-cdef"
<div class="c-condition-h__drop-target"></div>
<div v-if="isEditing"
:class="{'is-current': condition.id === currentConditionId}"
class="c-condition c-condition--edit"
>
<span class="c-cdef__separator c-row-separator"></span>
<span class="c-cdef__label">Condition Name</span>
<span class="c-cdef__controls">
<input v-model="condition.configuration.name"
class="t-condition-input__name"
type="text"
@blur="persist"
>
</span>
<!-- Edit view -->
<div class="c-condition__header">
<span class="c-condition__drag-grippy c-grippy c-grippy--vertical-drag"
title="Drag to reorder conditions"
:class="[{ 'is-enabled': !condition.isDefault }, { 'hide-nice': condition.isDefault }]"
:draggable="!condition.isDefault"
@dragstart="dragStart"
@dragend="dragEnd"
></span>
<span class="c-cdef__label">Output</span>
<span class="c-cdef__controls">
<span class="c-cdef__control">
<select v-model="selectedOutputSelection"
@change="setOutputValue"
>
<option v-for="option in outputOptions"
:key="option"
:value="option"
>
{{ initCap(option) }}
</option>
</select>
</span>
<span class="c-cdef__control">
<input v-if="selectedOutputSelection === outputOptions[2]"
v-model="condition.configuration.output"
class="t-condition-name-input"
type="text"
@blur="persist"
>
</span>
</span>
<span class="c-condition__disclosure c-disclosure-triangle c-tree__item__view-control is-enabled"
:class="{ 'c-disclosure-triangle--expanded': expanded }"
@click="expanded = !expanded"
></span>
<div v-if="!condition.isDefault"
class="c-cdef__match-and-criteria"
<span class="c-condition__name">{{ condition.configuration.name }}</span>
<span class="c-condition__summary">
<template v-if="!condition.isDefault && !canEvaluateCriteria">
Define criteria
</template>
<span v-else>
<condition-description :show-label="false"
:condition="condition"
/>
</span>
</span>
<div class="c-condition__buttons">
<button v-if="!condition.isDefault"
class="c-click-icon c-condition__duplicate-button icon-duplicate"
title="Duplicate this condition"
@click="cloneCondition"
></button>
<button v-if="!condition.isDefault"
class="c-click-icon c-condition__delete-button icon-trash"
title="Delete this condition"
@click="removeCondition"
></button>
</div>
</div>
<div v-if="expanded"
class="c-condition__definition c-cdef"
>
<span class="c-cdef__separator c-row-separator"></span>
<span class="c-cdef__label">Match</span>
<span class="c-cdef__label">Condition Name</span>
<span class="c-cdef__controls">
<select v-model="condition.configuration.trigger"
@change="persist"
<input v-model="condition.configuration.name"
class="t-condition-input__name"
type="text"
@change="persist"
>
<option v-for="option in triggers"
:key="option.value"
:value="option.value"
> {{ option.label }}</option>
</select>
</span>
<template v-if="telemetry.length || condition.configuration.criteria.length">
<div v-for="(criterion, index) in condition.configuration.criteria"
:key="criterion.id"
class="c-cdef__criteria"
>
<Criterion :telemetry="telemetry"
:criterion="criterion"
:index="index"
:trigger="condition.configuration.trigger"
:is-default="condition.configuration.criteria.length === 1"
@persist="persist"
/>
<div class="c-cdef__criteria__buttons">
<button class="c-click-icon c-cdef__criteria-duplicate-button icon-duplicate"
title="Duplicate this criteria"
@click="cloneCriterion(index)"
></button>
<button v-if="!(condition.configuration.criteria.length === 1)"
class="c-click-icon c-cdef__criteria-duplicate-button icon-trash"
title="Delete this criteria"
@click="removeCriterion(index)"
></button>
</div>
</div>
</template>
<div class="c-cdef__separator c-row-separator"></div>
<div class="c-cdef__controls"
:disabled="!telemetry.length"
<span class="c-cdef__label">Output</span>
<span class="c-cdef__controls">
<span class="c-cdef__control">
<select v-model="selectedOutputSelection"
@change="setOutputValue"
>
<option v-for="option in outputOptions"
:key="option"
:value="option"
>
{{ initCap(option) }}
</option>
</select>
</span>
<span class="c-cdef__control">
<input v-if="selectedOutputSelection === outputOptions[2]"
v-model="condition.configuration.output"
class="t-condition-name-input"
type="text"
@change="persist"
>
</span>
</span>
<div v-if="!condition.isDefault"
class="c-cdef__match-and-criteria"
>
<button
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
@click="addCriteria"
<span class="c-cdef__separator c-row-separator"></span>
<span class="c-cdef__label">Match</span>
<span class="c-cdef__controls">
<select v-model="condition.configuration.trigger"
@change="persist"
>
<option v-for="option in triggers"
:key="option.value"
:value="option.value"
> {{ option.label }}</option>
</select>
</span>
<template v-if="telemetry.length || condition.configuration.criteria.length">
<div v-for="(criterion, index) in condition.configuration.criteria"
:key="criterion.id"
class="c-cdef__criteria"
>
<Criterion :telemetry="telemetry"
:criterion="criterion"
:index="index"
:trigger="condition.configuration.trigger"
:is-default="condition.configuration.criteria.length === 1"
@persist="persist"
/>
<div class="c-cdef__criteria__buttons">
<button class="c-click-icon c-cdef__criteria-duplicate-button icon-duplicate"
title="Duplicate this criteria"
@click="cloneCriterion(index)"
></button>
<button v-if="!(condition.configuration.criteria.length === 1)"
class="c-click-icon c-cdef__criteria-duplicate-button icon-trash"
title="Delete this criteria"
@click="removeCriterion(index)"
></button>
</div>
</div>
</template>
<div class="c-cdef__separator c-row-separator"></div>
<div class="c-cdef__controls"
:disabled="!telemetry.length"
>
<span class="c-button__label">Add Criteria</span>
</button>
<button
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
@click="addCriteria"
>
<span class="c-button__label">Add Criteria</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div v-else
class="c-condition c-condition--browse"
>
<!-- Browse view -->
<div class="c-condition__header">
<span class="c-condition__name">
{{ condition.configuration.name }}
</span>
<span class="c-condition__output">
Output: {{ condition.configuration.output }}
</span>
</div>
<div class="c-condition__summary">
<condition-description :show-label="false"
:condition="condition"
/>
<div v-else
class="c-condition c-condition--browse"
:class="{'is-current': condition.id === currentConditionId}"
>
<!-- Browse view -->
<div class="c-condition__header">
<span class="c-condition__name">
{{ condition.configuration.name }}
</span>
<span class="c-condition__output">
Output: {{ condition.configuration.output }}
</span>
</div>
<div class="c-condition__summary">
<condition-description :show-label="false"
:condition="condition"
/>
</div>
</div>
</div>
</template>
@ -191,6 +201,10 @@ export default {
ConditionDescription
},
props: {
currentConditionId: {
type: String,
default: ''
},
condition: {
type: Object,
required: true
@ -207,6 +221,14 @@ export default {
type: Array,
required: true,
default: () => []
},
isDragging: {
type: Boolean,
default: false
},
moveIndex: {
type: Number,
default: 0
}
},
data() {
@ -217,8 +239,8 @@ export default {
selectedOutputSelection: '',
outputOptions: ['false', 'true', 'string'],
criterionIndex: 0,
selectedTelemetryName: '',
selectedFieldName: ''
draggingOver: false,
isDefault: this.condition.isDefault
};
},
computed: {
@ -228,7 +250,7 @@ export default {
keys.forEach((trigger) => {
triggerOptions.push({
value: TRIGGER[trigger],
label: TRIGGER_LABEL[TRIGGER[trigger]]
label: `when ${TRIGGER_LABEL[TRIGGER[trigger]]}`
});
});
return triggerOptions;
@ -286,11 +308,39 @@ export default {
dragStart(e) {
e.dataTransfer.setData('dragging', e.target); // required for FF to initiate drag
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);
},
dragStop(e) {
e.dataTransfer.clearData();
dragEnd(event) {
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() {
},

View File

@ -22,7 +22,6 @@
<template>
<section id="conditionCollection"
class="c-cs__conditions"
:class="{ 'is-expanded': expanded }"
>
<div class="c-cs__header c-section__header">
@ -53,28 +52,25 @@
<span class="c-cs-button__label">Add Condition</span>
</button>
<div class="c-cs__conditions-h">
<div v-for="(condition, index) in conditionCollection"
:key="condition.id"
class="c-condition-h"
>
<div v-if="isEditing"
class="c-c__drag-ghost"
@drop.prevent="dropCondition"
@dragenter="dragEnter"
@dragleave="dragLeave"
@dragover.prevent
></div>
<Condition :condition="condition"
:condition-index="index"
:telemetry="telemetryObjs"
:is-editing="isEditing"
@updateCondition="updateCondition"
@removeCondition="removeCondition"
@cloneCondition="cloneCondition"
@setMoveIndex="setMoveIndex"
/>
</div>
<div class="c-cs__conditions-h"
:class="{ 'is-active-dragging': isDragging }"
>
<Condition v-for="(condition, index) in conditionCollection"
:key="condition.id"
:condition="condition"
:current-condition-id="currentConditionId"
:condition-index="index"
:telemetry="telemetryObjs"
:is-editing="isEditing"
:move-index="moveIndex"
:is-dragging="isDragging"
@updateCondition="updateCondition"
@removeCondition="removeCondition"
@cloneCondition="cloneCondition"
@setMoveIndex="setMoveIndex"
@dragComplete="dragComplete"
@dropCondition="dropCondition"
/>
</div>
</div>
</section>
@ -109,9 +105,11 @@ export default {
conditionResults: {},
conditions: [],
telemetryObjs: [],
moveIndex: Number,
moveIndex: undefined,
isDragging: false,
defaultOutput: undefined
defaultOutput: undefined,
dragCounter: 0,
currentConditionId: ''
};
},
watch: {
@ -149,11 +147,13 @@ export default {
},
methods: {
handleConditionSetResultUpdated(data) {
this.currentConditionId = data.conditionId;
this.$emit('conditionSetResultUpdated', data)
},
observeForChanges() {
this.stopObservingForChanges = this.openmct.objects.observe(this.domainObject, 'configuration.conditionCollection', (newConditionCollection) => {
this.conditionCollection = newConditionCollection;
//this forces children to re-render
this.conditionCollection = newConditionCollection.map(condition => condition);
this.updateDefaultCondition();
});
},
@ -166,9 +166,7 @@ export default {
this.moveIndex = index;
this.isDragging = true;
},
dropCondition(e) {
let targetIndex = Array.from(document.querySelectorAll('.c-c__drag-ghost')).indexOf(e.target);
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
dropCondition(targetIndex) {
const oldIndexArr = Object.keys(this.conditionCollection);
const move = function (arr, old_index, new_index) {
while (old_index < 0) {
@ -194,26 +192,16 @@ export default {
}
this.reorder(reorderPlan);
e.target.classList.remove("dragging");
},
dragComplete() {
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) {
this.telemetryObjs.push(domainObject);
this.$emit('telemetryUpdated', this.telemetryObjs);
},
removeTelemetryObject(identifier) {
let index = _.findIndex(this.telemetryObjs, (obj) => {
let index = this.telemetryObjs.findIndex(obj => {
let objId = this.openmct.objects.makeKeyString(obj.identifier);
let id = this.openmct.objects.makeKeyString(identifier);
return objId === id;

View File

@ -27,20 +27,20 @@
>
{{ condition.configuration.name }}
</span>
<span v-for="(criterionDescription, index) in criterionDescriptions"
:key="criterionDescription"
class="c-style__condition-desc__text"
<span class="c-style__condition-desc__text"
v-if="!condition.isDefault"
>
<template v-if="!index">When</template>
{{ criterionDescription }}
<template v-if="index < (criterionDescriptions.length-1)">{{ triggerDescription }}</template>
{{ description }}
</span>
<span class="c-style__condition-desc__text"
v-else
>
Match if no other condition is matched
</span>
</div>
</template>
<script>
import { TRIGGER } from "@/plugins/condition/utils/constants";
import { OPERATIONS } from "@/plugins/condition/utils/operations";
export default {
name: 'ConditionDescription',
@ -59,95 +59,9 @@ export default {
}
}
},
data() {
return {
criterionDescriptions: [],
triggerDescription: ''
}
},
watch: {
condition: {
handler(val) {
this.getConditionDescription();
},
deep: true
}
},
mounted() {
this.getConditionDescription();
},
methods: {
getTriggerDescription(trigger) {
let description = '';
switch(trigger) {
case TRIGGER.ANY:
case TRIGGER.XOR:
description = 'or';
break;
case TRIGGER.ALL:
case TRIGGER.NOT: description = 'and';
break;
}
return description;
},
getConditionDescription() {
if (this.condition) {
this.triggerDescription = this.getTriggerDescription(this.condition.configuration.trigger);
this.criterionDescriptions = [];
this.condition.configuration.criteria.forEach((criterion, index) => {
this.getCriterionDescription(criterion, index);
});
if (this.condition.isDefault) {
this.criterionDescriptions.splice(0, 0, 'all else fails');
}
} else {
this.criterionDescriptions = [];
}
},
getCriterionDescription(criterion, index) {
if (!criterion.telemetry) {
let description = `Unknown ${criterion.metadata} ${this.getOperatorText(criterion.operation, criterion.input)}`;
this.criterionDescriptions.splice(index, 0, description);
} else if (criterion.telemetry === 'all' || criterion.telemetry === 'any') {
const telemetryDescription = criterion.telemetry === 'all' ? 'All telemetry' : 'Any telemetry';
let description = `${telemetryDescription} ${criterion.metadata} ${this.getOperatorText(criterion.operation, criterion.input)}`;
this.criterionDescriptions.splice(index, 0, description);
} else {
this.openmct.objects.get(criterion.telemetry).then((telemetryObject) => {
if (telemetryObject.type === 'unknown') {
let description = `Unknown ${criterion.metadata} ${this.getOperatorText(criterion.operation, criterion.input)}`;
this.criterionDescriptions.splice(index, 0, description);
} else {
let metadataValue = criterion.metadata;
let inputValue = criterion.input;
if (criterion.metadata) {
this.telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
const metadataObj = this.telemetryMetadata.valueMetadatas.find((metadata) => metadata.key === criterion.metadata);
if (metadataObj) {
if (metadataObj.name) {
metadataValue = metadataObj.name;
}
if(metadataObj.enumerations && inputValue.length) {
if (metadataObj.enumerations[inputValue[0]] && metadataObj.enumerations[inputValue[0]].string) {
inputValue = [metadataObj.enumerations[inputValue[0]].string];
}
}
}
}
let description = `${telemetryObject.name} ${metadataValue} ${this.getOperatorText(criterion.operation, inputValue)}`;
if (this.criterionDescriptions[index]) {
this.criterionDescriptions[index] = description;
} else {
this.criterionDescriptions.splice(index, 0, description);
}
}
});
}
},
getOperatorText(operationName, values) {
const found = OPERATIONS.find((operation) => operation.name === operationName);
return found ? found.getDescription(values) : '';
computed: {
description() {
return this.condition ? this.condition.summary : '';
}
}
}

View File

@ -66,22 +66,13 @@ export default {
}
},
getCriterionErrors(criterion, index) {
if (!criterion.telemetry) {
//It is sufficient to check for absence of telemetry here since the condition manager ensures that telemetry for a criterion is set if it exists
const isInvalidTelemetry = !criterion.telemetry && (criterion.telemetry !== 'all' && criterion.telemetry !== 'any');
if (isInvalidTelemetry) {
this.conditionErrors.push({
message: ERROR.TELEMETRY_NOT_FOUND,
additionalInfo: ''
});
} else {
if (criterion.telemetry !== 'all' && criterion.telemetry !== 'any') {
this.openmct.objects.get(criterion.telemetry).then((telemetryObject) => {
if (telemetryObject.type === 'unknown') {
this.conditionErrors.push({
message: ERROR.TELEMETRY_NOT_FOUND,
additionalInfo: criterion.telemetry ? `Key: ${this.openmct.objects.makeKeyString(criterion.telemetry)}` : ''
});
}
});
}
}
}
}

View File

@ -23,30 +23,33 @@
<template>
<div class="c-cs">
<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">
<template v-if="currentConditionOutput">
{{ currentConditionOutput }}
</template>
<template v-else>
{{ defaultConditionOutput }}
</template>
<span class="c-cs__current-output-value__label">Current Output</span>
<span class="c-cs__current-output-value__value">
<template v-if="currentConditionOutput">
{{ currentConditionOutput }}
</template>
<template v-else>
{{ defaultConditionOutput }}
</template>
</span>
</div>
</section>
<TestData :is-editing="isEditing"
:test-data="testData"
:telemetry="telemetryObjs"
@updateTestData="updateTestData"
/>
<ConditionCollection
:is-editing="isEditing"
:test-data="testData"
@conditionSetResultUpdated="updateCurrentOutput"
@updateDefaultOutput="updateDefaultOutput"
@telemetryUpdated="updateTelemetry"
/>
<div class="c-cs__test-data-and-conditions-w">
<TestData class="c-cs__test-data"
:is-editing="isEditing"
:test-data="testData"
:telemetry="telemetryObjs"
@updateTestData="updateTestData"
/>
<ConditionCollection class="c-cs__conditions"
:is-editing="isEditing"
:test-data="testData"
@conditionSetResultUpdated="updateCurrentOutput"
@updateDefaultOutput="updateDefaultOutput"
@telemetryUpdated="updateTelemetry"
/>
</div>
</div>
</template>

View File

@ -79,7 +79,7 @@
<input v-model="criterion.input[inputIndex]"
class="c-cdef__control__input"
:type="setInputType"
@blur="persist"
@change="persist"
>
<span v-if="inputIndex < inputCount-1">and</span>
</span>
@ -108,6 +108,7 @@
<script>
import { OPERATIONS } from '../utils/operations';
import { INPUT_TYPES } from '../utils/operations';
import {TRIGGER_CONJUNCTION} from "../utils/constants";
export default {
inject: ['openmct'],
@ -143,8 +144,8 @@ export default {
},
computed: {
setRowLabel: function () {
let operator = this.trigger === 'all' ? 'and ': 'or ';
return (this.index !== 0 ? operator : '') + 'when';
let operator = TRIGGER_CONJUNCTION[this.trigger];
return (this.index !== 0 ? operator : '') + ' when';
},
filteredOps: function () {
return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1);
@ -178,17 +179,18 @@ export default {
methods: {
checkTelemetry() {
if(this.criterion.telemetry) {
if (this.criterion.telemetry === 'any' || this.criterion.telemetry === 'all') {
this.updateMetadataOptions();
const isAnyAllTelemetry = this.criterion.telemetry === 'any' || this.criterion.telemetry === 'all';
const telemetryForCriterionExists = this.telemetry.find((telemetryObj) => this.openmct.objects.areIdsEqual(this.criterion.telemetry, telemetryObj.identifier));
if (!isAnyAllTelemetry &&
!telemetryForCriterionExists) {
//telemetry being used was removed. So reset this criterion.
this.criterion.telemetry = '';
this.criterion.metadata = '';
this.criterion.input = [];
this.criterion.operation = '';
this.persist();
} else {
if (!this.telemetry.find((telemetryObj) => this.openmct.objects.areIdsEqual(this.criterion.telemetry, telemetryObj.identifier))) {
//telemetry being used was removed. So reset this criterion.
this.criterion.telemetry = '';
this.criterion.metadata = '';
this.criterion.input = [];
this.criterion.operation = '';
this.persist();
}
this.updateMetadataOptions();
}
}
},
@ -221,19 +223,17 @@ export default {
this.persist();
}
if (this.criterion.telemetry) {
const telemetry = (this.criterion.telemetry === 'all' || this.criterion.telemetry === 'any') ? this.telemetry : [{
identifier: this.criterion.telemetry
}];
let telemetryPromises = telemetry.map((telemetryObject) => this.openmct.objects.get(telemetryObject.identifier));
Promise.all(telemetryPromises).then(telemetryObjects => {
this.telemetryMetadataOptions = [];
telemetryObjects.forEach(telemetryObject => {
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.addMetaDataOptions(telemetryMetadata.values());
});
this.updateOperations();
let telemetryObjects = this.telemetry;
if (this.criterion.telemetry !== 'all' && this.criterion.telemetry !== 'any') {
const found = this.telemetry.find(telemetryObj => (this.openmct.objects.areIdsEqual(telemetryObj.identifier, this.criterion.telemetry)));
telemetryObjects = found ? [found] : [];
}
this.telemetryMetadataOptions = [];
telemetryObjects.forEach(telemetryObject => {
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.addMetaDataOptions(telemetryMetadata.values());
});
this.updateOperations();
}
},
addMetaDataOptions(options) {

View File

@ -23,7 +23,6 @@
<template>
<section v-show="isEditing"
id="test-data"
class="c-cs__test-data"
:class="{ 'is-expanded': expanded }"
>
<div class="c-cs__header c-section__header">
@ -37,7 +36,7 @@
<div v-if="expanded"
class="c-cs__content"
>
<div class="c-cdef__controls"
<div class="c-cs__test-data__controls c-cdef__controls"
:disabled="!telemetry.length"
>
<label class="c-toggle-switch">
@ -96,7 +95,7 @@
>
</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"
title="Duplicate this test datum"
@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,318 @@
/*****************************************************************************
* 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 {
border: 1px solid transparent;
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;
}
&.is-current {
$c: $colorBodyFg;
border-color: rgba($c, 0.2);
background: rgba($c, 0.2);
}
}
/***************************** 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"
:key="index"
class="c-inspect-styles__condition"
:class="{'is-current': conditionStyle.conditionId === selectedConditionId}"
@click="applySelectedConditionStyle(conditionStyle.conditionId)"
>
<condition-error :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
@ -105,6 +107,8 @@ import ConditionDescription from "@/plugins/condition/components/ConditionDescri
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import Vue from 'vue';
import PreviewAction from "@/ui/preview/PreviewAction.js";
import {getApplicableStylesForItem} from "@/plugins/condition/utils/styleUtils";
import isEmpty from 'lodash/isEmpty';
export default {
name: 'ConditionalStylesView',
@ -115,24 +119,8 @@ export default {
},
inject: [
'openmct',
'domainObject'
'selection'
],
props: {
itemId: {
type: String,
default: ''
},
initialStyles: {
type: Object,
default() {
return undefined;
}
},
canHide: {
type: Boolean,
default: false
}
},
data() {
return {
conditionalStyles: [],
@ -141,13 +129,16 @@ export default {
isEditing: this.openmct.editor.isEditing(),
conditions: undefined,
conditionsLoaded: false,
navigateToPath: ''
navigateToPath: '',
selectedConditionId: ''
}
},
destroyed() {
this.openmct.editor.off('isEditing', this.setEditState);
this.removeListeners();
},
mounted() {
this.itemId = '';
this.getDomainObjectFromSelection();
this.previewAction = new PreviewAction(this.openmct);
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
let objectStyles = this.itemId ? this.domainObject.configuration.objectStyles[this.itemId] : this.domainObject.configuration.objectStyles;
@ -162,6 +153,52 @@ export default {
this.openmct.editor.on('isEditing', this.setEditState);
},
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) {
//If there are new conditions in the conditionSet we need to set those styles to default
this.conditionSetDomainObject = conditionSetDomainObject;
@ -170,6 +207,13 @@ export default {
},
setEditState(isEditing) {
this.isEditing = isEditing;
if (this.isEditing) {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
} else {
this.subscribeToConditionSet();
}
},
addConditionSet() {
let conditionSetDomainObject;
@ -240,23 +284,64 @@ export default {
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (this.itemId) {
domainObjectStyles[this.itemId].conditionSetIdentifier = undefined;
domainObjectStyles[this.itemId].selectedConditionId = undefined;
domainObjectStyles[this.itemId].defaultConditionId = undefined;
delete domainObjectStyles[this.itemId].conditionSetIdentifier;
domainObjectStyles[this.itemId].styles = undefined;
delete domainObjectStyles[this.itemId].styles;
if (_.isEmpty(domainObjectStyles[this.itemId])) {
if (isEmpty(domainObjectStyles[this.itemId])) {
delete domainObjectStyles[this.itemId];
}
} else {
domainObjectStyles.conditionSetIdentifier = undefined;
domainObjectStyles.selectedConditionId = undefined;
domainObjectStyles.defaultConditionId = undefined;
delete domainObjectStyles.conditionSetIdentifier;
domainObjectStyles.styles = undefined;
delete domainObjectStyles.styles;
}
if (_.isEmpty(domainObjectStyles)) {
if (isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
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() {
if (!this.conditions) {
@ -264,6 +349,9 @@ export default {
}
let conditionalStyles = [];
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
if (conditionConfiguration.isDefault) {
this.selectedConditionId = conditionConfiguration.id;
}
this.conditions[conditionConfiguration.id] = conditionConfiguration;
let foundStyle = this.findStyleByConditionId(conditionConfiguration.id);
if (foundStyle) {
@ -279,13 +367,39 @@ export default {
//we're doing this so that we remove styles for any conditions that have been removed from the condition set
this.conditionalStyles = conditionalStyles;
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) {
let staticStyle = objectStyles && objectStyles.staticStyle;
this.staticStyle = staticStyle || {
style: Object.assign({}, this.initialStyles)
};
if (staticStyle) {
this.staticStyle = {
style: Object.assign({}, this.initialStyles, staticStyle.style)
};
} else {
this.staticStyle = {
style: Object.assign({}, this.initialStyles)
};
}
},
findStyleByConditionId(id) {
return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id);
@ -298,14 +412,19 @@ export default {
let found = this.findStyleByConditionId(conditionStyle.conditionId);
if (found) {
found.style = conditionStyle.style;
this.selectedConditionId = found.conditionId;
this.persist(this.getDomainObjectConditionalStyle());
}
},
getDomainObjectConditionalStyle() {
getDomainObjectConditionalStyle(defaultConditionId) {
let objectStyle = {
styles: this.conditionalStyles,
staticStyle: this.staticStyle
staticStyle: this.staticStyle,
selectedConditionId: this.selectedConditionId
};
if (defaultConditionId) {
objectStyle.defaultConditionId = defaultConditionId;
}
if (this.conditionSetDomainObject) {
objectStyle.conditionSetIdentifier = this.conditionSetDomainObject.identifier;
}
@ -327,6 +446,10 @@ export default {
getCondition(id) {
return this.conditions ? this.conditions[id] : {};
},
applySelectedConditionStyle(conditionId) {
this.selectedConditionId = conditionId;
this.persist(this.getDomainObjectConditionalStyle());
},
persist(style) {
this.openmct.objects.mutate(this.domainObject, 'configuration.objectStyles', style);
}

View File

@ -0,0 +1,270 @@
/*****************************************************************************
* 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";
import isEmpty from 'lodash/isEmpty';
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>
<div class="c-style">
<span class="c-style-thumb"
:class="{ 'is-style-invisible': styleItem.style.isStyleInvisible }"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : styleItem.style ]"
<span :class="[
{ 'is-style-invisible': styleItem.style.isStyleInvisible },
{ '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"
:class="{ 'hide-nice': !styleItem.style.color }"
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
>
ABC
</span>
</span>
<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"
:options="borderColorOption"
@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"
:options="backgroundColorOption"
@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"
:options="colorOption"
@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"
:options="imageUrlOption"
@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"
:options="isStyleInvisibleOption"
@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 ToolbarToggleButton from "@/ui/toolbar/components/toolbar-toggle-button.vue";
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
export default {
name: 'StyleEditor',
@ -83,37 +87,52 @@ export default {
isEditing: {
type: Boolean
},
mixedStyles: {
type: Array,
default() {
return [];
}
},
styleItem: {
type: Object,
required: true
}
},
computed: {
itemStyle() {
return getStylesWithoutNoneValue(this.styleItem.style);
},
borderColorOption() {
let value = this.styleItem.style.border.replace('1px solid ', '');
return {
icon: 'icon-line-horz',
title: STYLE_CONSTANTS.borderColorTitle,
value: this.styleItem.style.border.replace('1px solid ', ''),
value: this.normalizeValueForSwatch(value),
property: 'border',
isEditing: this.isEditing
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('border') > -1
}
},
backgroundColorOption() {
let value = this.styleItem.style.backgroundColor;
return {
icon: 'icon-paint-bucket',
title: STYLE_CONSTANTS.backgroundColorTitle,
value: this.styleItem.style.backgroundColor,
value: this.normalizeValueForSwatch(value),
property: 'backgroundColor',
isEditing: this.isEditing
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('backgroundColor') > -1
}
},
colorOption() {
let value = this.styleItem.style.color;
return {
icon: 'icon-font',
title: STYLE_CONSTANTS.textColorTitle,
value: this.styleItem.style.color,
value: this.normalizeValueForSwatch(value),
property: 'color',
isEditing: this.isEditing
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('color') > -1
}
},
imageUrlOption() {
@ -138,7 +157,8 @@ export default {
property: 'imageUrl',
formKeys: ['url'],
value: {url: this.styleItem.style.imageUrl},
isEditing: this.isEditing
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
}
},
isStyleInvisibleOption() {
@ -163,7 +183,23 @@ export default {
}
},
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) {
value = this.normalizeValueForStyle(value);
if (item.property === 'border') {
value = '1px solid ' + value;
}
@ -172,7 +208,7 @@ export default {
} else {
this.styleItem.style[item.property] = value;
}
this.$emit('persist', this.styleItem);
this.$emit('persist', this.styleItem, item.property);
}
}
}

View File

@ -0,0 +1,601 @@
/*****************************************************************************
* 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 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>
<template v-if="!conditionSetDomainObject">
<div class="c-inspect-styles__header">
Object Style
</div>
<div class="c-inspect-styles__content">
<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>
<button
id="addConditionSet"
class="c-button c-button--major c-toggle-styling-button labeled"
@click="addConditionSet"
>
<span class="c-cs-button__label">Use Conditional Styling...</span>
</button>
</div>
</template>
<template v-else>
<div class="c-inspect-styles__header">
Conditional Object Styles
</div>
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject"
class="c-object-label icon-conditional"
:href="navigateToPath"
@click="navigateOrPreview"
>
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
</a>
<template v-if="isEditing">
<button
id="changeConditionSet"
class="c-button labeled"
@click="addConditionSet"
>
<span class="c-button__label">Change...</span>
</button>
<button class="c-click-icon icon-x"
title="Remove conditional styles"
@click="removeConditionSet"
></button>
</template>
</div>
<div v-if="conditionsLoaded"
class="c-inspect-styles__conditions"
>
<div v-for="(conditionStyle, index) in conditionalStyles"
:key="index"
class="c-inspect-styles__condition"
:class="{'is-current': conditionStyle.conditionId === selectedConditionId}"
@click="applySelectedConditionStyle(conditionStyle.conditionId)"
>
<condition-error :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
/>
<condition-description :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
/>
<style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:is-editing="isEditing"
@persist="updateConditionalStyle"
/>
</div>
</div>
</template>
</div>
</template>
<script>
import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";
import ConditionSetSelectorDialog from "@/plugins/condition/components/inspector/ConditionSetSelectorDialog.vue";
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
import Vue from 'vue';
export default {
name: 'StylesView',
components: {
StyleEditor,
ConditionError,
ConditionDescription
},
inject: [
'openmct',
'selection'
],
data() {
return {
staticStyle: undefined,
isEditing: this.openmct.editor.isEditing(),
mixedStyles: [],
isStaticAndConditionalStyles: false,
conditionalStyles: [],
conditionSetDomainObject: undefined,
conditions: undefined,
conditionsLoaded: false,
navigateToPath: '',
selectedConditionId: ''
}
},
destroyed() {
this.removeListeners();
},
mounted() {
this.items = [];
this.previewAction = new PreviewAction(this.openmct);
this.isMultipleSelection = this.selection.length > 1;
this.getObjectsAndItemsFromSelection();
if (!this.isMultipleSelection) {
let objectStyles = this.getObjectStyles();
this.initializeStaticStyle(objectStyles);
if (objectStyles && objectStyles.conditionSetIdentifier) {
this.openmct.objects.get(objectStyles.conditionSetIdentifier).then(this.initialize);
this.conditionalStyles = objectStyles.styles;
}
} else {
this.initializeStaticStyle();
}
this.openmct.editor.on('isEditing', this.setEditState);
},
methods: {
getObjectStyles() {
let objectStyles;
if (this.domainObjectsById) {
const domainObject = Object.values(this.domainObjectsById)[0];
if (domainObject.configuration && domainObject.configuration.objectStyles) {
objectStyles = domainObject.configuration.objectStyles;
}
} else if (this.items.length) {
const itemId = this.items[0].id;
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) {
objectStyles = this.domainObject.configuration.objectStyles[itemId];
}
} else if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
objectStyles = this.domainObject.configuration.objectStyles;
}
return objectStyles;
},
setEditState(isEditing) {
this.isEditing = isEditing;
if (this.isEditing) {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
} else {
this.subscribeToConditionSet();
}
},
enableConditionSetNav() {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => {
this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.objectPath
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
}
);
},
navigateOrPreview(event) {
// If editing, display condition set in Preview overlay; otherwise nav to it while browsing
if (this.openmct.editor.isEditing()) {
event.preventDefault();
this.previewAction.invoke(this.objectPath);
}
},
isItemType(type, item) {
return item && (item.type === type);
},
hasConditionalStyle(domainObject, layoutItem) {
const id = layoutItem ? layoutItem.id : undefined;
return getConditionSetIdentifierForItem(domainObject, id) !== undefined;
},
getObjectsAndItemsFromSelection() {
let domainObject;
let subObjects = [];
let itemsWithConditionalStyles = 0;
//multiple selection
let itemInitialStyles = [];
let itemStyle;
this.selection.forEach((selectionItem) => {
const item = selectionItem[0].context.item;
const layoutItem = selectionItem[0].context.layoutItem;
const isChildItem = selectionItem.length > 1;
if (!isChildItem) {
domainObject = item;
itemStyle = getApplicableStylesForItem(item);
if (this.hasConditionalStyle(item)) {
itemsWithConditionalStyles += 1;
}
} else {
this.canHide = true;
domainObject = selectionItem[1].context.item;
if (item && !layoutItem || this.isItemType('subobject-view', layoutItem)) {
subObjects.push(item);
itemStyle = getApplicableStylesForItem(item);
if (this.hasConditionalStyle(item)) {
itemsWithConditionalStyles += 1;
}
} else {
itemStyle = getApplicableStylesForItem(domainObject, layoutItem || item);
this.items.push({
id: layoutItem.id,
applicableStyles: itemStyle
});
if (this.hasConditionalStyle(item, layoutItem)) {
itemsWithConditionalStyles += 1;
}
}
}
itemInitialStyles.push(itemStyle);
});
this.isStaticAndConditionalStyles = this.isMultipleSelection && itemsWithConditionalStyles;
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) {
let keys = Object.keys(this.domainObject.configuration.objectStyles || {});
keys.forEach((key) => {
if (this.isKeyItemId(key)) {
if (!(newItems.find(item => item.id === key))) {
this.removeItemStyles(key);
}
}
});
},
isKeyItemId(key) {
return (key !== 'styles') &&
(key !== 'staticStyle') &&
(key !== 'defaultConditionId') &&
(key !== 'selectedConditionId') &&
(key !== 'conditionSetIdentifier');
},
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, '*', (newObject) => {
this.domainObjectsById[id] = JSON.parse(JSON.stringify(newObject));
});
this.unObserveObjects.push(unobserveObject);
},
removeListeners() {
if (this.stopObserving) {
this.stopObserving();
}
if (this.stopObservingItems) {
this.stopObservingItems();
}
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
if (this.unObserveObjects) {
this.unObserveObjects.forEach((unObserveObject) => {
unObserveObject();
});
}
this.unObserveObjects = [];
},
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 : '';
},
initialize(conditionSetDomainObject) {
//If there are new conditions in the conditionSet we need to set those styles to default
this.conditionSetDomainObject = conditionSetDomainObject;
this.enableConditionSetNav();
this.initializeConditionalStyles();
},
initializeConditionalStyles() {
if (!this.conditions) {
this.conditions = {};
}
let conditionalStyles = [];
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
if (conditionConfiguration.isDefault) {
this.selectedConditionId = conditionConfiguration.id;
}
this.conditions[conditionConfiguration.id] = conditionConfiguration;
let foundStyle = this.findStyleByConditionId(conditionConfiguration.id);
if (foundStyle) {
foundStyle.style = Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, foundStyle.style);
conditionalStyles.push(foundStyle);
} else {
conditionalStyles.splice(index, 0, {
conditionId: conditionConfiguration.id,
style: Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles)
});
}
});
//we're doing this so that we remove styles for any conditions that have been removed from the condition set
this.conditionalStyles = conditionalStyles;
this.conditionsLoaded = true;
this.getAndPersistStyles(null, this.selectedConditionId);
if (!this.isEditing) {
this.subscribeToConditionSet();
}
},
//TODO: Double check how this works for single styles
initializeStaticStyle(objectStyles) {
let staticStyle = objectStyles && objectStyles.staticStyle;
if (staticStyle) {
this.staticStyle = {
style: Object.assign({}, this.initialStyles, staticStyle.style)
};
} else {
this.staticStyle = {
style: Object.assign({}, this.initialStyles)
};
}
},
removeItemStyles(itemId) {
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (itemId && domainObjectStyles[itemId]) {
delete domainObjectStyles[itemId];
if (Object.keys(domainObjectStyles).length <= 0) {
domainObjectStyles = undefined;
}
this.persist(this.domainObject, domainObjectStyles);
}
},
findStyleByConditionId(id) {
return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id);
},
getCondition(id) {
return this.conditions ? this.conditions[id] : {};
},
addConditionSet() {
let conditionSetDomainObject;
const handleItemSelection = (item) => {
if (item) {
conditionSetDomainObject = item;
}
};
const dismissDialog = (overlay, initialize) => {
overlay.dismiss();
if (initialize && conditionSetDomainObject) {
this.conditionSetDomainObject = conditionSetDomainObject;
this.conditionalStyles = [];
this.initializeConditionalStyles();
}
};
let vm = new Vue({
provide: {
openmct: this.openmct
},
components: {ConditionSetSelectorDialog},
data() {
return {
handleItemSelection
}
},
template: '<condition-set-selector-dialog @conditionSetSelected="handleItemSelection"></condition-set-selector-dialog>'
}).$mount();
let overlay = this.openmct.overlays.overlay({
element: vm.$el,
size: 'small',
buttons: [
{
label: 'OK',
emphasis: 'true',
callback: () => dismissDialog(overlay, true)
},
{
label: 'Cancel',
callback: () => dismissDialog(overlay, false)
}
],
onDestroy: () => vm.$destroy()
});
},
removeConditionSet() {
this.conditionSetDomainObject = undefined;
this.conditionalStyles = [];
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (this.domainObjectsById) {
const domainObjects = Object.values(this.domainObjectsById);
domainObjects.forEach(domainObject => {
let objectStyles = (domainObject.configuration && domainObject.configuration.objectStyles) || {};
this.removeConditionalStyles(objectStyles);
if (objectStyles && Object.keys(objectStyles).length <= 0) {
objectStyles = undefined;
}
this.persist(domainObject, objectStyles);
});
}
if (this.items.length) {
this.items.forEach((item) => {
const itemId = item.id;
this.removeConditionalStyles(domainObjectStyles, itemId);
if (domainObjectStyles[itemId] && Object.keys(domainObjectStyles[itemId]).length <= 0) {
delete domainObjectStyles[itemId];
}
});
} else {
this.removeConditionalStyles(domainObjectStyles);
}
if (domainObjectStyles && Object.keys(domainObjectStyles).length <= 0) {
domainObjectStyles = undefined;
}
this.persist(this.domainObject, domainObjectStyles);
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
},
removeConditionalStyles(domainObjectStyles, itemId) {
if (itemId && domainObjectStyles[itemId]) {
domainObjectStyles[itemId].conditionSetIdentifier = undefined;
delete domainObjectStyles[itemId].conditionSetIdentifier;
domainObjectStyles[itemId].selectedConditionId = undefined;
domainObjectStyles[itemId].defaultConditionId = undefined;
domainObjectStyles[itemId].styles = undefined;
delete domainObjectStyles[itemId].styles;
} else {
domainObjectStyles.conditionSetIdentifier = undefined;
delete domainObjectStyles.conditionSetIdentifier;
domainObjectStyles.selectedConditionId = undefined;
domainObjectStyles.defaultConditionId = undefined;
domainObjectStyles.styles = undefined;
delete domainObjectStyles.styles;
}
},
updateStaticStyle(staticStyle, property) {
//update the static style for each of the layoutItems as well as each sub object item
this.staticStyle = staticStyle;
this.removeConditionSet();
this.getAndPersistStyles(property);
},
updateConditionalStyle(conditionStyle, property) {
let foundStyle = this.findStyleByConditionId(conditionStyle.conditionId);
if (foundStyle) {
foundStyle.style = conditionStyle.style;
this.selectedConditionId = foundStyle.conditionId;
this.getAndPersistStyles(property);
}
},
getAndPersistStyles(property, defaultConditionId) {
this.persist(this.domainObject, this.getDomainObjectStyle(this.domainObject, property, this.items, defaultConditionId));
if (this.domainObjectsById) {
const domainObjects = Object.values(this.domainObjectsById);
domainObjects.forEach(domainObject => {
this.persist(domainObject, this.getDomainObjectStyle(domainObject, property, null, defaultConditionId));
});
}
if (!this.items.length && !this.domainObjectsById) {
this.persist(this.domainObject, this.getDomainObjectStyle(this.domainObject, property, null, defaultConditionId));
}
this.isStaticAndConditionalStyles = false;
if (property) {
let foundIndex = this.mixedStyles.indexOf(property);
if (foundIndex > -1) {
this.mixedStyles.splice(foundIndex, 1);
}
}
},
getDomainObjectStyle(domainObject, property, items, defaultConditionId) {
let objectStyle = {
styles: this.conditionalStyles,
staticStyle: this.staticStyle,
selectedConditionId: this.selectedConditionId
};
if (defaultConditionId) {
objectStyle.defaultConditionId = defaultConditionId;
}
if (this.conditionSetDomainObject) {
objectStyle.conditionSetIdentifier = this.conditionSetDomainObject.identifier;
}
let domainObjectStyles = (domainObject.configuration && domainObject.configuration.objectStyles) || {};
if (items) {
items.forEach(item => {
let itemStaticStyle = {};
let itemConditionalStyle = { styles: []};
if (!this.conditionSetDomainObject) {
if (domainObjectStyles[item.id] && domainObjectStyles[item.id].staticStyle) {
itemStaticStyle = Object.assign({}, domainObjectStyles[item.id].staticStyle.style);
}
if (item.applicableStyles[property] !== undefined) {
itemStaticStyle[property] = this.staticStyle.style[property];
}
if (Object.keys(itemStaticStyle).length <= 0) {
itemStaticStyle = undefined;
}
domainObjectStyles[item.id] = { staticStyle: { style: itemStaticStyle } };
} else {
objectStyle.styles.forEach((conditionalStyle, index) => {
let style = {};
Object.keys(item.applicableStyles).concat(['isStyleInvisible']).forEach(key => {
style[key] = conditionalStyle.style[key];
});
itemConditionalStyle.styles.push({
...conditionalStyle,
style
});
});
domainObjectStyles[item.id] = {
...domainObjectStyles[item.id],
...objectStyle,
...itemConditionalStyle
};
}
});
} else {
domainObjectStyles = {
...domainObjectStyles,
...objectStyle
};
}
return domainObjectStyles;
},
applySelectedConditionStyle(conditionId) {
this.selectedConditionId = conditionId;
this.getAndPersistStyles();
},
persist(domainObject, style) {
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
}
}
}
</script>

View File

@ -60,6 +60,31 @@
&__condition {
@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.2);
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 {

View File

@ -20,11 +20,12 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import {OPERATIONS} from '../utils/operations';
import {computeCondition} from "@/plugins/condition/utils/evaluator";
import TelemetryCriterion from './TelemetryCriterion';
import { evaluateResults } from "../utils/evaluator";
import { getLatestTimestamp } from '../utils/time';
import { getOperatorText } from "@/plugins/condition/utils/operations";
export default class TelemetryCriterion extends EventEmitter {
export default class AllTelemetryCriterion extends TelemetryCriterion {
/**
* Subscribes/Unsubscribes to telemetry and emits the result
@ -34,23 +35,35 @@ export default class TelemetryCriterion extends EventEmitter {
* @param openmct
*/
constructor(telemetryDomainObjectDefinition, openmct) {
super();
super(telemetryDomainObjectDefinition, openmct);
}
this.openmct = openmct;
this.objectAPI = this.openmct.objects;
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;
initialize() {
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
this.telemetryDataCache = {};
}
updateTelemetry(telemetryObjects) {
this.telemetryObjects = Object.assign({}, telemetryObjects);
isValid() {
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
}
updateTelemetryObjects(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) {
@ -68,105 +81,106 @@ export default class TelemetryCriterion extends EventEmitter {
});
const datum = {
result: computeCondition(this.telemetryDataCache, this.telemetry === 'all')
result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry)
};
if (data) {
// TODO check back to see if we should format times here
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key]
});
}
return datum;
}
handleSubscription(data, telemetryObjects) {
if(this.isValid()) {
this.emitEvent('criterionResultUpdated', this.formatData(data, telemetryObjects));
} else {
this.emitEvent('criterionResultUpdated', this.formatData({}, telemetryObjects));
}
}
getResult(data, telemetryObjects) {
const validatedData = this.isValid() ? data : {};
findOperation(operation) {
for (let i=0; i < OPERATIONS.length; i++) {
if (operation === OPERATIONS[i].name) {
return OPERATIONS[i].operation;
}
if (validatedData) {
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
}
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));
Object.values(telemetryObjects).forEach(telemetryObject => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (this.telemetryDataCache[id] === undefined) {
this.telemetryDataCache[id] = false;
}
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() {
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
requestLAD(telemetryObjects) {
const options = {
strategy: 'latest',
size: 1
};
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
.map(key => this.telemetryAPI.request(
options.telemetryObjects[key],
.map(key => this.openmct.telemetry.request(
telemetryObjects[key],
options
));
let telemetryDataCache = {};
return Promise.all(telemetryRequests)
.then(telemetryRequestsResults => {
let latestDatum;
let latestTimestamp;
const timeSystems = this.openmct.time.getAllTimeSystems();
const timeSystem = this.openmct.time.timeSystem();
telemetryRequestsResults.forEach((results, index) => {
latestDatum = results.length ? results[results.length - 1] : {};
if (index < telemetryRequestsResults.length-1) {
if (latestDatum) {
this.telemetryDataCache[latestDatum.id] = this.computeResult(latestDatum);
}
}
const latestDatum = results.length ? results[results.length - 1] : {};
const datumId = keys[index];
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);
telemetryDataCache[datumId] = this.computeResult(normalizedDatum);
latestTimestamp = getLatestTimestamp(
latestTimestamp,
normalizedDatum,
timeSystems,
timeSystem
);
});
const datum = {
result: evaluateResults(Object.values(telemetryDataCache), this.telemetry),
...latestTimestamp
};
return {
id: this.id,
data: this.formatData(latestDatum, options.telemetryObjects)
data: datum
};
});
}
getDescription() {
const telemetryDescription = this.telemetry === 'all' ? 'all telemetry' : 'any telemetry';
let metadataValue = this.metadata;
let inputValue = this.input;
if (this.metadata) {
const telemetryObjects = Object.values(this.telemetryObjects);
for (let i=0; i < telemetryObjects.length; i++) {
const telemetryObject = telemetryObjects[i];
const metadataObject = this.getMetaDataObject(telemetryObject, this.metadata);
if (metadataObject) {
metadataValue = this.getMetadataValueFromMetaData(metadataObject) || this.metadata;
inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input;
break;
}
}
}
return `${telemetryDescription} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`;
}
destroy() {
this.emitEvent('criterionRemoved');
delete this.telemetryObjects;
delete this.telemetryDataCache;
delete this.telemetryObjectIdAsString;
delete this.telemetryObject;
}
}

View File

@ -21,7 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import {OPERATIONS} from '../utils/operations';
import { OPERATIONS, getOperatorText } from '../utils/operations';
export default class TelemetryCriterion extends EventEmitter {
@ -36,44 +36,89 @@ export default class TelemetryCriterion extends EventEmitter {
super();
this.openmct = openmct;
this.objectAPI = this.openmct.objects;
this.telemetryAPI = this.openmct.telemetry;
this.timeAPI = this.openmct.time;
this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
this.id = telemetryDomainObjectDefinition.id;
this.telemetry = telemetryDomainObjectDefinition.telemetry;
this.operation = telemetryDomainObjectDefinition.operation;
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.telemetryObject = telemetryDomainObjectDefinition.telemetryObject;
this.telemetryObjectIdAsString = this.objectAPI.makeKeyString(telemetryDomainObjectDefinition.telemetry);
this.on(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription);
this.result = undefined;
this.initialize();
this.emitEvent('criterionUpdated', this);
}
updateTelemetry(telemetryObjects) {
initialize() {
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
updateTelemetryObjects(telemetryObjects) {
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) {
const datum = {
result: this.computeResult(data)
};
if (data) {
// TODO check back to see if we should format times here
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key]
});
}
return datum;
}
handleSubscription(data) {
if(this.isValid()) {
this.emitEvent('criterionResultUpdated', this.formatData(data));
} else {
this.emitEvent('criterionResultUpdated', this.formatData({}));
getResult(data) {
const validatedData = this.isValid() ? data : {};
this.result = this.computeResult(validatedData);
}
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) {
@ -95,7 +140,7 @@ export default class TelemetryCriterion extends EventEmitter {
this.input.forEach(input => params.push(input));
}
if (typeof comparator === 'function') {
result = comparator(params);
result = !!comparator(params);
}
}
return result;
@ -108,42 +153,54 @@ export default class TelemetryCriterion extends EventEmitter {
});
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
getMetaDataObject(telemetryObject, metadata) {
let metadataObject;
if (metadata) {
const telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
metadataObject = telemetryMetadata.valueMetadatas.find((valueMetadata) => valueMetadata.key === metadata);
}
return metadataObject;
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
getInputValueFromMetaData(metadataObject, input) {
let inputValue;
if (metadataObject) {
if(metadataObject.enumerations && input.length) {
const enumeration = metadataObject.enumerations[input[0]];
if (enumeration !== undefined && enumeration.string) {
inputValue = [enumeration.string];
}
}
);
}
return inputValue;
}
if (!this.isValid()) {
return {
id: this.id,
data: this.formatData({})
};
getMetadataValueFromMetaData(metadataObject) {
let metadataValue;
if (metadataObject) {
if (metadataObject.name) {
metadataValue = metadataObject.name;
}
}
return metadataValue;
}
getDescription(criterion, index) {
let description;
if (!this.telemetry || !this.telemetryObject || (this.telemetryObject.type === 'unknown')) {
description = `Unknown ${this.metadata} ${getOperatorText(this.operation, this.input)}`;
} else {
const metadataObject = this.getMetaDataObject(this.telemetryObject, this.metadata);
const metadataValue = this.getMetadataValueFromMetaData(metadataObject) || this.metadata;
const inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input;
description = `${this.telemetryObject.name} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`;
}
return this.telemetryAPI.request(
this.telemetryObject,
options
).then(results => {
const latestDatum = results.length ? results[results.length - 1] : {};
return {
id: this.id,
data: this.formatData(latestDatum)
};
});
return description;
}
destroy() {
this.off(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription);
this.emitEvent('criterionRemoved');
delete this.telemetryObjectIdAsString;
delete this.telemetryObject;
delete this.telemetryObjectIdAsString;
}
}

View File

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

View File

@ -20,25 +20,28 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct } from "testTools";
import { createOpenMct, resetApplicationState } from "utils/testing";
import ConditionPlugin from "./plugin";
let openmct = createOpenMct();
openmct.install(new ConditionPlugin());
let conditionSetDefinition;
let mockConditionSetDomainObject;
let element;
let child;
import StylesView from "./components/inspector/StylesView.vue";
import Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils";
describe('the plugin', function () {
let conditionSetDefinition;
let mockConditionSetDomainObject;
let element;
let child;
let openmct;
beforeAll((done) => {
beforeAll(() => {
resetApplicationState(openmct);
});
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(new ConditionPlugin());
conditionSetDefinition = openmct.types.get('conditionSet').definition;
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
element = document.createElement('div');
child = document.createElement('div');
@ -55,7 +58,11 @@ describe('the plugin', function () {
conditionSetDefinition.initialize(mockConditionSetDomainObject);
openmct.on('start', done);
openmct.start(appHolder);
openmct.startHeadless();
});
afterEach(() => {
resetApplicationState(openmct);
});
let mockConditionSetObject = {
@ -94,4 +101,259 @@ describe('the plugin', function () {
});
});
describe('the condition set usage for multiple display layout items', () => {
let displayLayoutItem;
let lineLayoutItem;
let boxLayoutItem;
let selection;
let component;
let styleViewComponentObject;
const conditionSetDomainObject = {
"configuration":{
"conditionTestData":[
{
"telemetry":"",
"metadata":"",
"input":""
}
],
"conditionCollection":[
{
"id":"39584410-cbf9-499e-96dc-76f27e69885d",
"configuration":{
"name":"Unnamed Condition",
"output":"Sine > 0",
"trigger":"all",
"criteria":[
{
"id":"85fbb2f7-7595-42bd-9767-a932266c5225",
"telemetry":{
"namespace":"",
"key":"be0ba97f-b510-4f40-a18d-4ff121d5ea1a"
},
"operation":"greaterThan",
"input":[
"0"
],
"metadata":"sin"
},
{
"id":"35400132-63b0-425c-ac30-8197df7d5862",
"telemetry":"any",
"operation":"enumValueIs",
"input":[
"0"
],
"metadata":"state"
}
]
},
"summary":"Match if all criteria are met: Sine Wave Generator Sine > 0 and any telemetry State is OFF "
},
{
"isDefault":true,
"id":"2532d90a-e0d6-4935-b546-3123522da2de",
"configuration":{
"name":"Default",
"output":"Default",
"trigger":"all",
"criteria":[
]
},
"summary":""
}
]
},
"composition":[
{
"namespace":"",
"key":"be0ba97f-b510-4f40-a18d-4ff121d5ea1a"
},
{
"namespace":"",
"key":"077ffa67-e78f-4e99-80e0-522ac33a3888"
}
],
"telemetry":{
},
"name":"Condition Set",
"type":"conditionSet",
"identifier":{
"namespace":"",
"key":"863012c1-f6ca-4ab0-aed7-fd43d5e4cd12"
}
};
const staticStyle = {
"style":{
"backgroundColor":"#717171",
"border":"1px solid #00ffff"
}
};
const conditionalStyle = {
"conditionId":"39584410-cbf9-499e-96dc-76f27e69885d",
"style":{
"isStyleInvisible":"",
"backgroundColor":"#717171",
"border":"1px solid #ffff00"
}
};
beforeEach(() => {
displayLayoutItem = {
"composition":[
],
"configuration":{
"items":[
{
"fill":"#717171",
"stroke":"",
"x":1,
"y":1,
"width":10,
"height":5,
"type":"box-view",
"id":"89b88746-d325-487b-aec4-11b79afff9e8"
},
{
"x":18,
"y":9,
"x2":23,
"y2":4,
"stroke":"#717171",
"type":"line-view",
"id":"57d49a28-7863-43bd-9593-6570758916f0"
}
],
"layoutGrid":[
10,
10
]
},
"name":"Display Layout",
"type":"layout",
"identifier":{
"namespace":"",
"key":"c5e636c1-6771-4c9c-b933-8665cab189b3"
}
};
lineLayoutItem = {
"x":18,
"y":9,
"x2":23,
"y2":4,
"stroke":"#717171",
"type":"line-view",
"id":"57d49a28-7863-43bd-9593-6570758916f0"
};
boxLayoutItem = {
"fill": "#717171",
"stroke": "",
"x": 1,
"y": 1,
"width": 10,
"height": 5,
"type": "box-view",
"id": "89b88746-d325-487b-aec4-11b79afff9e8"
};
selection = [
[{
context: {
"layoutItem": lineLayoutItem,
"index":1
}
},
{
context: {
"item": displayLayoutItem,
"supportsMultiSelect":true
}
}],
[{
context: {
"layoutItem": boxLayoutItem,
"index": 0
}
},
{
context: {
item: displayLayoutItem,
"supportsMultiSelect":true
}
}]
];
let viewContainer = document.createElement('div');
child.append(viewContainer);
component = new Vue({
provide: {
openmct: openmct,
selection: selection
},
el: viewContainer,
components: {
StylesView
},
template: '<styles-view/>'
});
return Vue.nextTick().then(() => {
styleViewComponentObject = component.$root.$children[0];
styleViewComponentObject.setEditState(true);
});
});
it('initializes the items in the view', () => {
expect(styleViewComponentObject.items.length).toBe(2);
});
it('initializes conditional styles', () => {
styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject;
styleViewComponentObject.conditionalStyles = [];
styleViewComponentObject.initializeConditionalStyles();
expect(styleViewComponentObject.conditionalStyles.length).toBe(2);
});
it('updates applicable conditional styles', () => {
styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject;
styleViewComponentObject.conditionalStyles = [];
styleViewComponentObject.initializeConditionalStyles();
expect(styleViewComponentObject.conditionalStyles.length).toBe(2);
styleViewComponentObject.updateConditionalStyle(conditionalStyle, 'border');
return Vue.nextTick().then(() => {
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
[boxLayoutItem, lineLayoutItem].forEach((item) => {
const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles;
expect(itemStyles.length).toBe(2);
const foundStyle = itemStyles.find((style) => {
return style.conditionId === conditionalStyle.conditionId;
});
expect(foundStyle).toBeDefined();
const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item);
const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']);
Object.keys(foundStyle.style).forEach((key) => {
expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1);
expect(foundStyle.style[key]).toEqual(conditionalStyle.style[key]);
});
});
});
});
it('updates applicable static styles', () => {
styleViewComponentObject.updateStaticStyle(staticStyle, 'border');
return Vue.nextTick().then(() => {
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
[boxLayoutItem, lineLayoutItem].forEach((item) => {
const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle;
expect(itemStyle).toBeDefined();
const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item);
const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']);
Object.keys(itemStyle.style).forEach((key) => {
expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1);
expect(itemStyle.style[key]).toEqual(staticStyle.style[key]);
});
});
});
});
});
});

View File

@ -28,10 +28,17 @@ export const TRIGGER = {
};
export const TRIGGER_LABEL = {
'any': 'when any criteria are met',
'all': 'when all criteria are met',
'not': 'when no criteria are met',
'xor': 'when only one criteria is met'
'any': 'any criteria are met',
'all': 'all criteria are met',
'not': 'no criteria are met',
'xor': 'only one criterion is met'
};
export const TRIGGER_CONJUNCTION = {
'any': 'or',
'all': 'and',
'not': 'and',
'xor': 'or'
};
export const STYLE_CONSTANTS = {

View File

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

View File

@ -20,47 +20,185 @@
* 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 results = {
result: false,
result1: false,
result2: false
};
const result = computeConditionByLimit(results, 0);
expect(result).toBeTrue();
const tests = [
{
name: 'allTrue',
values: [true, true, true, true, true],
any: true,
all: true,
not: false,
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
// }
];
describe(`based on trigger ${TRIGGER.ANY}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.ANY);
expect(result).toEqual(test[TRIGGER.ANY])
});
});
});
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();
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])
});
});
});
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();
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])
});
});
});
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.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

@ -20,7 +20,21 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
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 = [
{
@ -32,7 +46,7 @@ export const OPERATIONS = [
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' is ' + values.join(', ');
return ' is ' + joinValues(values, 1);
}
},
{
@ -44,7 +58,7 @@ export const OPERATIONS = [
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' is not ' + values.join(', ');
return ' is not ' + joinValues(values, 1);
}
},
{
@ -56,7 +70,7 @@ export const OPERATIONS = [
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' > ' + values.join(', ');
return ' > ' + joinValues(values, 1);
}
},
{
@ -68,7 +82,7 @@ export const OPERATIONS = [
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' < ' + values.join(', ');
return ' < ' + joinValues(values, 1);
}
},
{
@ -80,7 +94,7 @@ export const OPERATIONS = [
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' >= ' + values.join(', ');
return ' >= ' + joinValues(values, 1);
}
},
{
@ -92,14 +106,13 @@ export const OPERATIONS = [
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' <= ' + values.join(', ');
return ' <= ' + joinValues(values, 1);
}
},
{
name: 'between',
operation: function (input) {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
let numberInputs = convertToNumbers(input);
let larger = Math.max(...numberInputs.slice(1,3));
let smaller = Math.min(...numberInputs.slice(1,3));
return (numberInputs[0] > smaller) && (numberInputs[0] < larger);
@ -114,8 +127,7 @@ export const OPERATIONS = [
{
name: 'notBetween',
operation: function (input) {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
let numberInputs = convertToNumbers(input);
let larger = Math.max(...numberInputs.slice(1,3));
let smaller = Math.min(...numberInputs.slice(1,3));
return (numberInputs[0] < smaller) || (numberInputs[0] > larger);
@ -136,7 +148,7 @@ export const OPERATIONS = [
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' contains ' + values.join(', ');
return ' contains ' + joinValues(values, 1);
}
},
{
@ -148,7 +160,7 @@ export const OPERATIONS = [
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' does not contain ' + values.join(', ');
return ' does not contain ' + joinValues(values, 1);
}
},
{
@ -160,7 +172,7 @@ export const OPERATIONS = [
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' starts with ' + values.join(', ');
return ' starts with ' + joinValues(values, 1);
}
},
{
@ -172,7 +184,7 @@ export const OPERATIONS = [
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' ends with ' + values.join(', ');
return ' ends with ' + joinValues(values, 1);
}
},
{
@ -184,7 +196,7 @@ export const OPERATIONS = [
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' is exactly ' + values.join(', ');
return ' is exactly ' + joinValues(values, 1);
}
},
{
@ -214,33 +226,36 @@ export const OPERATIONS = [
{
name: 'enumValueIs',
operation: function (input) {
return input[0] === input[1];
let stringInputs = convertToStrings(input);
return stringInputs[0] === stringInputs[1];
},
text: 'is',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' is ' + values.join(', ');
return ' is ' + joinValues(values, 1);
}
},
{
name: 'enumValueIsNot',
operation: function (input) {
return input[0] !== input[1];
let stringInputs = convertToStrings(input);
return stringInputs[0] !== stringInputs[1];
},
text: 'is not',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' is not ' + values.join(', ');
return ' is not ' + joinValues(values, 1);
}
},
{
name: 'valueIs',
name: 'isOneOf',
operation: function (input) {
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
if (input[1]) {
const values = input[1].split(',');
return values.find((value) => input[0].toString() === _.trim(value.toString()));
return values.some((value) => lhsValue === value.toString().trim());
}
return false;
},
@ -252,11 +267,12 @@ export const OPERATIONS = [
}
},
{
name: 'valueIsNot',
name: 'isNotOneOf',
operation: function (input) {
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
if (input[1]) {
const values = input[1].split(',');
const found = values.find((value) => input[0].toString() === _.trim(value.toString()));
const found = values.some((value) => lhsValue === value.toString().trim());
return !found;
}
return false;
@ -274,3 +290,8 @@ export const INPUT_TYPES = {
'string': 'text',
'number': 'number'
};
export const getOperatorText = (operationName, values) => {
const found = OPERATIONS.find((operation) => operation.name === operationName);
return found ? found.getDescription(values) : '';
};

View File

@ -21,12 +21,14 @@
*****************************************************************************/
import { OPERATIONS } from "./operations";
let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIs');
let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIsNot');
let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'isOneOf');
let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'isNotOneOf');
let isBetween = OPERATIONS.find((operation) => operation.name === 'between');
let isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween');
let enumIsOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIs');
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', () => {
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"];
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,169 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import isEmpty from 'lodash/isEmpty';
export const getStyleProp = (key, defaultValue) => {
let styleProp = undefined;
switch(key) {
case 'fill': styleProp = {
backgroundColor: defaultValue || 'transparent'
};
break;
case 'stroke': styleProp = {
border: '1px solid ' + (defaultValue || 'transparent')
};
break;
case 'color': styleProp = {
color: defaultValue || 'transparent'
};
break;
case 'url': styleProp = {
imageUrl: defaultValue || 'transparent'
};
break;
const NONE_VALUE = '__no_value';
const styleProps = {
backgroundColor: {
svgProperty: 'fill',
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view' ||
type === 'telemetry-view' ||
type === 'box-view' ||
type === 'subobject-view');
}
},
border: {
svgProperty: 'stroke',
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view' ||
type === 'telemetry-view' ||
type === 'box-view' ||
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.conditionSetIdentifier) {
return domainObjectStyles.styles;
}
}
};
export const getConditionSetIdentifierForItem = (domainObject, id) => {
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
if (domainObjectStyles) {
if (id) {
if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) {
return domainObjectStyles[id].conditionSetIdentifier;
}
} else if (domainObjectStyles.conditionSetIdentifier) {
return domainObjectStyles.conditionSetIdentifier;
}
}
};
//Returns either existing static styles or uses SVG defaults if available
export const getApplicableStylesForItem = (domainObject, item) => {
const type = item && item.type;
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>
<a class="c-condition-widget"
:href="internalDomainObject.url"
<component :is="urlDefined ? 'a' : 'span'"
class="c-condition-widget"
:href="urlDefined ? internalDomainObject.url : null"
>
<div class="c-condition-widget__label">
{{ internalDomainObject.label }}
</div>
</a>
</component>
</template>
<script>
@ -38,6 +39,11 @@ export default {
internalDomainObject: this.domainObject
}
},
computed: {
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
},

View File

@ -28,10 +28,12 @@
border: 1px solid transparent;
display: inline-block;
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

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