Compare commits

...

139 Commits

Author SHA1 Message Date
Jamie Vigliotta
2d8afa1456 parsing datum itself rather than specific key of datum, let telemetry api do the work 2020-07-24 11:26:28 -07:00
Jamie Vigliotta
f4ef82ef74 Merge branch 'master' into imagery-timestamp-parsing
Merg'n master
2020-07-24 11:23:15 -07:00
Jamie Vigliotta
f43121a38a updating karma timeouts for testing purposes 2020-07-24 10:57:29 -07:00
David Tsay
9124f4f566 allow table row to control object path for context menu actions (#3232)
Need for changes in VISTA
2020-07-24 10:41:36 -07:00
Jamie Vigliotta
9f7d788c86 weird merge issues 2020-07-24 09:36:19 -07:00
Jamie Vigliotta
d2374920ff merging 2020-07-24 09:32:26 -07:00
charlesh88
a6b9ba184c Merge latest master, resolve conflicts and fix constants 2020-07-23 14:33:48 -07:00
Jamie Vigliotta
c00ab72d04 linting fixes 2020-07-23 11:46:37 -07:00
Jamie Vigliotta
98fb507f01 removing "my other items" testing references 2020-07-23 11:43:25 -07:00
Jamie Vigliotta
5258227d87 Merge branch 'new-tree-refactor' of https://github.com/nasa/openmct into new-tree-refactor
Merging
2020-07-23 11:38:05 -07:00
Jamie Vigliotta
f43ba8f8f4 fixed issues when multiple root children exist, added plugin to change the name of the default root object 2020-07-23 11:37:30 -07:00
Charles Hacskaylo
d9baa94970 UI enhancement fixes 2 (#3225)
- Fixed incorrect CSS naming: `c-frame-edit__move` changed to
 `c-frame__move-bar`;
 - Fixed `display: contents` that was erroneously applied to
 `u-angular-object-view-wrapper` and preventing styling from being
 applied to plots, renamed class to `.l-angular-ov-wrapper`;
 - Removed commented CSS;
2020-07-22 20:20:17 -07:00
Shefali Joshi
afeb89a51a [VIPEROMCT-16] Creates a closure for telemetryObject so that requests can resolve correctly even if the telemetryObject is destroyed (#3210)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-07-22 15:22:31 -07:00
johnriedel
07992f0b2a [Tabs View] add ability to remove tabs from tabs view interface (#3147) (#3148)
* add ability to remove tabs from tabs view interface (#3147)

* an "X" on each tab is visible in edit mode

* replaced custom removeDialog with openmct.overlays.dialog

* Minor mods to markup and CSS

- Changed tab from button to div to allow a cleaner approach to the
nested close button;
- Changed close "icon-x" span to a button and added `c-click-icon` style
tag;
- Tweaked class naming to simplify and align with a more functional/
descriptive approach;

* use ES6 arrow func to avoid self=this

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-07-22 15:17:35 -07:00
Deep Tailor
a5c4508578 add minified vue to production (#3183)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2020-07-22 14:47:46 -07:00
Charles Hacskaylo
a4fab3ce8a UI enhancement fixes (#3222)
- Added min-width and min-height to Display Layout lines to allow
 easier selection and move when line is purely vertical or horizontal;
 - Fixed spacing, size and icon of overlay close button;
2020-07-22 11:35:06 -07:00
Charles Hacskaylo
97d80f57cc UI enhancements (#3217)
* UI enhancements for #3176

- Large overlay now displays fullscreen;

* UI enhancements for #3176

- Adding new ".is-in-small-container" CSS - VERY WIP!
- TODO: fix table implementation;

* UI fixes for NIRVSS client #170

- Hide table header filter inputs when table is in small container;

* UI fixes for NIRVSS client #170

- Fixing legends and plot layout when small, and within a stacked plot;
- Add new `hideLegendWhenSmall` property;
- Remove 'hidden' from plot legend position options;
- Reduced opacity of tabular headers in Espresso theme;
- VERY, VERY WIP right now!

* UI fixes for NIRVSS client #170

- Fixing legends and plot layout when small, and within a stacked plot;
- Cleanups, indention, removed commented CSS;
- Tightened up spacing in plot Y axis;

* UI enhancements for #3176

- Move local controls for plots and imagery, prevent overlapping with
 view large button when in a hidden frame in a layout;
 - Finesse local control styling for increased legibility;
 - Move l-state-indicators to avoid overlap with repositioned local
 controls, finesse styling;

* UI enhancements for #3176

- Tweak large overlay close button for better visual alignment;

* UI enhancements for #3176

- Significant improvements to lines in Display Layouts;
- Increased border-width for lines and boxes;
- Code enhanced for proper handling of horizontal and
vertical lines - but still isn't working properly;
- Renamed box-view.scss to box-and-line-views.scss;
- VERY WIP!

* Fixed incorrect grid array reference

* UI enhancements for #3176

- Fixed final issue with Display Layout line drawing object, thank you
@deeptailor!;

* UI enhancements for #3176

- Contrast enhancements and markup normalization for `c-object-label`
elements in main view, Layout frames, Inspector and overlay;
- Enhanced `l-overlay-large` layout;
- Tightened up margins and spacing in plots;
- Refined `is-paused` styling in Telemetry Tables;
- Now hide Telemetry Tables 'Export Data' button if rows are selected,
which use a separate export button;
- Layout frames now hide button's text labels when small;
- Layout frames spacing tightened up and improved;

* UI enhancements for #3176

- Tweak Snow theme constants;

* UI enhancements for #3176

- Fixed ObjectFrame getOverlayElement method, added a wrapper div
around the viewed object to properly control resulting layout in the
overlay;
- Simplified preview CSS to remove background, border and padding;
- Layout tweaks to add space between scrollbar and thumbs in Imagery
view;
- Removed dev "-info" element in LineView.vue;

* UI enhancements for #3176

- Improved styling for 'edit lock' button;

* UI enhancements for #3176

- Show Display Layout frame "-move" bar on hover, rather than select, to
 make it easier to select items with hidden frames, and only show -move
  bar's drag grippy when that frame is selected;
- `pointer-events: none` applied to table's body and plot's plot areas
when placed in a Layout and being edited, prevents distracting
interactions (plot zoom/pan, table row selection) when selecting and
moving elements in a Layout;
- Refined hover styles for c-button to use $filterHov, simplified and
normalized hover styling;
- Converted a number of old `<a>` tags to `<buttons>` to normalize
styling and use the appropriate control;
- Edit lock button is now colored when locked;

* Fix linting issue

* Minor tweaks

- Tweaked control positioning;

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2020-07-21 21:01:38 -07:00
Shefali Joshi
41138a1731 Merge pull request #3205 from nasa/data-dropout-fixes
### Reviewer Checklist

1. Changes appear to address issue? Y
2. Appropriate unit tests included? Y
3. Code style and in-line documentation are appropriate? Y
4. Commit messages meet standards? Y
5. Has associated issue been labelled `unverified`? (only applicable if this PR closes the issue) Y
2020-07-21 10:08:31 -07:00
Shefali Joshi
a54a2f8f84 Merge branch 'master' into data-dropout-fixes 2020-07-21 10:01:01 -07:00
Joel McKinnon
5bbe710552 Merge pull request #3215 from nasa/circle-ci-chromeheadless-test-failure
Removing use of ChromeHeadless and using FirefoxHeadless for Circle CI builds
2020-07-21 09:53:40 -07:00
Joshi
f2d34d7c33 For the short term, removing use of ChromeHeadless and using FirefoxHeadless instead. (added npm dependency)
Also increasing browserNoActivityTimeout to 90000

Resolves #3214
2020-07-20 15:12:53 -07:00
Shefali Joshi
8fa1770885 Merge branch 'master' into data-dropout-fixes 2020-07-20 15:01:11 -07:00
Deep Tailor
7221dc1ac6 make clear data indicator a configurable option (#3206) 2020-07-17 16:48:14 -07:00
Charles Hacskaylo
25bb9939d6 Merge pull request #3193 from nasa/fix-safari-3192
Fix Safari display issues for #3192
2020-07-17 15:59:38 -07:00
charlesh88
0ff9821091 Merge latest master, resolve conflicts 2020-07-17 15:23:36 -07:00
Charles Hacskaylo
e7e12504f2 Merge branch 'master' into fix-safari-3192 2020-07-17 15:19:32 -07:00
Andrew Henry
63bf856d89 Enable persistence operations from Object Providers (#3200)
* Implement 'save' method in Object API

* Refactor legacy persistence code to work with new save object API

* Added 'isPersistable' check to object API

* Fixed incompatibility between object API changes and composition policies

* Make save method private

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-07-17 09:58:03 -07:00
Shefali Joshi
e3dcd51f8d Disallow editor Edit mode when object is locked (#3208)
* Don't allow editor edit if object is locked

* Adds log statement

* Use capture phase for onDragOver

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-07-17 09:23:51 -07:00
charlesh88
353bdb5ca5 Remove unneeded hard-coded CSS color property 2020-07-16 17:16:58 -07:00
Jamie Vigliotta
28a93fb262 some PR comment updates!; 2020-07-16 16:15:11 -07:00
Charles Hacskaylo
cb63f4eca1 Fix is-missing layout problem #3194 (#3195)
- Fixes related to `is-missing` including fixes for Display Layout
alphanumeric views and Tabs view tabs;
2020-07-16 12:43:37 -07:00
Andrew Henry
3f60c3c0f1 Merge branch 'master' into data-dropout-fixes 2020-07-16 11:55:02 -07:00
Andrew Henry
16bb22e834 Added regression test 2020-07-16 11:50:03 -07:00
charlesh88
b1467548da Fix Safari display issues #3192
- Tweaks to fix `c-tab` elements, fix clip-path for webkit;
- Fix Notebook Snapshots header;
2020-07-14 23:40:42 -07:00
charlesh88
baa7c0bc58 Fix Safari display issues #3192
- Tweak to Status area indicator hover bubbles;
2020-07-14 21:51:22 -07:00
charlesh88
73b81e38e7 Fix Safari display issues #3192
- Fix collapsed Status area indicators width problem;
2020-07-14 19:22:18 -07:00
charlesh88
8b088b7a2c Fix Safari display issues #3192
- Fix Status area indicators width problem;
- Also fixes collapsing-status-area-indicator-bubbles transition problem
 as well!;
2020-07-14 18:53:30 -07:00
charlesh88
894da25461 Fix Safari display issues #3192
- Fix Inspector `__content` to properly use flex column layout;
- Change `u-angular-object-view-wrapper` to `display: contents`;
- Fix `gl-plot` to properly use `flex: 1 1 auto` instead of width and
height;
2020-07-14 18:42:52 -07:00
David Tsay
87d63806b9 [Plots] Better Pan/Marquee handling (#3165)
* check for alt key pressed on mouse events

* allows for release of alt key during drag

* eliminates non-browser event states like alt-tab switching apps in windows

* do not listen to plot mouse events on browser context (ctrl) click

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-07-13 12:39:37 -07:00
Charles Hacskaylo
f0e7f8cfc0 Fix incorrect property inspection (#3180)
- Fixed Folder grid and list views;
- Fixed plot collapsed and expanded legends;

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-07-13 11:36:21 -07:00
Deep Tailor
db597e1e93 [Tabs View] Fix tab not being added (#3160) 2020-07-13 10:24:22 -07:00
Andrew Henry
98db273f5d Remove unsubscribe callback 2020-07-10 15:10:33 -07:00
Charles Hacskaylo
8a6f944655 Missing items (#3125)
* Missing objects styling WIP

- Grabbing prior work from `missing-items` branch;

* Missing objects styling WIP

- Grabbing prior work on hover and missing theme constants from
`missing-items` branch;
- Refined theme constants values;
- Renamed relevant mixins and classes from "isUnknown" to "isMissing";
- Applied new hover and missing/unknown styling to Folder-view grid
items;

* Missing objects styling WIP

- Significant refinements and additions to `is-missing`;
- Normalize object type icons as a markup `*__type-icon` to support
styling and positioning of `is-missing__indicator` as a markup element;
- Application to tree items, l-browse-bar in main view, c-object-label,
grid view;
- Change hover approach in grid-items and tree to use filters;

* Missing objects styling WIP

- Styles added to object-name component in Inspector, markup simplified;
- Styles added to Tabs view;

* Missing objects styling WIP

- Simplified and consolidated `is-missing` approach into
`.c-object-label` class;
- Modded `.c-object-label` class to use flex 1 1 auto, instead of 0 1
auto - be on the outlook for regression problems!;
- TODO: wire up `is-missing` for real and Folder List view;

* Missing objects styling WIP

- Added `is-missing` styling to Folder list view;
- Cleanups, simplification and normalization with tree items in
list-item and list-view.scss;
- Using `c-object-label` now in Folder list view;
- Removed too-broad `<a>` color definition in table.scss;

* Missing objects styling WIP

- `is-missing` added to layout frames, with support for hidden
frames and telemetry views.
- Further styles enhancement;
- Continued added wiring points into markup;

* Missing objects styling WIP

- `is-missing` added to mct-plot;
- Significant improvements for cursor lock indicators in plots;

* Missing objects styling WIP

- Plot legend fixes, added overflow scrolling for collapsed and expanded
 legends;
- Removed conmmented code;

* Wire up 'is-missing'

- Added property checks on domainObject for status 'missing';

* Fix linting issues

* remove carat from eslint package

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-07-10 15:08:14 -07:00
Andrew Henry
bacad24811 Delete telemetry cache only when count reaches 0 2020-07-10 13:27:30 -07:00
Deep Tailor
8cc58946cf Remove Karma Report that was accidentally checked in (#3097)
* remove karma report

* add report.*.json to gitignore

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2020-07-10 12:47:52 -07:00
Deep Tailor
3338bc1000 upgrade painterro to +1.00.35 (#3172) 2020-07-09 13:41:52 -07:00
David Tsay
80c20b3d05 [Plots] Allow user to change marker style (#3135)
* lineWidth is not supported in modern browsers

* allow marker shape selection in plot inspector

add shapes for canvas2d - point, cross, star

* change line style to line method in anticipation of adding true line style attribute

* add canvas2d circle marker shape

* allow point shapes for webGL plots

add circle shape

* refactor for clarity

* refactor shape to shape code

* add missing semi-colon

* helper function for marker options display in inspector

refactor for clarity

* access correct file

* add diamond marker shape

* add triangle shape to canvas

* add webgl draw triangle marker shape

* refactor fragment shader for clarity
2020-07-09 13:14:32 -07:00
Joel McKinnon
0d9558b891 Merge pull request #3162 from nasa/display-layout-fix-3161
Fixes issue created when removing Lodash function
2020-07-07 16:54:54 -07:00
Deep Tailor
c29c3c386f fix issue created by lodash upgrade 2020-07-07 15:39:21 -07:00
Jamie V
9ceb3c5b1e Merge pull request #3130 from nasa/display-layout-fix-3128
[Display Layouts] Prevent duplicate from being added when composition 'add' is fired
2020-07-06 11:36:25 -07:00
Jamie Vigliotta
8882e5f3b3 updated addChild and removeChild functions as they were not working at all 2020-07-02 12:06:43 -07:00
Deep Tailor
bee3a9eedf Merge branch 'master' into display-layout-fix-3128 2020-07-02 10:48:53 -07:00
Jamie V
e515d19acd Merge pull request #3144 from nasa/switching-type-error
Fix for non working switch from alpha to tables and plots
2020-07-02 10:38:01 -07:00
Deep Tailor
dd13efe065 Merge branch 'master' into switching-type-error 2020-07-02 10:26:13 -07:00
Jamie Vigliotta
9cfabe0054 Merge branch 'master' into new-tree-refactor
Merging master
2020-07-02 10:15:39 -07:00
Charles Hacskaylo
b5dfbe268c Merge pull request #3146 from nasa/new-folder-action-fix-07012020
Added unnamed folder and made it required
2020-07-01 16:48:10 -07:00
Deep Tailor
a9b9107cc3 change icon and action name 2020-07-01 16:30:03 -07:00
Deep Tailor
cfda4e4214 added unnamed folder and required 2020-07-01 16:20:33 -07:00
Deep Tailor
0a657de4b2 Fix for non working switch from alpha to tables 2020-07-01 16:09:05 -07:00
Shefali Joshi
8153edb9cb Update any/all criterion when telemetry is removed (#3138)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-07-01 12:22:47 -07:00
Jamie V
22ca339fb9 [LADTable] Lad bounds listener FIX (#3114)
* added bounds listener, moved history request to function, checking for race conditions

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-07-01 09:50:18 -07:00
Charles Hacskaylo
7f8764560b Add new glyphs 062320 (#3140)
* Adding new glyphs for multiple branches

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-06-30 16:19:45 -07:00
Charles Hacskaylo
4411bb0a2d UI fixes for NIRVSS #170 (#3141)
* UI fixes for NIRVSS client #170
2020-06-30 16:12:28 -07:00
David Tsay
4ecd264d93 [Time Conductor] add history and select range features (#2932)
* basic brush prototype visible

* require alt pressed for grab handle. display only

* pan and zoom now co-exist

* revert selection to times

* make LocalTimeSystem UTCBased (Earth based)

* add LocalTimeSystem

* make isTimeFixed check reusable

* linting

* zoom axis sets start and end times

* pass isFixed as props so we can watch for change from parent

* disable cursor for local time and enable for fixed time

* linting

* resize brush on window resize

* just use d3-brush instead of entire d3 package

* WIP prototyping conductor history

* set global bounds before emitting change event

* WIP conductor history

* WIP save history to and pull history from local storage

* WIP persistence works

* reset axis height after prototyping

* conductor history functionality complete

* clean up refactoring

* add presets

code cleanup

* axis visual tuning

* remove unused function calls

* change tick to timespan to avoid confusion

* fix bounds to use for timespans on pan axis

* linting

* linting

* more linting

* linting

* change realtime end bound to 30 secondes

* add max duration validation

* Tweaks to Time Conductor History menu

- Enhanced styles for `.c-menu`;
- Added hint messaging and separator;
- Reversed displayed history array so that latest entry is always first;

* refactor to use browser mouse events instead of d3brush

* Styling Time Conductor axis area

- Styles for `is-zooming` state and brush;
- Styles for `is-alt-key-down` for panning;
- Styles for hover modified;

* resolve merge conflicts

* Styling Time Conductor axis and inputs

- Moved panning and zooming styles up into `conductor.scss`;
- Stubbed in :class names in Conductor.vue;
- New theme constants;

* fix merge conflict

* move zoom/pan styling up to conductor

* WIP almost there

* fix zoom

* move altPressed up to parent

* handle no drag on pan

* rename inMode vars for clarity

* Styling for Time Conductor zoom and pan

- Minor fix to hover cursor for alt-pressed panning;

* add configurable bounds limit to time conductor

* add presets and records

* fixes for history

* remove lodash

* add default configurables for examples

* do not install local time system

* cleanup

* fix indentation

remove logging

* remove comments

* section-hint without section-separator styling

* provide reasonable defaults for conductor configuration

* specify input to check validation on

* improve validation

* first check both inputs for valid formats

* clear each valid input on new entry

* tear down listeners

* add user instructions

* allow preset bounds to be declared as callback function

* set this.left on resize

code refactoring

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-30 12:10:35 -07:00
Jamie Vigliotta
7975cf89ef lint fixes 2020-06-29 15:35:59 -07:00
Jamie Vigliotta
a9c0e5a139 dynamic item height for desktop and mobile views 2020-06-29 15:10:39 -07:00
Shefali Joshi
16677c99c9 Add staleness evaluation to conditions. (#3110)
* Add staleness evaluation to conditions.
Add supporting tests
Resolves #3109

* Fix broken test

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-29 14:13:44 -07:00
Deep Tailor
6ab468086a Lock views and prevent editing (#3094)
* working lock and unlock

* prevent flexible layout drop hints from showing

* fix lint issue

* wip

* disable mousedown when not editing in DisplayLayout

* continued wip

* Cherrypick new glyphs from add-new-glyphs-062320

* More new glyphs, updated art

- New glyphs: icon-unlocked and icon-target;
- Updated art for icon-lock glyph;

* Edit toggle refinements WIP

- Markup, CSS in BrowseBar.vue;

* More new glyphs, updated art

- New glyphs: icon-unlocked and icon-target;
- Updated art for icon-lock glyph;

* Edit toggle refinements

- Replaced toggle switch with button;

* prevent styling changes when locked

* fix lint issues

* fix tests

* make reviewer suggested changes

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-06-29 13:14:42 -07:00
charlesh88
92a7ac18a2 Styling for new-tree-refactor
- Using new glyphs for items expand/collapse in Status area;
2020-06-26 18:13:55 -07:00
charlesh88
240bc9e713 Adding new glyphs
- Added `icon-items-collapse` and `icon-items-expand`;
2020-06-26 18:10:26 -07:00
charlesh88
d09013ba28 Styling for new-tree-refactor
- Normalizing status area expand/collapse iconography to match new
approach in panes;
2020-06-26 17:58:27 -07:00
Deep Tailor
9d2991ee10 [Snapshots] Add download as PNG and JPG buttons (#3123)
* working export

* fix lint errors
2020-06-26 17:34:36 -07:00
charlesh88
d99d414c84 Styling for new-tree-refactor
- Fix styles for Snow theme;
- Sync Maelstrom and Espresso themes;
- Remove too-broad `<a>` hover styling from global.scss;
- Disallow pointer-events on `is-navigated` object's label (click on
c-nav__down element still allowed);
2020-06-26 17:17:18 -07:00
charlesh88
2638302dab Styling for new-tree-refactor
- Significant consolidation and cleanups in mct-tree.scss;
- Normalize base and hover styles across new tree, legacy tree,
list-items (used in Notebook) and Folder List View;
- Class naming normalization, change `c-list-item__name-value` to
`c-list-item__name`;
- Add styling to override and remove `<a> outline: dotted` coming from
normalize-min;
- Removed too-broad `<a>` coloring in tables;
2020-06-26 16:28:19 -07:00
Deep Tailor
dadb6120c2 fix lint error 2020-06-26 14:00:39 -07:00
Deep Tailor
d9a94db59d prevent composition from adding a dupe into layout 2020-06-26 13:51:03 -07:00
charlesh88
8241ed8bb8 Styling for new-tree-refactor WIP
- Nav up arrows now left-align properly;
2020-06-25 21:28:46 -07:00
charlesh88
23dffe33a9 Styling for new-tree-refactor WIP
- WIP!
- Refinements to "empty" object element;
- Changed sync-tree icon glyph;
2020-06-25 19:23:21 -07:00
charlesh88
92e08f19bd Merge branch 'new-tree-refactor' of https://github.com/nasa/openmct into new-tree-refactor 2020-06-25 14:12:08 -07:00
Jamie Vigliotta
c644e5266a remove arrows for search results, hightlight "my items" correctly, added empty folder notic 2020-06-25 13:32:09 -07:00
Joel McKinnon
6dd8d448df Merge pull request #3116 from nasa/new-folder-action
New folder action
2020-06-25 13:27:28 -07:00
Deep Tailor
ef2db1edaf Merge branch 'master' into new-folder-action 2020-06-25 13:13:06 -07:00
Deep Tailor
3748927e87 Display layout fixes 062320 (#3111)
* fix for persisting new domainObject

* convert stacked plot to alpha
2020-06-25 11:03:31 -07:00
charlesh88
7fa93c18f3 More new glyphs, updated art
- New glyphs: icon-unlocked and icon-target;
- Updated art for icon-lock glyph;
2020-06-25 10:48:02 -07:00
Deep Tailor
7e4aac028b Merge branch 'master' into new-folder-action 2020-06-25 10:08:26 -07:00
charlesh88
5b6e61d95a Styling for new-tree-refactor WIP
- WIP!
- Significant mods and simplification in pane.vue and assoc CSS for
expand/collapse functionality;
- Wait spinner when in tree: cleanups, simplification;
2020-06-24 19:05:44 -07:00
charlesh88
474b1ed2bf Styling for new-tree-refactor WIP
- WIP!
- Added new `c-click-link` CSS class;
- Move tree sync button into tree pane area;
- Added named "controls" slot to pane.vue;
- _up and _down arrows now use visibility instead of opacity to prevent
accidental clicks;
2020-06-24 16:49:37 -07:00
Jamie V
8e54b8a819 LAD Table (Set) Composition Policy (#2962)
* added LAD Table composition policy, with a check for lad table sets, child can only be lad table
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-24 13:26:22 -07:00
David Tsay
9e5eddec9b [Plots] y-axis width fix (#3112)
* remove lodash

native implementation of lodash max

* remove unused lodash imports

* add 'missing' semi-colon

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-24 11:44:40 -07:00
Deep Tailor
c46e4c5dad Merge branch 'master' into new-folder-action 2020-06-24 09:51:39 -07:00
Jamie V
f0dc928230 Imagery Bug Fixes (Future Date Issues) (#3107)
[Example Imagery] Console error on pause when start and end date is in future #3103

* added some checks for no image

* some code style updates and removing a nested if statement

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2020-06-24 09:47:04 -07:00
charlesh88
0d0de1ed64 Styling for new-tree-refactor WIP
- WIP!
- New glyphs, markup changes in BrowseBar.vue;
- Refinements to tree items, WIP;
- TODO: move hard-coded CSS values into _constants, make
theme-compatible;
2020-06-23 19:30:37 -07:00
Deep Tailor
6f674930d9 remove fdescribe 2020-06-23 18:34:19 -07:00
Deep Tailor
8675fc3fa6 add a few more tests 2020-06-23 18:33:51 -07:00
charlesh88
85ab5cb319 Adding new glyphs
- Multiple new glyphs cherrypicked from branch `add-new-glyphs-062320`;
2020-06-23 18:28:30 -07:00
Deep Tailor
25434342f3 remove unused imports 2020-06-23 16:20:44 -07:00
Deep Tailor
8044dfe726 fix tests 2020-06-23 16:20:28 -07:00
Deep Tailor
cd6c7fdc5e fix broken tests 2020-06-23 15:54:10 -07:00
Deep Tailor
7ff85dc396 remove report 2020-06-23 15:30:37 -07:00
Deep Tailor
fb4877924a remove fdescribe 2020-06-23 15:23:00 -07:00
Deep Tailor
4b13cbdb33 add test 2020-06-23 15:22:43 -07:00
Deep Tailor
51c9328dfd working new folder action 2020-06-23 14:39:19 -07:00
Andrew Henry
31ac67b393 Do not respond to bounds tick changes (#3106)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-23 13:14:17 -07:00
Shefali Joshi
0399766ccd Merge pull request #3074 from nasa/testing-guidelines
Added guidelines to testing documentation
2020-06-23 10:53:39 -07:00
Shefali Joshi
18ab034147 Merge branch 'master' into testing-guidelines 2020-06-23 10:40:58 -07:00
Joel McKinnon
8a4bc2a463 bumped angular to >=1.8.0 (#3100) 2020-06-22 14:56:06 -07:00
Deep Tailor
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
Jamie Vigliotta
299005982f eslint fix 2020-06-18 14:12:04 -07:00
Jamie Vigliotta
8d3b277ef4 modified so search now uses the same container and virtual scroll 2020-06-18 13:39:58 -07:00
Jamie Vigliotta
8eae707833 Merge branch 'new-tree-refactor' of https://github.com/nasa/openmct into new-tree-refactor
Merge.
2020-06-18 10:29:04 -07:00
Jamie Vigliotta
f62b39054a Merge branch 'master' into new-tree-refactor
Merging master.
.
2020-06-18 10:27:33 -07:00
Jamie V
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
Andrew Henry
e9cf337aac Merge branch 'master' into testing-guidelines 2020-06-17 15:38:56 -07:00
Andrew Henry
04a18248c7 Added reference to Angular memory leak best practices 2020-06-17 15:38:17 -07:00
Andrew Henry
d462db60de Add note on convenience function for test cleanup 2020-06-17 15:31:35 -07:00
Andrew Henry
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
Shefali Joshi
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
Andrew Henry
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
Jamie V
7515ce504e Merge branch 'master' into new-tree-refactor 2020-06-17 13:37:16 -07:00
Jamie Vigliotta
0b5ca621ec initial PR review updates 2020-06-17 13:06:18 -07:00
Jamie Vigliotta
0e0644cd1f loading, sync bug, search issues, opitmization 2020-06-17 12:48:41 -07:00
Andrew Henry
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
Jamie Vigliotta
6089ae3531 added scrollTo on load if in viewed objects directory 2020-06-16 17:33:27 -07:00
Jamie Vigliotta
876eb59787 checking domainobject composition for length to verify children instead of composition object itself 2020-06-16 10:29:05 -07:00
Jamie Vigliotta
17be0d7132 removing console logs 2020-06-15 16:34:38 -07:00
Jamie Vigliotta
ace880dd41 Merge branch 'master' into new-tree-refactor
Merging master.
2020-06-15 13:35:52 -07:00
Jamie Vigliotta
dc3781c8e5 scrollTo set for sync, bug fixes, window resize handling 2020-06-15 13:35:12 -07:00
Joel McKinnon
b5aba7ce8f Merge pull request #3096 from nasa/revert-3095-patch-1
Revert "Update API.md"
2020-06-12 10:12:56 -07:00
Joel McKinnon
0db5648e10 Revert "Update API.md" 2020-06-12 10:07:49 -07:00
Joel McKinnon
83325da738 Merge pull request #3095 from willmendil/patch-1
Update API.md
2020-06-12 08:50:23 -07:00
Will
4d1b2f3456 Update API.md
typo
2020-06-12 17:42:39 +02:00
Joel McKinnon
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
Andrew Henry
91a1b3f31d Fixes a grammatical issue in the wording of the contributor's guide 2020-06-10 10:29:47 -07:00
Jamie V
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
Jamie Vigliotta
b76b4e4098 revised new tree refactor, moved most of the logic to mct-tree instead of tree-item 2020-06-09 12:55:00 -07:00
Shefali Joshi
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
Deep Tailor
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
Andrew Henry
8962b0c88b Merge branch 'master' into testing-guidelines 2020-05-28 15:48:12 -07:00
Andrew Henry
3876151a4b Added guidelines to testing documentation
Migrating our testing guidelines into the open source repository in the interests of transparency, and to assist contributors to the project.
2020-05-27 13:57:46 -07:00
193 changed files with 7868 additions and 3196 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,12 +11,12 @@ jobs:
name: Update npm
command: 'sudo npm install -g npm@latest'
- restore_cache:
key: dependency-cache-{{ checksum "package.json" }}
key: dependency-cache-13-{{ checksum "package.json" }}
- run:
name: Installing dependencies (npm install)
command: npm install
- save_cache:
key: dependency-cache-{{ checksum "package.json" }}
key: dependency-cache-13-{{ checksum "package.json" }}
paths:
- node_modules
- run:

3
.gitignore vendored
View File

@@ -37,4 +37,7 @@ protractor/logs
# npm-debug log
npm-debug.log
# karma reports
report.*.json
package-lock.json

View File

@@ -136,7 +136,7 @@ this repository. This is verified by the command line build.
#### Code Guidelines
JavaScript sources in Open MCT should:
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).
@@ -178,7 +178,7 @@ JavaScript sources in Open MCT should:
code, and present these in the following order:
* First, variable declarations and initialization.
* Secondly, imperative statements.
* Finally, the returned value. Functions should only have a single return statement.
* 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
@@ -189,7 +189,7 @@ JavaScript sources in Open MCT should:
```JavaScript
if (responseCode === 401)
```
1. Dont use the ternary operator. Yes it's terse, but there's probably a clearer way of writing it.
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.
@@ -226,9 +226,9 @@ typically from the author of the change and its reviewer.
Automated testing shall occur whenever changes are merged into the main
development branch and must be confirmed alongside any pull request.
Automated tests are typically unit tests which exercise individual software
components. Tests are subject to code review along with the actual
implementation, to ensure that tests are applicable and useful.
Automated tests are tests which exercise plugins, API, and utility classes.
Tests are subject to code review along with the actual implementation, to
ensure that tests are applicable and useful.
Examples of useful tests:
* Tests which replicate bugs (or their root causes) to verify their
@@ -238,8 +238,26 @@ Examples of useful tests:
* Tests which verify expected interactions with other components in the
system.
During automated testing, code coverage metrics will be reported. Line
coverage must remain at or above 80%.
#### Guidelines
* 100% statement coverage is achievable and desirable.
* Do blackbox testing. Test external behaviors, not internal details. Write tests that describe what your plugin is supposed to do. How it does this doesn't matter, so don't test it.
* Unit test specs for plugins should be defined at the plugin level. Start with one test spec per plugin named pluginSpec.js, and as this test spec grows too big, break it up into multiple test specs that logically group related tests.
* Unit tests for API or for utility functions and classes may be defined at a per-source file level.
* Wherever possible only use and mock public API, builtin functions, and UI in your test specs. Do not directly invoke any private functions. ie. only call or mock functions and objects exposed by openmct.* (eg. openmct.telemetry, openmct.objectView, etc.), and builtin browser functions (fetch, requestAnimationFrame, setTimeout, etc.).
* Where builtin functions have been mocked, be sure to clear them between tests.
* Test at an appropriate level of isolation. Eg.
* If youre testing a view, you do not need to test the whole application UI, you can just fetch the view provider using the public API and render the view into an element that you have created.
* You do not need to test that the view switcher works, there should be separate tests for that.
* You do not need to test that telemetry providers work, you can mock openmct.telemetry.request() to feed test data to the view.
* Use your best judgement when deciding on appropriate scope.
* Automated tests for plugins should start by actually installing the plugin being tested, and then test that installing the plugin adds the desired features and behavior to Open MCT, observing the above rules.
* All variables used in a test spec, including any instances of the Open MCT API should be declared inside of an appropriate block scope (not at the root level of the source file), and should be initialized in the relevant beforeEach block. `beforeEach` is preferable to `beforeAll` to avoid leaking of state between tests.
* A `afterEach` or `afterAll` should be used to do any clean up necessary to prevent leakage of state between test specs. This can happen when functions on `window` are wrapped, or when the URL is changed. [A convenience function](https://github.com/nasa/openmct/blob/master/src/utils/testing.js#L59) is provided for resetting the URL and clearing builtin spies between tests.
* If writing unit tests for legacy Angular code be sure to follow [best practices in order to avoid memory leaks](https://www.thecodecampus.de/blog/avoid-memory-leaks-angularjs-unit-tests/).
#### Examples
* [Example of an automated test spec for an object view plugin](https://github.com/nasa/openmct/blob/master/src/plugins/telemetryTable/pluginSpec.js)
* [Example of an automated test spec for API](https://github.com/nasa/openmct/blob/master/src/api/time/TimeAPISpec.js)
### Commit Message Standards

View File

@@ -1,6 +1,6 @@
# Open MCT License
Open MCT, Copyright (c) 2014-2019, United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.
Open MCT, 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.

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

@@ -28,6 +28,16 @@ define([
domain: 2
}
},
// Need to enable "LocalTimeSystem" plugin to make use of this
// {
// key: "local",
// name: "Time",
// format: "local-format",
// source: "utc",
// hints: {
// domain: 3
// }
// },
{
key: "sin",
name: "Sine",
@@ -61,6 +71,15 @@ define([
domain: 1
}
},
{
key: "local",
name: "Time",
format: "utc",
source: "utc",
hints: {
domain: 2
}
},
{
key: "state",
source: "value",

View File

@@ -34,8 +34,8 @@
<body>
</body>
<script>
const FIVE_MINUTES = 5 * 60 * 1000;
const THIRTY_MINUTES = 30 * 60 * 1000;
const THIRTY_SECONDS = 30 * 1000;
const THIRTY_MINUTES = THIRTY_SECONDS * 60;
[
'example/eventGenerator'
@@ -63,7 +63,39 @@
bounds: {
start: Date.now() - THIRTY_MINUTES,
end: Date.now()
}
},
// commonly used bounds can be stored in history
// bounds (start and end) can accept either a milliseconds number
// or a callback function returning a milliseconds number
// a function is useful for invoking Date.now() at exact moment of preset selection
presets: [
{
label: 'Last Day',
bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 24,
end: () => Date.now()
}
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 2,
end: () => Date.now()
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - 1000 * 60 * 60,
end: () => Date.now()
}
}
],
// maximum recent bounds to retain in conductor history
records: 10,
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
limit: 1000 * 60 * 60 * 24
},
{
name: "Realtime",
@@ -71,7 +103,7 @@
clock: 'local',
clockOffsets: {
start: - THIRTY_MINUTES,
end: FIVE_MINUTES
end: THIRTY_SECONDS
}
}
]
@@ -81,7 +113,10 @@
openmct.install(openmct.plugins.LADTable());
openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay']));
openmct.install(openmct.plugins.ObjectMigration());
openmct.install(openmct.plugins.ClearData(['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked']));
openmct.install(openmct.plugins.ClearData(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
{indicator: true}
));
openmct.start();
</script>
</html>

View File

@@ -23,7 +23,7 @@
/*global module,process*/
const devMode = process.env.NODE_ENV !== 'production';
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'FirefoxHeadless'];
const coverageEnabled = process.env.COVERAGE === 'true';
const reporters = ['progress', 'html'];
@@ -95,6 +95,7 @@ module.exports = (config) => {
stats: 'errors-only',
logLevel: 'warn'
},
singleRun: true
singleRun: true,
browserNoActivityTimeout: 400000
});
}

View File

@@ -4,7 +4,7 @@
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {
"angular": "1.7.9",
"angular": ">=1.8.0",
"angular-route": "1.4.14",
"babel-eslint": "8.2.6",
"comma-separated-values": "^3.6.4",
@@ -41,6 +41,7 @@
"jsdoc": "^3.3.2",
"karma": "^2.0.3",
"karma-chrome-launcher": "^2.2.0",
"karma-firefox-launcher": "^1.3.0",
"karma-cli": "^1.0.1",
"karma-coverage": "^1.1.2",
"karma-coverage-istanbul-reporter": "^2.1.1",
@@ -59,7 +60,7 @@
"moment-timezone": "0.5.28",
"node-bourbon": "^4.2.3",
"node-sass": "^4.9.2",
"painterro": "^0.2.65",
"painterro": "^1.0.35",
"printj": "^1.2.1",
"raw-loader": "^0.5.1",
"request": "^2.69.0",
@@ -84,10 +85,10 @@
"build:prod": "cross-env NODE_ENV=production webpack",
"build:dev": "webpack",
"build:watch": "webpack --watch",
"test": "karma start --single-run",
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:coverage": "./scripts/test-coverage.sh",
"test:watch": "karma start --no-single-run",
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"verify": "concurrently 'npm:test' 'npm:lint'",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
"otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",

View File

@@ -31,13 +31,13 @@
</mct-form>
</div>
<div class="c-overlay__button-bar">
<a class='c-button c-button--major'
<button class='c-button c-button--major'
ng-class="{ disabled: !createForm.$valid }"
ng-click="ngModel.confirm()">
OK
</a>
<a class='c-button '
</button>
<button class='c-button '
ng-click="ngModel.cancel()">
Cancel
</a>
</button>
</div>

View File

@@ -31,13 +31,13 @@
</mct-include>
</div>
<div class="c-overlay__button-bar">
<a ng-repeat="option in ngModel.dialog.options"
<button ng-repeat="option in ngModel.dialog.options"
href=''
class="s-button lg"
title="{{option.description}}"
ng-click="ngModel.confirm(option.key)"
ng-class="{ major: $first, subtle: !$first }">
{{option.name}}
</a>
</button>
</div>
</mct-container>

View File

@@ -24,7 +24,7 @@
<div class="c-overlay__outer">
<button ng-click="ngModel.cancel()"
ng-if="ngModel.cancel"
class="c-click-icon c-overlay__close-button icon-x-in-circle"></button>
class="c-click-icon c-overlay__close-button icon-x"></button>
<div class="c-overlay__contents" ng-transclude></div>
</div>
</div>

View File

@@ -81,10 +81,15 @@ define(
* context.
*/
PropertiesAction.appliesTo = function (context) {
var domainObject = (context || {}).domainObject,
type = domainObject && domainObject.getCapability('type'),
creatable = type && type.hasFeature('creation');
if (domainObject && domainObject.model && domainObject.model.locked) {
return false;
}
// Only allow creatable types to be edited
return domainObject && creatable;
};

View File

@@ -36,8 +36,6 @@ define(
}
EditPersistableObjectsPolicy.prototype.allow = function (action, context) {
var identifier;
var provider;
var domainObject = context.domainObject;
var key = action.getMetadata().key;
var category = (context || {}).category;
@@ -46,9 +44,8 @@ define(
// is also invoked during the create process which should be allowed,
// because it may be saved elsewhere
if ((key === 'edit' && category === 'view-control') || key === 'properties') {
identifier = objectUtils.parseKeyString(domainObject.getId());
provider = this.openmct.objects.getProvider(identifier);
return provider.save !== undefined;
let newStyleObject = objectUtils.toNewFormat(domainObject, domainObject.getId());
return this.openmct.objects.isPersistable(newStyleObject);
}
return true;

View File

@@ -43,7 +43,7 @@ define(
);
mockObjectAPI = jasmine.createSpyObj('objectAPI', [
'getProvider'
'isPersistable'
]);
mockAPI = {
@@ -69,34 +69,31 @@ define(
});
it("Applies to edit action", function () {
mockObjectAPI.getProvider.and.returnValue({});
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
expect(mockObjectAPI.isPersistable).not.toHaveBeenCalled();
policy.allow(mockEditAction, testContext);
expect(mockObjectAPI.getProvider).toHaveBeenCalled();
expect(mockObjectAPI.isPersistable).toHaveBeenCalled();
});
it("Applies to properties action", function () {
mockObjectAPI.getProvider.and.returnValue({});
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
expect(mockObjectAPI.isPersistable).not.toHaveBeenCalled();
policy.allow(mockPropertiesAction, testContext);
expect(mockObjectAPI.getProvider).toHaveBeenCalled();
expect(mockObjectAPI.isPersistable).toHaveBeenCalled();
});
it("does not apply to other actions", function () {
mockObjectAPI.getProvider.and.returnValue({});
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
expect(mockObjectAPI.isPersistable).not.toHaveBeenCalled();
policy.allow(mockOtherAction, testContext);
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
expect(mockObjectAPI.isPersistable).not.toHaveBeenCalled();
});
it("Tests object provider for editability", function () {
mockObjectAPI.getProvider.and.returnValue({});
mockObjectAPI.isPersistable.and.returnValue(false);
expect(policy.allow(mockEditAction, testContext)).toBe(false);
expect(mockObjectAPI.getProvider).toHaveBeenCalled();
mockObjectAPI.getProvider.and.returnValue({save: function () {}});
expect(mockObjectAPI.isPersistable).toHaveBeenCalled();
mockObjectAPI.isPersistable.and.returnValue(true);
expect(policy.allow(mockEditAction, testContext)).toBe(true);
});
});

View File

@@ -19,7 +19,13 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="c-object-label">
<div class="c-object-label__type-icon {{type.getCssClass()}}" ng-class="{ 'l-icon-link':location.isLink() }"></div>
<div class="c-object-label"
ng-class="{ 'is-missing': model.status === 'missing' }"
>
<div class="c-object-label__type-icon {{type.getCssClass()}}"
ng-class="{ 'l-icon-link':location.isLink() }"
>
<span class="is-missing__indicator" title="This item is missing"></span>
</div>
<div class='c-object-label__name'>{{model.name}}</div>
</div>

View File

@@ -48,9 +48,8 @@ define(
// prevents editing of objects that cannot be persisted, so we can assume that this
// is a new object.
if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) {
var identifier = objectUtils.parseKeyString(parent.getId());
var provider = this.openmct.objects.getProvider(identifier);
return provider.save !== undefined;
let newStyleObject = objectUtils.toNewFormat(parent, parent.getId());
return this.openmct.objects.isPersistable(newStyleObject);
}
return true;
};

View File

@@ -33,7 +33,7 @@ define(
beforeEach(function () {
objectAPI = jasmine.createSpyObj('objectsAPI', [
'getProvider'
'isPersistable'
]);
mockOpenMCT = {
@@ -51,10 +51,6 @@ define(
'isEditContextRoot'
]);
mockParent.getCapability.and.returnValue(mockEditorCapability);
objectAPI.getProvider.and.returnValue({
save: function () {}
});
persistableCompositionPolicy = new PersistableCompositionPolicy(mockOpenMCT);
});
@@ -65,19 +61,22 @@ define(
it("Does not allow composition for objects that are not persistable", function () {
mockEditorCapability.isEditContextRoot.and.returnValue(false);
objectAPI.isPersistable.and.returnValue(true);
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(true);
objectAPI.getProvider.and.returnValue({});
objectAPI.isPersistable.and.returnValue(false);
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(false);
});
it("Always allows composition of objects in edit mode to support object creation", function () {
mockEditorCapability.isEditContextRoot.and.returnValue(true);
objectAPI.isPersistable.and.returnValue(true);
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(true);
expect(objectAPI.getProvider).not.toHaveBeenCalled();
expect(objectAPI.isPersistable).not.toHaveBeenCalled();
mockEditorCapability.isEditContextRoot.and.returnValue(false);
objectAPI.isPersistable.and.returnValue(true);
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(true);
expect(objectAPI.getProvider).toHaveBeenCalled();
expect(objectAPI.isPersistable).toHaveBeenCalled();
});
});

View File

@@ -297,7 +297,8 @@ define([
"persistenceService",
"identifierService",
"notificationService",
"$q"
"$q",
"openmct"
]
},
{

View File

@@ -20,8 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
function () {
define(["objectUtils"],
function (objectUtils) {
/**
* Defines the `persistence` capability, used to trigger the
@@ -47,6 +47,7 @@ define(
identifierService,
notificationService,
$q,
openmct,
domainObject
) {
// Cache modified timestamp
@@ -58,6 +59,7 @@ define(
this.persistenceService = persistenceService;
this.notificationService = notificationService;
this.$q = $q;
this.openmct = openmct;
}
/**
@@ -66,7 +68,7 @@ define(
*/
function rejectIfFalsey(value, $q) {
if (!value) {
return $q.reject("Error persisting object");
return Promise.reject("Error persisting object");
} else {
return value;
}
@@ -98,7 +100,7 @@ define(
dismissable: true
});
return $q.reject(error);
return Promise.reject(error);
}
/**
@@ -110,34 +112,16 @@ define(
*/
PersistenceCapability.prototype.persist = function () {
var self = this,
domainObject = this.domainObject,
model = domainObject.getModel(),
modified = model.modified,
persisted = model.persisted,
persistenceService = this.persistenceService,
persistenceFn = persisted !== undefined ?
this.persistenceService.updateObject :
this.persistenceService.createObject;
domainObject = this.domainObject;
if (persisted !== undefined && persisted === modified) {
return this.$q.when(true);
}
// Update persistence timestamp...
domainObject.useCapability("mutation", function (m) {
m.persisted = modified;
}, modified);
// ...and persist
return persistenceFn.apply(persistenceService, [
this.getSpace(),
this.getKey(),
domainObject.getModel()
]).then(function (result) {
return rejectIfFalsey(result, self.$q);
}).catch(function (error) {
return notifyOnError(error, domainObject, self.notificationService, self.$q);
});
let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), domainObject.getId());
return this.openmct.objects
.save(newStyleObject)
.then(function (result) {
return rejectIfFalsey(result, self.$q);
}).catch(function (error) {
return notifyOnError(error, domainObject, self.notificationService, self.$q);
});
};
/**

View File

@@ -19,7 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* PersistenceCapabilitySpec. Created by vwoeltje on 11/6/14.
*/
@@ -40,7 +39,8 @@ define(
model,
SPACE = "some space",
persistence,
happyPromise;
mockOpenMCT,
mockNewStyleDomainObject;
function asPromise(value, doCatch) {
return (value || {}).then ? value : {
@@ -56,7 +56,6 @@ define(
}
beforeEach(function () {
happyPromise = asPromise(true);
model = { someKey: "some value", name: "domain object"};
mockPersistenceService = jasmine.createSpyObj(
@@ -94,12 +93,23 @@ define(
},
useCapability: jasmine.createSpy()
};
mockNewStyleDomainObject = Object.assign({}, model);
mockNewStyleDomainObject.identifier = {
namespace: "",
key: id
}
// Simulate mutation capability
mockDomainObject.useCapability.and.callFake(function (capability, mutator) {
if (capability === 'mutation') {
model = mutator(model) || model;
}
});
mockOpenMCT = {};
mockOpenMCT.objects = jasmine.createSpyObj('Object API', ['save']);
mockIdentifierService.parse.and.returnValue(mockIdentifier);
mockIdentifier.getSpace.and.returnValue(SPACE);
mockIdentifier.getKey.and.returnValue(key);
@@ -110,51 +120,28 @@ define(
mockIdentifierService,
mockNofificationService,
mockQ,
mockOpenMCT,
mockDomainObject
);
});
describe("successful persistence", function () {
beforeEach(function () {
mockPersistenceService.updateObject.and.returnValue(happyPromise);
mockPersistenceService.createObject.and.returnValue(happyPromise);
mockOpenMCT.objects.save.and.returnValue(Promise.resolve(true));
});
it("creates unpersisted objects with the persistence service", function () {
// Verify precondition; no call made during constructor
expect(mockPersistenceService.createObject).not.toHaveBeenCalled();
expect(mockOpenMCT.objects.save).not.toHaveBeenCalled();
persistence.persist();
expect(mockPersistenceService.createObject).toHaveBeenCalledWith(
SPACE,
key,
model
);
});
it("updates previously persisted objects with the persistence service", function () {
// Verify precondition; no call made during constructor
expect(mockPersistenceService.updateObject).not.toHaveBeenCalled();
model.persisted = 12321;
persistence.persist();
expect(mockPersistenceService.updateObject).toHaveBeenCalledWith(
SPACE,
key,
model
);
expect(mockOpenMCT.objects.save).toHaveBeenCalledWith(mockNewStyleDomainObject);
});
it("reports which persistence space an object belongs to", function () {
expect(persistence.getSpace()).toEqual(SPACE);
});
it("updates persisted timestamp on persistence", function () {
model.modified = 12321;
persistence.persist();
expect(model.persisted).toEqual(12321);
});
it("refreshes the domain object model from persistence", function () {
var refreshModel = {someOtherKey: "some other value"};
model.persisted = 1;
@@ -165,30 +152,37 @@ define(
it("does not trigger error notification on successful" +
" persistence", function () {
persistence.persist();
expect(mockQ.reject).not.toHaveBeenCalled();
expect(mockNofificationService.error).not.toHaveBeenCalled();
let rejected = false;
return persistence.persist()
.catch(() => rejected = true)
.then(() => {
expect(rejected).toBe(false);
expect(mockNofificationService.error).not.toHaveBeenCalled();
});
});
});
describe("unsuccessful persistence", function () {
var sadPromise = {
then: function (callback) {
return asPromise(callback(0), true);
}
};
beforeEach(function () {
mockPersistenceService.createObject.and.returnValue(sadPromise);
mockOpenMCT.objects.save.and.returnValue(Promise.resolve(false));
});
it("rejects on falsey persistence result", function () {
persistence.persist();
expect(mockQ.reject).toHaveBeenCalled();
let rejected = false;
return persistence.persist()
.catch(() => rejected = true)
.then(() => {
expect(rejected).toBe(true);
});
});
it("notifies user on persistence failure", function () {
persistence.persist();
expect(mockQ.reject).toHaveBeenCalled();
expect(mockNofificationService.error).toHaveBeenCalled();
let rejected = false;
return persistence.persist()
.catch(() => rejected = true)
.then(() => {
expect(rejected).toBe(true);
expect(mockNofificationService.error).toHaveBeenCalled();
});
});
});
});

View File

@@ -40,7 +40,18 @@ define(
}
MoveAction.prototype = Object.create(AbstractComposeAction.prototype);
MoveAction.appliesTo = AbstractComposeAction.appliesTo;
MoveAction.appliesTo = function (context) {
var applicableObject =
context.selectedObject || context.domainObject;
if (applicableObject && applicableObject.model.locked) {
return false;
}
return Boolean(applicableObject &&
applicableObject.hasCapability('context'));
};
return MoveAction;
}

View File

@@ -216,8 +216,14 @@ define(['zepto', 'objectUtils'], function ($, objectUtils) {
};
ImportAsJSONAction.appliesTo = function (context) {
return context.domainObject !== undefined &&
context.domainObject.hasCapability("composition");
let domainObject = context.domainObject;
if (domainObject && domainObject.model.locked) {
return false;
}
return domainObject !== undefined &&
domainObject.hasCapability("composition");
};
return ImportAsJSONAction;

View File

@@ -29,9 +29,9 @@
type="text" tabindex="10000"
ng-model="ngModel.input"
ng-keyup="controller.search()"/>
<a class="c-search__clear-input clear-icon icon-x-in-circle"
<button class="c-search__clear-input clear-icon icon-x-in-circle"
ng-class="{show: !(ngModel.input === '' || ngModel.input === undefined)}"
ng-click="ngModel.input = ''; controller.search()"></a>
ng-click="ngModel.input = ''; controller.search()"></button>
<!-- To prevent double triggering of clicks on click away, render
non-clickable version of the button when menu active-->
<a ng-if="!toggle.isActive()" class="menu-icon context-available"
@@ -45,16 +45,16 @@
</mct-include>
</div>
<a class="c-button c-search__btn-cancel"
<button class="c-button c-search__btn-cancel"
ng-show="!(ngModel.input === '' || ngModel.input === undefined)"
ng-click="ngModel.input = ''; ngModel.checkAll = true; menuController.checkAll(); controller.search()">
Cancel</a>
Cancel</button>
</div>
<div class="active-filter-display flex-elem holder"
ng-class="{invisible: ngModel.filtersString === '' || ngModel.filtersString === undefined || !ngModel.search}">
<a class="clear-filters icon-x-in-circle s-icon-button"
ng-click="ngModel.checkAll = true; menuController.checkAll()"></a>Filtered by: {{ ngModel.filtersString }}
<button class="clear-filters icon-x-in-circle s-icon-button"
ng-click="ngModel.checkAll = true; menuController.checkAll()"></button>Filtered by: {{ ngModel.filtersString }}
</div>
<div class="flex-elem holder results-msg" ng-model="ngModel" ng-show="!loading && ngModel.search">
@@ -72,7 +72,7 @@
ng-model="ngModel"
class="l-flex-row flex-elem grows">
</mct-representation>
<a class="load-more-button s-button vsm" ng-if="controller.areMore()" ng-click="controller.loadMore()">More Results</a>
<button class="load-more-button s-button vsm" ng-if="controller.areMore()" ng-click="controller.loadMore()">More Results</button>
</div>
</div>
</div>

View File

@@ -1,621 +0,0 @@
{
"header": {
"event": "Allocation failed - JavaScript heap out of memory",
"location": "OnFatalError",
"filename": "report.20200527.134750.93992.001.json",
"dumpEventTime": "2020-05-27T13:47:50Z",
"dumpEventTimeStamp": "1590612470877",
"processId": 93992,
"commandLine": [
"node",
"/Users/dtailor/Desktop/openmct/node_modules/.bin/karma",
"start",
"--single-run"
],
"nodejsVersion": "v11.9.0",
"wordSize": 64,
"componentVersions": {
"node": "11.9.0",
"v8": "7.0.276.38-node.16",
"uv": "1.25.0",
"zlib": "1.2.11",
"brotli": "1.0.7",
"ares": "1.15.0",
"modules": "67",
"nghttp2": "1.34.0",
"napi": "4",
"llhttp": "1.0.1",
"http_parser": "2.8.0",
"openssl": "1.1.1a",
"cldr": "34.0",
"icu": "63.1",
"tz": "2018e",
"unicode": "11.0",
"arch": "x64",
"platform": "darwin",
"release": "node"
},
"osVersion": "Darwin 18.7.0 Darwin Kernel Version 18.7.0: Thu Jan 23 06:52:12 PST 2020; root:xnu-4903.278.25~1/RELEASE_X86_64",
"machine": "Darwin 18.7.0 Darwin Kernel Version 18.7.0: Thu Jan 23 06:52:12 PST 2020; root:xnu-4903.278.25~1/RELEASE_X86_64tailor x86_64"
},
"javascriptStack": {
"message": "No stack.",
"stack": [
"Unavailable."
]
},
"nativeStack": [
" [pc=0x10013090e] report::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, v8::Local<v8::String>) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x100063744] node::OnFatalError(char const*, char const*) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x1001a8c47] v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x1001a8be4] v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x1005add42] v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x1005b0273] v8::internal::Heap::CheckIneffectiveMarkCompact(unsigned long, double) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x1005ac7a8] v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x1005aa965] v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x1005b720c] v8::internal::Heap::AllocateRawWithLightRetry(int, v8::internal::AllocationSpace, v8::internal::AllocationAlignment) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x1005b728f] v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationSpace, v8::internal::AllocationAlignment) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x100586484] v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationSpace) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x1008389a4] v8::internal::Runtime_AllocateInNewSpace(int, v8::internal::Object**, v8::internal::Isolate*) [/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node]",
" [pc=0x14acfddcfc7d] "
],
"javascriptHeap": {
"totalMemory": 1479229440,
"totalCommittedMemory": 1477309024,
"usedMemory": 1445511032,
"availableMemory": 50296592,
"memoryLimit": 1526909922,
"heapSpaces": {
"read_only_space": {
"memorySize": 524288,
"committedMemory": 42224,
"capacity": 515584,
"used": 33520,
"available": 482064
},
"new_space": {
"memorySize": 4194304,
"committedMemory": 4194288,
"capacity": 2062336,
"used": 59016,
"available": 2003320
},
"old_space": {
"memorySize": 305860608,
"committedMemory": 305138544,
"capacity": 283264904,
"used": 282942208,
"available": 322696
},
"code_space": {
"memorySize": 6291456,
"committedMemory": 5687328,
"capacity": 5237152,
"used": 5237152,
"available": 0
},
"map_space": {
"memorySize": 5255168,
"committedMemory": 5143024,
"capacity": 2523280,
"used": 2523280,
"available": 0
},
"large_object_space": {
"memorySize": 1157103616,
"committedMemory": 1157103616,
"capacity": 1202204368,
"used": 1154715856,
"available": 47488512
},
"new_large_object_space": {
"memorySize": 0,
"committedMemory": 0,
"capacity": 0,
"used": 0,
"available": 0
}
}
},
"resourceUsage": {
"userCpuSeconds": 43.1616,
"kernelCpuSeconds": 43.1616,
"cpuConsumptionPercent": 5.42705e-06,
"maxRss": 1966080000000,
"pageFaults": {
"IORequired": 245,
"IONotRequired": 832598
},
"fsActivity": {
"reads": 0,
"writes": 0
}
},
"libuv": [
],
"environmentVariables": {
"npm_config_save_dev": "",
"npm_config_legacy_bundling": "",
"npm_config_dry_run": "",
"npm_package_devDependencies_markdown_toc": "^0.11.7",
"npm_config_only": "",
"npm_config_browser": "",
"npm_config_viewer": "man",
"npm_config_commit_hooks": "true",
"npm_package_gitHead": "7126abe7ec1d66d3252f3598fbd6bd27217018bc",
"npm_config_also": "",
"npm_package_scripts_otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
"npm_package_devDependencies_minimist": "^1.1.1",
"npm_config_sign_git_commit": "",
"npm_config_rollback": "true",
"npm_package_devDependencies_fast_sass_loader": "1.4.6",
"TERM_PROGRAM": "Apple_Terminal",
"npm_config_usage": "",
"npm_config_audit": "true",
"npm_package_devDependencies_git_rev_sync": "^1.4.0",
"npm_package_devDependencies_file_loader": "^1.1.11",
"npm_package_devDependencies_d3_selection": "1.3.x",
"NODE": "/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node",
"npm_package_homepage": "https://github.com/nasa/openmct#readme",
"INIT_CWD": "/Users/dtailor/Desktop/openmct",
"NVM_CD_FLAGS": "",
"npm_config_globalignorefile": "/Users/dtailor/.nvm/versions/node/v11.9.0/etc/npmignore",
"npm_package_devDependencies_comma_separated_values": "^3.6.4",
"SHELL": "/bin/bash",
"TERM": "xterm-256color",
"npm_config_init_author_url": "",
"npm_config_shell": "/bin/bash",
"npm_config_maxsockets": "50",
"npm_package_devDependencies_vue_template_compiler": "2.5.6",
"npm_package_devDependencies_style_loader": "^1.0.1",
"npm_package_devDependencies_moment_duration_format": "^2.2.2",
"npm_config_parseable": "",
"npm_config_shrinkwrap": "true",
"npm_config_metrics_registry": "https://registry.npmjs.org/",
"TMPDIR": "/var/folders/ks/ytghmh9x4lj3cchr5km5lhkcb7v9y2/T/",
"npm_config_timing": "",
"npm_config_init_license": "ISC",
"npm_package_scripts_lint": "eslint platform example src --ext .js,.vue openmct.js",
"npm_package_devDependencies_d3_array": "1.2.x",
"Apple_PubSub_Socket_Render": "/private/tmp/com.apple.launchd.PsV6Dfq4Tm/Render",
"npm_config_if_present": "",
"npm_package_devDependencies_concurrently": "^3.6.1",
"TERM_PROGRAM_VERSION": "421.2",
"npm_package_scripts_jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
"npm_config_sign_git_tag": "",
"npm_config_init_author_email": "",
"npm_config_cache_max": "Infinity",
"npm_config_preid": "",
"npm_config_long": "",
"npm_config_local_address": "",
"npm_config_cert": "",
"npm_config_git_tag_version": "true",
"npm_package_devDependencies_exports_loader": "^0.7.0",
"TERM_SESSION_ID": "0630D2FA-BAC2-48D3-A21D-9AB58A79FB14",
"npm_config_noproxy": "",
"npm_config_registry": "https://registry.npmjs.org/",
"npm_config_fetch_retries": "2",
"npm_package_private": "true",
"npm_package_devDependencies_karma_jasmine": "^1.1.2",
"npm_package_repository_url": "git+https://github.com/nasa/openmct.git",
"npm_config_versions": "",
"npm_config_key": "",
"npm_config_message": "%s",
"npm_package_readmeFilename": "README.md",
"npm_package_devDependencies_painterro": "^0.2.65",
"npm_package_scripts_verify": "concurrently 'npm:test' 'npm:lint'",
"npm_package_devDependencies_webpack": "^4.16.2",
"npm_package_devDependencies_eventemitter3": "^1.2.0",
"npm_package_description": "The Open MCT core platform",
"USER": "dtailor",
"NVM_DIR": "/Users/dtailor/.nvm",
"npm_package_license": "Apache-2.0",
"npm_package_scripts_build_dev": "webpack",
"npm_package_devDependencies_webpack_cli": "^3.1.0",
"npm_package_devDependencies_location_bar": "^3.0.1",
"npm_package_devDependencies_jasmine_core": "^3.1.0",
"npm_config_globalconfig": "/Users/dtailor/.nvm/versions/node/v11.9.0/etc/npmrc",
"npm_package_devDependencies_karma": "^2.0.3",
"npm_config_prefer_online": "",
"npm_config_always_auth": "",
"npm_config_logs_max": "10",
"npm_package_devDependencies_angular": "1.7.9",
"SSH_AUTH_SOCK": "/private/tmp/com.apple.launchd.JH8E4KgH06/Listeners",
"npm_package_devDependencies_request": "^2.69.0",
"npm_package_devDependencies_eslint": "5.2.0",
"__CF_USER_TEXT_ENCODING": "0x167DA7C2:0x0:0x0",
"npm_execpath": "/Users/dtailor/.nvm/versions/node/v11.9.0/lib/node_modules/npm/bin/npm-cli.js",
"npm_config_global_style": "",
"npm_config_cache_lock_retries": "10",
"npm_config_cafile": "",
"npm_config_update_notifier": "true",
"npm_package_scripts_test_debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"npm_package_devDependencies_glob": ">= 3.0.0",
"npm_config_heading": "npm",
"npm_config_audit_level": "low",
"npm_package_devDependencies_mini_css_extract_plugin": "^0.4.1",
"npm_package_devDependencies_copy_webpack_plugin": "^4.5.2",
"npm_config_read_only": "",
"npm_config_offline": "",
"npm_config_searchlimit": "20",
"npm_config_fetch_retry_mintimeout": "10000",
"npm_package_devDependencies_webpack_dev_middleware": "^3.1.3",
"npm_config_json": "",
"npm_config_access": "",
"npm_config_argv": "{\"remain\":[],\"cooked\":[\"run\",\"test\"],\"original\":[\"run\",\"test\"]}",
"npm_package_scripts_lint_fix": "eslint platform example src --ext .js,.vue openmct.js --fix",
"npm_package_devDependencies_uuid": "^3.3.3",
"npm_package_devDependencies_karma_coverage": "^1.1.2",
"PATH": "/Users/dtailor/.nvm/versions/node/v11.9.0/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/Users/dtailor/Desktop/openmct/node_modules/.bin:/Users/dtailor/.nvm/versions/node/v11.9.0/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/Users/dtailor/Desktop/openmct/node_modules/.bin:/Users/dtailor/.nvm/versions/node/v11.9.0/bin:/Users/dtailor/.homebrew/bin:/Users/dtailor/local/bin:/Users/dtailor/.homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Users/dtailor/Applications/Visual Studio Code.app/Contents/Resources/app/bin:/opt/local/bin:/Users/dtailor/.homebrew/bin:/Users/dtailor/.homebrew/bin:/Users/dtailor/.homebrew/bin:/Users/dtailor/local/bin:/Users/dtailor/.homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin",
"npm_config_allow_same_version": "",
"npm_config_https_proxy": "",
"npm_config_engine_strict": "",
"npm_config_description": "true",
"npm_package_devDependencies_html2canvas": "^1.0.0-alpha.12",
"_": "/Users/dtailor/Desktop/openmct/node_modules/.bin/karma",
"npm_config_userconfig": "/Users/dtailor/.npmrc",
"npm_config_init_module": "/Users/dtailor/.npm-init.js",
"npm_package_author": "",
"npm_package_devDependencies_karma_chrome_launcher": "^2.2.0",
"npm_package_devDependencies_d3_scale": "1.0.x",
"npm_config_cidr": "",
"npm_package_devDependencies_printj": "^1.2.1",
"PWD": "/Users/dtailor/Desktop/openmct",
"npm_config_user": "377333698",
"npm_config_node_version": "11.9.0",
"npm_package_bugs_url": "https://github.com/nasa/openmct/issues",
"npm_package_scripts_test_watch": "karma start --no-single-run",
"npm_lifecycle_event": "test",
"npm_package_devDependencies_v8_compile_cache": "^1.1.0",
"npm_config_ignore_prepublish": "",
"npm_config_save": "true",
"npm_config_editor": "vi",
"npm_config_auth_type": "legacy",
"npm_package_repository_type": "git",
"npm_package_devDependencies_vue": "2.5.6",
"npm_package_devDependencies_marked": "^0.3.5",
"npm_package_devDependencies_angular_route": "1.4.14",
"npm_package_name": "openmct",
"LANG": "en_US.UTF-8",
"npm_config_script_shell": "",
"npm_config_tag": "latest",
"npm_config_global": "",
"npm_config_progress": "true",
"npm_package_scripts_start": "node app.js",
"npm_package_devDependencies_karma_coverage_istanbul_reporter": "^2.1.1",
"npm_config_ham_it_up": "",
"npm_config_searchstaleness": "900",
"npm_config_optional": "true",
"npm_package_scripts_docs": "npm run jsdoc ; npm run otherdoc",
"npm_package_devDependencies_istanbul_instrumenter_loader": "^3.0.1",
"XPC_FLAGS": "0x0",
"npm_config_save_prod": "",
"npm_config_force": "",
"npm_config_bin_links": "true",
"npm_package_devDependencies_moment": "2.25.3",
"npm_package_devDependencies_karma_webpack": "^3.0.0",
"npm_package_devDependencies_express": "^4.13.1",
"npm_config_searchopts": "",
"npm_package_devDependencies_d3_time": "1.0.x",
"FORCE_COLOR": "2",
"npm_config_node_gyp": "/Users/dtailor/.nvm/versions/node/v11.9.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js",
"npm_config_depth": "Infinity",
"npm_package_scripts_build_prod": "cross-env NODE_ENV=production webpack",
"npm_config_sso_poll_frequency": "500",
"npm_config_rebuild_bundle": "true",
"npm_package_version": "1.0.0-snapshot",
"XPC_SERVICE_NAME": "0",
"npm_config_unicode": "true",
"npm_package_devDependencies_jsdoc": "^3.3.2",
"SHLVL": "4",
"HOME": "/Users/dtailor",
"npm_config_fetch_retry_maxtimeout": "60000",
"npm_package_scripts_test": "karma start --single-run",
"npm_package_devDependencies_zepto": "^1.2.0",
"npm_package_devDependencies_eslint_plugin_vue": "^6.0.0",
"npm_config_ca": "",
"npm_config_tag_version_prefix": "v",
"npm_config_strict_ssl": "true",
"npm_config_sso_type": "oauth",
"npm_config_scripts_prepend_node_path": "warn-only",
"npm_config_save_prefix": "^",
"npm_config_loglevel": "notice",
"npm_package_devDependencies_lodash": "^3.10.1",
"npm_package_devDependencies_karma_cli": "^1.0.1",
"npm_package_devDependencies_d3_color": "1.0.x",
"npm_config_save_exact": "",
"npm_config_dev": "",
"npm_config_group": "1286109195",
"npm_config_fetch_retry_factor": "10",
"npm_package_devDependencies_webpack_hot_middleware": "^2.22.3",
"npm_package_devDependencies_cross_env": "^6.0.3",
"npm_package_devDependencies_babel_eslint": "8.2.6",
"HOMEBREW_PREFIX": "/Users/dtailor/.homebrew",
"npm_config_version": "",
"npm_config_prefer_offline": "",
"npm_config_cache_lock_stale": "60000",
"npm_config_otp": "",
"npm_config_cache_min": "10",
"npm_package_devDependencies_vue_loader": "^15.2.6",
"npm_config_searchexclude": "",
"npm_config_cache": "/Users/dtailor/.npm",
"npm_package_scripts_test_coverage": "./scripts/test-coverage.sh",
"npm_package_devDependencies_d3_interpolate": "1.1.x",
"npm_package_devDependencies_d3_format": "1.2.x",
"LOGNAME": "dtailor",
"npm_lifecycle_script": "karma start --single-run",
"npm_config_color": "true",
"npm_package_devDependencies_node_bourbon": "^4.2.3",
"npm_package_devDependencies_karma_sourcemap_loader": "^0.3.7",
"npm_package_devDependencies_karma_html_reporter": "^0.2.7",
"npm_config_proxy": "",
"npm_config_package_lock": "true",
"npm_package_devDependencies_d3_time_format": "2.1.x",
"npm_package_devDependencies_d3_axis": "1.0.x",
"npm_config_package_lock_only": "",
"npm_package_devDependencies_moment_timezone": "0.5.28",
"npm_config_save_optional": "",
"NVM_BIN": "/Users/dtailor/.nvm/versions/node/v11.9.0/bin",
"npm_config_ignore_scripts": "",
"npm_config_user_agent": "npm/6.5.0 node/v11.9.0 darwin x64",
"npm_package_devDependencies_imports_loader": "^0.8.0",
"npm_package_devDependencies_file_saver": "^1.3.8",
"npm_config_cache_lock_wait": "10000",
"npm_config_production": "",
"npm_package_scripts_build_watch": "webpack --watch",
"DISPLAY": "/private/tmp/com.apple.launchd.E3N8oC6RMf/org.macosforge.xquartz:0",
"npm_config_send_metrics": "",
"npm_config_save_bundle": "",
"npm_package_scripts_prepare": "npm run build:prod",
"npm_config_node_options": "",
"npm_config_umask": "0022",
"npm_config_init_version": "1.0.0",
"npm_package_devDependencies_split": "^1.0.0",
"npm_package_devDependencies_raw_loader": "^0.5.1",
"npm_config_init_author_name": "",
"npm_config_git": "git",
"npm_config_scope": "",
"npm_package_scripts_clean": "rm -rf ./dist",
"npm_package_devDependencies_node_sass": "^4.9.2",
"npm_package_devDependencies_css_loader": "^1.0.0",
"DISABLE_UPDATE_CHECK": "1",
"npm_config_onload_script": "",
"npm_config_unsafe_perm": "true",
"npm_config_tmp": "/var/folders/ks/ytghmh9x4lj3cchr5km5lhkcb7v9y2/T",
"npm_package_devDependencies_d3_collection": "1.0.x",
"npm_node_execpath": "/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node",
"npm_config_link": "",
"npm_config_prefix": "/Users/dtailor/.nvm/versions/node/v11.9.0",
"npm_package_devDependencies_html_loader": "^0.5.5"
},
"userLimits": {
"core_file_size_blocks": {
"soft": 0,
"hard": "unlimited"
},
"data_seg_size_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
},
"file_size_blocks": {
"soft": "unlimited",
"hard": "unlimited"
},
"max_locked_memory_bytes": {
"soft": "unlimited",
"hard": "unlimited"
},
"max_memory_size_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
},
"open_files": {
"soft": 24576,
"hard": "unlimited"
},
"stack_size_bytes": {
"soft": 8388608,
"hard": 67104768
},
"cpu_time_seconds": {
"soft": "unlimited",
"hard": "unlimited"
},
"max_user_processes": {
"soft": 1418,
"hard": 2128
},
"virtual_memory_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
}
},
"sharedObjects": [
"/Users/dtailor/.nvm/versions/node/v11.9.0/bin/node",
"/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation",
"/usr/lib/libSystem.B.dylib",
"/usr/lib/libc++.1.dylib",
"/usr/lib/libobjc.A.dylib",
"/usr/lib/libDiagnosticMessagesClient.dylib",
"/usr/lib/libicucore.A.dylib",
"/usr/lib/libz.1.dylib",
"/usr/lib/libc++abi.dylib",
"/usr/lib/system/libcache.dylib",
"/usr/lib/system/libcommonCrypto.dylib",
"/usr/lib/system/libcompiler_rt.dylib",
"/usr/lib/system/libcopyfile.dylib",
"/usr/lib/system/libcorecrypto.dylib",
"/usr/lib/system/libdispatch.dylib",
"/usr/lib/system/libdyld.dylib",
"/usr/lib/system/libkeymgr.dylib",
"/usr/lib/system/liblaunch.dylib",
"/usr/lib/system/libmacho.dylib",
"/usr/lib/system/libquarantine.dylib",
"/usr/lib/system/libremovefile.dylib",
"/usr/lib/system/libsystem_asl.dylib",
"/usr/lib/system/libsystem_blocks.dylib",
"/usr/lib/system/libsystem_c.dylib",
"/usr/lib/system/libsystem_configuration.dylib",
"/usr/lib/system/libsystem_coreservices.dylib",
"/usr/lib/system/libsystem_darwin.dylib",
"/usr/lib/system/libsystem_dnssd.dylib",
"/usr/lib/system/libsystem_info.dylib",
"/usr/lib/system/libsystem_m.dylib",
"/usr/lib/system/libsystem_malloc.dylib",
"/usr/lib/system/libsystem_networkextension.dylib",
"/usr/lib/system/libsystem_notify.dylib",
"/usr/lib/system/libsystem_sandbox.dylib",
"/usr/lib/system/libsystem_secinit.dylib",
"/usr/lib/system/libsystem_kernel.dylib",
"/usr/lib/system/libsystem_platform.dylib",
"/usr/lib/system/libsystem_pthread.dylib",
"/usr/lib/system/libsystem_symptoms.dylib",
"/usr/lib/system/libsystem_trace.dylib",
"/usr/lib/system/libunwind.dylib",
"/usr/lib/system/libxpc.dylib",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices",
"/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics",
"/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO",
"/System/Library/Frameworks/ColorSync.framework/Versions/A/ColorSync",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/ATS",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSyncLegacy.framework/Versions/A/ColorSyncLegacy",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/HIServices",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/LangAnalysis.framework/Versions/A/LangAnalysis",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/PrintCore",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/QD.framework/Versions/A/QD",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/SpeechSynthesis.framework/Versions/A/SpeechSynthesis",
"/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight",
"/System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface",
"/usr/lib/libxml2.2.dylib",
"/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate",
"/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation",
"/usr/lib/libcompression.dylib",
"/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration",
"/System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay",
"/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit",
"/System/Library/Frameworks/Metal.framework/Versions/A/Metal",
"/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders",
"/System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/A/MultitouchSupport",
"/System/Library/Frameworks/Security.framework/Versions/A/Security",
"/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore",
"/usr/lib/libbsm.0.dylib",
"/usr/lib/liblzma.5.dylib",
"/usr/lib/libauto.dylib",
"/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration",
"/usr/lib/libarchive.2.dylib",
"/usr/lib/liblangid.dylib",
"/usr/lib/libCRFSuite.dylib",
"/usr/lib/libenergytrace.dylib",
"/usr/lib/system/libkxld.dylib",
"/System/Library/PrivateFrameworks/AppleFSCompression.framework/Versions/A/AppleFSCompression",
"/usr/lib/libOpenScriptingUtil.dylib",
"/usr/lib/libcoretls.dylib",
"/usr/lib/libcoretls_cfhelpers.dylib",
"/usr/lib/libpam.2.dylib",
"/usr/lib/libsqlite3.dylib",
"/usr/lib/libxar.1.dylib",
"/usr/lib/libbz2.1.0.dylib",
"/usr/lib/libnetwork.dylib",
"/usr/lib/libapple_nghttp2.dylib",
"/usr/lib/libpcap.A.dylib",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/FSEvents",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/OSServices.framework/Versions/A/OSServices",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SearchKit.framework/Versions/A/SearchKit",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/AE.framework/Versions/A/AE",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList",
"/System/Library/Frameworks/NetFS.framework/Versions/A/NetFS",
"/System/Library/PrivateFrameworks/NetAuth.framework/Versions/A/NetAuth",
"/System/Library/PrivateFrameworks/login.framework/Versions/A/Frameworks/loginsupport.framework/Versions/A/loginsupport",
"/System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC",
"/System/Library/PrivateFrameworks/CoreNLP.framework/Versions/A/CoreNLP",
"/System/Library/PrivateFrameworks/MetadataUtilities.framework/Versions/A/MetadataUtilities",
"/usr/lib/libmecabra.dylib",
"/usr/lib/libmecab.1.0.0.dylib",
"/usr/lib/libgermantok.dylib",
"/usr/lib/libThaiTokenizer.dylib",
"/usr/lib/libChineseTokenizer.dylib",
"/usr/lib/libiconv.2.dylib",
"/usr/lib/libcharset.1.dylib",
"/System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling",
"/System/Library/PrivateFrameworks/CoreEmoji.framework/Versions/A/CoreEmoji",
"/System/Library/PrivateFrameworks/Lexicon.framework/Versions/A/Lexicon",
"/System/Library/PrivateFrameworks/LinguisticData.framework/Versions/A/LinguisticData",
"/usr/lib/libcmph.dylib",
"/System/Library/Frameworks/CoreData.framework/Versions/A/CoreData",
"/System/Library/Frameworks/OpenDirectory.framework/Versions/A/Frameworks/CFOpenDirectory.framework/Versions/A/CFOpenDirectory",
"/System/Library/PrivateFrameworks/APFS.framework/Versions/A/APFS",
"/usr/lib/libutil.dylib",
"/System/Library/Frameworks/ServiceManagement.framework/Versions/A/ServiceManagement",
"/System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Versions/A/BackgroundTaskManagement",
"/usr/lib/libxslt.1.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/vImage",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvMisc.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLinearAlgebra.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparseBLAS.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libQuadrature.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBNNS.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparse.dylib",
"/System/Library/PrivateFrameworks/GPUWrangler.framework/Versions/A/GPUWrangler",
"/System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator",
"/System/Library/PrivateFrameworks/IOPresentment.framework/Versions/A/IOPresentment",
"/System/Library/PrivateFrameworks/DSExternalDisplay.framework/Versions/A/DSExternalDisplay",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreFSCache.dylib",
"/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSCore.framework/Versions/A/MPSCore",
"/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSImage.framework/Versions/A/MPSImage",
"/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSNeuralNetwork.framework/Versions/A/MPSNeuralNetwork",
"/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSMatrix.framework/Versions/A/MPSMatrix",
"/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSRayIntersector.framework/Versions/A/MPSRayIntersector",
"/System/Library/PrivateFrameworks/MetalTools.framework/Versions/A/MetalTools",
"/System/Library/PrivateFrameworks/AggregateDictionary.framework/Versions/A/AggregateDictionary",
"/usr/lib/libMobileGestalt.dylib",
"/System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage",
"/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL",
"/System/Library/PrivateFrameworks/GraphVisualizer.framework/Versions/A/GraphVisualizer",
"/System/Library/PrivateFrameworks/FaceCore.framework/Versions/A/FaceCore",
"/System/Library/Frameworks/OpenCL.framework/Versions/A/OpenCL",
"/usr/lib/libFosl_dynamic.dylib",
"/System/Library/PrivateFrameworks/OTSVG.framework/Versions/A/OTSVG",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontParser.dylib",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontRegistry.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libRadiance.dylib",
"/System/Library/PrivateFrameworks/AppleJPEG.framework/Versions/A/AppleJPEG",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGFXShared.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLU.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLImage.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCVMSPluginSupport.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreVMClient.dylib",
"/usr/lib/libcups.2.dylib",
"/System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos",
"/System/Library/Frameworks/GSS.framework/Versions/A/GSS",
"/usr/lib/libresolv.9.dylib",
"/System/Library/PrivateFrameworks/Heimdal.framework/Versions/A/Heimdal",
"/usr/lib/libheimdal-asn1.dylib",
"/System/Library/Frameworks/OpenDirectory.framework/Versions/A/OpenDirectory",
"/System/Library/PrivateFrameworks/CommonAuth.framework/Versions/A/CommonAuth",
"/System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation",
"/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio",
"/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox",
"/System/Library/PrivateFrameworks/AppleSauce.framework/Versions/A/AppleSauce",
"/System/Library/PrivateFrameworks/AssertionServices.framework/Versions/A/AssertionServices",
"/System/Library/PrivateFrameworks/BaseBoard.framework/Versions/A/BaseBoard"
]
}

View File

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

View File

@@ -266,7 +266,9 @@ 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());
this.install(this.plugins.NewFolderAction());
}
MCT.prototype = Object.create(EventEmitter.prototype);
@@ -433,6 +435,10 @@ define([
plugin(this);
};
MCT.prototype.destroy = function () {
this.emit('destroy');
};
MCT.prototype.plugins = plugins;
return MCT;

View File

@@ -23,7 +23,7 @@
define([
'./plugins/plugins',
'legacyRegistry',
'testUtils'
'utils/testing'
], function (plugins, legacyRegistry, testUtils) {
describe("MCT", function () {
var openmct;
@@ -32,6 +32,10 @@ define([
var mockListener;
var oldBundles;
beforeAll(() => {
testUtils.resetApplicationState();
});
beforeEach(function () {
mockPlugin = jasmine.createSpy('plugin');
mockPlugin2 = jasmine.createSpy('plugin2');
@@ -52,6 +56,7 @@ define([
legacyRegistry.delete(bundle);
}
});
testUtils.resetApplicationState(openmct);
});
it("exposes plugins", function () {

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

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

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

@@ -25,10 +25,11 @@ define([
], function (
utils
) {
function ObjectServiceProvider(eventEmitter, objectService, instantiate, topic) {
function ObjectServiceProvider(eventEmitter, objectService, instantiate, topic, $injector) {
this.eventEmitter = eventEmitter;
this.objectService = objectService;
this.instantiate = instantiate;
this.$injector = $injector;
this.generalTopic = topic('mutation');
this.bridgeEventBuses();
@@ -68,16 +69,53 @@ define([
removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
};
ObjectServiceProvider.prototype.save = function (object) {
var key = object.key;
ObjectServiceProvider.prototype.create = async function (object) {
let model = utils.toOldFormat(object);
return object.getCapability('persistence')
.persist()
.then(function () {
return utils.toNewFormat(object, key);
});
return this.getPersistenceService().createObject(
this.getSpace(utils.makeKeyString(object.identifier)),
object.identifier.key,
model
);
}
ObjectServiceProvider.prototype.update = async function (object) {
let model = utils.toOldFormat(object);
return this.getPersistenceService().updateObject(
this.getSpace(utils.makeKeyString(object.identifier)),
object.identifier.key,
model
);
}
/**
* Get the space in which this domain object is persisted;
* this is useful when, for example, decided which space a
* newly-created domain object should be persisted to (by
* default, this should be the space of its containing
* object.)
*
* @returns {string} the name of the space which should
* be used to persist this object
*/
ObjectServiceProvider.prototype.getSpace = function (keystring) {
return this.getIdentifierService().parse(keystring).getSpace();
};
ObjectServiceProvider.prototype.getIdentifierService = function () {
if (this.identifierService === undefined) {
this.identifierService = this.$injector.get('identifierService');
}
return this.identifierService;
};
ObjectServiceProvider.prototype.getPersistenceService = function () {
if (this.persistenceService === undefined) {
this.persistenceService = this.$injector.get('persistenceService');
}
return this.persistenceService;
}
ObjectServiceProvider.prototype.delete = function (object) {
// TODO!
};
@@ -118,7 +156,8 @@ define([
eventEmitter,
objectService,
instantiate,
topic
topic,
openmct.$injector
)
);

View File

@@ -47,7 +47,7 @@ define([
this.eventEmitter = new EventEmitter();
this.providers = {};
this.rootRegistry = new RootRegistry();
this.rootProvider = new RootObjectProvider(this.rootRegistry);
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
}
/**
@@ -101,14 +101,25 @@ define([
*/
/**
* Save this domain object in its current state.
* Create the given domain object in the corresponding persistence store
*
* @method save
* @method create
* @memberof module:openmct.ObjectProvider#
* @param {module:openmct.DomainObject} domainObject the domain object to
* save
* create
* @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved
* has been created, or be rejected if it cannot be saved
*/
/**
* Update this domain object in its persistence store
*
* @method update
* @memberof module:openmct.ObjectProvider#
* @param {module:openmct.DomainObject} domainObject the domain object to
* update
* @returns {Promise} a promise which will resolve when the domain object
* has been updated, or be rejected if it cannot be saved
*/
/**
@@ -161,8 +172,41 @@ define([
throw new Error('Delete not implemented');
};
ObjectAPI.prototype.save = function () {
throw new Error('Save not implemented');
ObjectAPI.prototype.isPersistable = function (domainObject) {
let provider = this.getProvider(domainObject.identifier);
return provider !== undefined &&
provider.create !== undefined &&
provider.update !== undefined;
}
/**
* Save this domain object in its current state. EXPERIMENTAL
*
* @private
* @memberof module:openmct.ObjectAPI#
* @param {module:openmct.DomainObject} domainObject the domain object to
* save
* @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved
*/
ObjectAPI.prototype.save = function (domainObject) {
let provider = this.getProvider(domainObject.identifier);
let result;
if (!this.isPersistable(domainObject)) {
result = Promise.reject('Object provider does not support saving');
} else if (hasAlreadyBeenPersisted(domainObject)) {
result = Promise.resolve(true);
} else {
if (domainObject.persisted === undefined) {
this.mutate(domainObject, 'persisted', domainObject.modified);
result = provider.create(domainObject);
} else {
this.mutate(domainObject, 'persisted', domainObject.modified);
result = provider.update(domainObject);
}
}
return result;
};
/**
@@ -276,5 +320,9 @@ define([
* @memberof module:openmct
*/
function hasAlreadyBeenPersisted(domainObject) {
return domainObject.persisted !== undefined &&
domainObject.persisted === domainObject.modified;
}
return ObjectAPI;
});

View File

@@ -0,0 +1,60 @@
import ObjectAPI from './ObjectAPI.js';
describe("The Object API", () => {
let objectAPI;
let mockDomainObject;
const TEST_NAMESPACE = "test-namespace";
const FIFTEEN_MINUTES = 15 * 60 * 1000;
beforeEach(() => {
objectAPI = new ObjectAPI();
mockDomainObject = {
identifier: {
namespace: TEST_NAMESPACE,
key: "test-key"
},
name: "test object",
type: "test-type"
};
})
describe("The save function", () => {
it("Rejects if no provider available", () => {
let rejected = false;
return objectAPI.save(mockDomainObject)
.catch(() => rejected = true)
.then(() => expect(rejected).toBe(true));
});
describe("when a provider is available", () => {
let mockProvider;
beforeEach(() => {
mockProvider = jasmine.createSpyObj("mock provider", [
"create",
"update"
]);
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
})
it("Calls 'create' on provider if object is new", () => {
objectAPI.save(mockDomainObject);
expect(mockProvider.create).toHaveBeenCalled();
expect(mockProvider.update).not.toHaveBeenCalled();
});
it("Calls 'update' on provider if object is not new", () => {
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.modified = Date.now();
objectAPI.save(mockDomainObject);
expect(mockProvider.create).not.toHaveBeenCalled();
expect(mockProvider.update).toHaveBeenCalled();
});
it("Does not persist if the object is unchanged", () => {
mockDomainObject.persisted =
mockDomainObject.modified = Date.now();
objectAPI.save(mockDomainObject);
expect(mockProvider.create).not.toHaveBeenCalled();
expect(mockProvider.update).not.toHaveBeenCalled();
});
});
})
});

View File

@@ -20,28 +20,37 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
], function (
) {
function RootObjectProvider(rootRegistry) {
this.rootRegistry = rootRegistry;
class RootObjectProvider {
constructor(rootRegistry) {
if(!RootObjectProvider.instance) {
this.rootRegistry = rootRegistry;
this.rootObject = {
identifier: {
key: "ROOT",
namespace: ""
},
name: 'The root object',
type: 'root',
composition: []
};
RootObjectProvider.instance = this;
}
return RootObjectProvider.instance;
}
RootObjectProvider.prototype.get = function () {
return this.rootRegistry.getRoots()
.then(function (roots) {
return {
identifier: {
key: "ROOT",
namespace: ""
},
name: 'The root object',
type: 'root',
composition: roots
};
});
};
updateName(name) {
this.rootObject.name = name
}
return RootObjectProvider;
});
async get() {
let roots = await this.rootRegistry.getRoots();
this.rootObject.composition = roots;
return this.rootObject;
}
}
const instance = function (rootRegistry) {
return new RootObjectProvider(rootRegistry);
}
export default instance;

View File

@@ -31,7 +31,7 @@ define([
beforeEach(function () {
rootRegistry = jasmine.createSpyObj('rootRegistry', ['getRoots']);
rootRegistry.getRoots.and.returnValue(Promise.resolve(['some root']));
rootObjectProvider = new RootObjectProvider(rootRegistry);
rootObjectProvider = new RootObjectProvider.default(rootRegistry);
});
it('supports fetching root', function () {

View File

@@ -7,7 +7,7 @@
<div class="c-overlay__outer">
<button
v-if="dismissable"
class="c-click-icon c-overlay__close-button icon-x-in-circle"
class="c-click-icon c-overlay__close-button icon-x"
@click="destroy"
></button>
<div

View File

@@ -29,13 +29,12 @@
}
&__close-button {
$p: $interiorMargin;
border-radius: 100% !important;
$p: $interiorMargin + 2px;
color: $overlayColorFg;
display: inline-block;
font-size: 1.25em;
font-size: 1.5em;
position: absolute;
top: $p; right: $p;
z-index: 99;
}
&__contents {
@@ -43,7 +42,7 @@
display: flex;
flex-direction: column;
outline: none;
overflow: hidden;
overflow: auto;
}
&__top-bar {
@@ -87,6 +86,10 @@
.c-click-icon {
filter: $overlayBrightnessAdjust;
}
.c-object-label__name {
filter: $objectLabelNameFilter;
}
}
body.desktop {
@@ -100,7 +103,6 @@ body.desktop {
}
// Overlay types, styling for desktop. Appended to .l-overlay-wrapper element.
.l-overlay-large,
.l-overlay-small,
.l-overlay-fit {
.c-overlay__outer {
@@ -118,8 +120,28 @@ body.desktop {
.l-overlay-large {
// Default
.c-overlay__outer {
@include overlaySizing($overlayOuterMarginLg);
$pad: $interiorMarginLg;
$tbPad: floor($pad * 0.8);
$lrPad: $pad;
.c-overlay {
&__blocker {
display: none;
}
&__outer {
@include overlaySizing($overlayOuterMarginFullscreen);
padding: $tbPad $lrPad;
}
&__close-button {
//top: $interiorMargin;
//right: $interiorMargin;
}
}
.l-browse-bar {
margin-right: 50px; // Don't cover close button
margin-bottom: $interiorMargin;
}
}
@@ -139,4 +161,4 @@ body.desktop {
min-width: 20%;
}
}
}
}

View File

@@ -334,8 +334,8 @@ define([
});
if (subscriber.callbacks.length === 0) {
subscriber.unsubscribe();
delete this.subscribeCache[keyString];
}
delete this.subscribeCache[keyString];
}.bind(this);
};

View File

@@ -156,6 +156,29 @@ define([
expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue');
});
it('only deletes subscription cache when there are no more subscribers', function () {
var unsubFunc = jasmine.createSpy('unsubscribe');
telemetryProvider.subscribe.and.returnValue(unsubFunc);
telemetryProvider.supportsSubscribe.and.returnValue(true);
telemetryAPI.addProvider(telemetryProvider);
var callback = jasmine.createSpy('callback');
var callbacktwo = jasmine.createSpy('callback two');
var callbackThree = jasmine.createSpy('callback three');
var unsubscribe = telemetryAPI.subscribe(domainObject, callback);
var unsubscribeTwo = telemetryAPI.subscribe(domainObject, callbacktwo);
expect(telemetryProvider.subscribe.calls.count()).toBe(1);
unsubscribe();
var unsubscribeThree = telemetryAPI.subscribe(domainObject, callbackThree);
// Regression test for where subscription cache was deleted on each unsubscribe, resulting in
// superfluous additional subscriptions. If the subscription cache is being deleted on each unsubscribe,
// then a subsequent subscribe will result in a new subscription at the provider.
expect(telemetryProvider.subscribe.calls.count()).toBe(1);
unsubscribeTwo();
unsubscribeThree();
});
it('does subscribe/unsubscribe', function () {
var unsubFunc = jasmine.createSpy('unsubscribe');
telemetryProvider.subscribe.and.returnValue(unsubFunc);

View File

@@ -0,0 +1,32 @@
/*****************************************************************************
* 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 function ladTableCompositionPolicy(openmct) {
return function (parent, child) {
if(parent.type === 'LadTable') {
return 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.getFormattedTimestamp(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,46 +100,91 @@ 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.getParsedTimestamp(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 = newTimestamp;
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) {
let newTimestampInBounds = this.inBounds(newTimestamp),
noExistingTimestamp = this.timestamp === undefined,
newTimestampIsLatest = newTimestamp > this.timestamp;
return newTimestampInBounds &&
(noExistingTimestamp || newTimestampIsLatest);
},
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.resetValues();
this.requestHistory();
}
},
inBounds(timestamp) {
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
},
updateTimeSystem(timeSystem) {
this.value = '---';
this.timestamp = '---';
this.valueClass = '';
this.resetValues();
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);
},
resetValues() {
this.value = '---';
this.timestamp = undefined;
this.valueClass = '';
},
getParsedTimestamp(timestamp) {
if(this.timeSystemFormat()) {
return this.formats[this.timestampKey].parse(timestamp);
}
},
getFormattedTimestamp(timestamp) {
if(this.timeSystemFormat()) {
return this.formats[this.timestampKey].format(timestamp);
}
},
timeSystemFormat() {
if(this.formats[this.timestampKey]) {
return true;
} else {
console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`);
return false;
}
}
}
}

View File

@@ -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 }}

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(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,237 @@
/*****************************************************************************
* 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 = ['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);
this.updateBounds = this.updateBounds.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);
});
this.openmct.time.on('bounds', this.updateBounds);
}
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);
});
this.openmct.time.on('bounds', this.updateBounds);
}
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);
}
}
}
updateBounds(bounds, isTick) {
if (!isTick) {
this.setUrlFromTimeApi();
}
}
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

@@ -0,0 +1,28 @@
/*****************************************************************************
* 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 URLTimeSettingsSynchronizer from "./URLTimeSettingsSynchronizer.js";
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

@@ -29,24 +29,28 @@ define([
ClearDataAction,
Vue
) {
return function plugin(appliesToObjects) {
return function plugin(appliesToObjects, options = {indicator: true}) {
let installIndicator = options.indicator;
appliesToObjects = appliesToObjects || [];
return function install(openmct) {
let component = new Vue ({
provide: {
openmct
},
components: {
GlobalClearIndicator: GlobaClearIndicator.default
},
template: '<GlobalClearIndicator></GlobalClearIndicator>'
}),
indicator = {
element: component.$mount().$el
};
if (installIndicator) {
let component = new Vue ({
provide: {
openmct
},
components: {
GlobalClearIndicator: GlobaClearIndicator.default
},
template: '<GlobalClearIndicator></GlobalClearIndicator>'
}),
indicator = {
element: component.$mount().$el
};
openmct.indicators.add(indicator);
openmct.indicators.add(indicator);
}
openmct.contextMenu.registerAction(new ClearDataAction.default(openmct, appliesToObjects));
};

View File

@@ -26,6 +26,7 @@ import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { evaluateResults } from './utils/evaluator';
import { getLatestTimestamp } from './utils/time';
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
import {TRIGGER_CONJUNCTION, TRIGGER_LABEL} from "./utils/constants";
/*
* conditionConfiguration = {
@@ -41,13 +42,14 @@ import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
* ]
* }
*/
export default class ConditionClass extends EventEmitter {
export default class Condition extends EventEmitter {
/**
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
* @constructor
* @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
* @param openmct
* @param conditionManager
*/
constructor(conditionConfiguration, openmct, conditionManager) {
super();
@@ -62,6 +64,7 @@ export default class ConditionClass extends EventEmitter {
this.createCriteria(conditionConfiguration.configuration.criteria);
}
this.trigger = conditionConfiguration.configuration.trigger;
this.description = '';
}
getResult(datum) {
@@ -109,7 +112,7 @@ export default class ConditionClass extends EventEmitter {
return {
id: criterionConfiguration.id || uuid(),
telemetry: criterionConfiguration.telemetry || '',
telemetryObject: this.conditionManager.telemetryObjects[this.openmct.objects.makeKeyString(criterionConfiguration.telemetry)],
telemetryObjects: this.conditionManager.telemetryObjects,
operation: criterionConfiguration.operation || '',
input: criterionConfiguration.input === undefined ? [] : criterionConfiguration.input,
metadata: criterionConfiguration.metadata || ''
@@ -120,6 +123,7 @@ export default class ConditionClass extends EventEmitter {
criterionConfigurations.forEach((criterionConfiguration) => {
this.addCriterion(criterionConfiguration);
});
this.updateDescription();
}
updateCriteria(criterionConfigurations) {
@@ -127,10 +131,11 @@ export default class ConditionClass extends EventEmitter {
this.createCriteria(criterionConfigurations);
}
updateTelemetry() {
updateTelemetryObjects() {
this.criteria.forEach((criterion) => {
criterion.updateTelemetry(this.conditionManager.telemetryObjects);
criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects);
});
this.updateDescription();
}
/**
@@ -145,6 +150,7 @@ export default class ConditionClass extends EventEmitter {
criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);
}
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
if (!this.criteria) {
this.criteria = [];
}
@@ -173,11 +179,14 @@ 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('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
let criterion = found.item;
criterion.unsubscribe();
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
this.criteria.splice(found.index, 1, newCriterion);
this.updateDescription();
}
}
@@ -188,8 +197,12 @@ export default class ConditionClass extends EventEmitter {
criterion.off('criterionUpdated', (obj) => {
this.handleCriterionUpdated(obj);
});
criterion.off('telemetryIsStale', (obj) => {
this.handleStaleCriterion(obj);
});
criterion.destroy();
this.criteria.splice(found.index, 1);
this.updateDescription();
return true;
}
@@ -200,9 +213,42 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(criterion.id);
if (found) {
this.criteria[found.index] = criterion.data;
this.updateDescription();
}
}
handleStaleCriterion(updatedCriterion) {
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
let latestTimestamp = {};
latestTimestamp = getLatestTimestamp(
latestTimestamp,
updatedCriterion.data,
this.timeSystems,
this.openmct.time.timeSystem()
);
this.conditionManager.updateCurrentCondition(latestTimestamp);
}
updateDescription() {
const triggerDescription = this.getTriggerDescription();
let description = '';
this.criteria.forEach((criterion, index) => {
if (!index) {
description = `Match if ${triggerDescription.prefix}`;
}
description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`;
});
this.description = description;
this.conditionManager.updateConditionDescription(this);
}
getTriggerDescription() {
return {
conjunction: TRIGGER_CONJUNCTION[this.trigger],
prefix: `${TRIGGER_LABEL[this.trigger]}: `
};
}
requestLADConditionResult() {
let latestTimestamp;
let criteriaResults = {};

View File

@@ -57,7 +57,7 @@ export default class ConditionManager extends EventEmitter {
endpoint,
this.telemetryReceived.bind(this, endpoint)
);
this.updateConditionTelemetry();
this.updateConditionTelemetryObjects();
}
unsubscribeFromTelemetry(endpointIdentifier) {
@@ -70,11 +70,11 @@ export default class ConditionManager extends EventEmitter {
this.subscriptions[id]();
delete this.subscriptions[id];
delete this.telemetryObjects[id];
this.removeConditionTelemetry();
this.removeConditionTelemetryObjects();
}
initialize() {
this.conditionClassCollection = [];
this.conditions = [];
if (this.conditionSetDomainObject.configuration.conditionCollection.length) {
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
this.initCondition(conditionConfiguration, index);
@@ -82,13 +82,14 @@ export default class ConditionManager extends EventEmitter {
}
}
updateConditionTelemetry() {
this.conditionClassCollection.forEach((condition) => condition.updateTelemetry());
updateConditionTelemetryObjects() {
this.conditions.forEach((condition) => condition.updateTelemetryObjects());
}
removeConditionTelemetry() {
removeConditionTelemetryObjects() {
let conditionsChanged = false;
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration) => {
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, conditionIndex) => {
let conditionChanged = false;
conditionConfiguration.configuration.criteria.forEach((criterion, index) => {
const isAnyAllTelemetry = criterion.telemetry && (criterion.telemetry === 'any' || criterion.telemetry === 'all');
if (!isAnyAllTelemetry) {
@@ -100,10 +101,16 @@ export default class ConditionManager extends EventEmitter {
criterion.metadata = '';
criterion.input = [];
criterion.operation = '';
conditionsChanged = true;
conditionChanged = true;
}
} else {
conditionChanged = true;
}
});
if (conditionChanged) {
this.updateCondition(conditionConfiguration, conditionIndex);
conditionsChanged = true;
}
});
if (conditionsChanged) {
this.persistConditions();
@@ -111,18 +118,24 @@ export default class ConditionManager extends EventEmitter {
}
updateCondition(conditionConfiguration, index) {
let condition = this.conditionClassCollection[index];
condition.update(conditionConfiguration);
let condition = this.conditions[index];
this.conditionSetDomainObject.configuration.conditionCollection[index] = conditionConfiguration;
condition.update(conditionConfiguration);
this.persistConditions();
}
updateConditionDescription(condition) {
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
found.summary = condition.description;
this.persistConditions();
}
initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this);
if (index !== undefined) {
this.conditionClassCollection.splice(index + 1, 0, condition);
this.conditions.splice(index + 1, 0, condition);
} else {
this.conditionClassCollection.unshift(condition);
this.conditions.unshift(condition);
}
}
@@ -181,15 +194,15 @@ export default class ConditionManager extends EventEmitter {
}
removeCondition(index) {
let condition = this.conditionClassCollection[index];
let condition = this.conditions[index];
condition.destroy();
this.conditionClassCollection.splice(index, 1);
this.conditions.splice(index, 1);
this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1);
this.persistConditions();
}
findConditionById(id) {
return this.conditionClassCollection.find(conditionClass => conditionClass.id === id);
return this.conditions.find(condition => condition.id === id);
}
reorderConditions(reorderPlan) {
@@ -234,14 +247,14 @@ export default class ConditionManager extends EventEmitter {
}
requestLADConditionSetOutput() {
if (!this.conditionClassCollection.length) {
if (!this.conditions.length) {
return Promise.resolve([]);
}
return this.compositionLoad.then(() => {
let latestTimestamp;
let conditionResults = {};
const conditionRequests = this.conditionClassCollection
const conditionRequests = this.conditions
.map(condition => condition.requestLADConditionResult());
return Promise.all(conditionRequests)
@@ -281,7 +294,7 @@ export default class ConditionManager extends EventEmitter {
isTelemetryUsed(endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
for(const condition of this.conditionClassCollection) {
for(const condition of this.conditions) {
if (condition.isTelemetryUsed(id)) {
return true;
}
@@ -300,10 +313,14 @@ export default class ConditionManager extends EventEmitter {
let timestamp = {};
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
this.conditionClassCollection.forEach(condition => {
this.conditions.forEach(condition => {
condition.getResult(normalizedDatum);
});
this.updateCurrentCondition(timestamp);
}
updateCurrentCondition(timestamp) {
const currentCondition = this.getCurrentCondition();
this.emit('conditionSetResultUpdated',
@@ -364,7 +381,7 @@ export default class ConditionManager extends EventEmitter {
this.stopObservingForChanges();
}
this.conditionClassCollection.forEach((condition) => {
this.conditions.forEach((condition) => {
condition.destroy();
})
}

View File

@@ -126,7 +126,7 @@ describe('ConditionManager', () => {
it('creates a conditionCollection with a default condition', function () {
expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(1);
let defaultConditionId = conditionMgr.conditionClassCollection[0].id;
let defaultConditionId = conditionMgr.conditions[0].id;
expect(defaultConditionId).toEqual(mockCondition.id);
});

View File

@@ -36,19 +36,20 @@ describe("The condition", function () {
beforeEach (() => {
conditionManager = jasmine.createSpyObj('conditionManager',
['on']
['on', 'updateConditionDescription']
);
mockTelemetryReceived = jasmine.createSpy('listener');
conditionManager.on('telemetryReceived', mockTelemetryReceived);
conditionManager.updateConditionDescription.and.returnValue(function () {});
testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "value",
name: "Value",
valueMetadatas: [{
key: "some-key",
name: "Some attribute",
hints: {
range: 2
}
@@ -78,7 +79,7 @@ describe("The condition", function () {
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', 'subscribe', 'getMetadata']);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values);
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
mockTimeSystems = {
key: 'utc'

View File

@@ -46,6 +46,7 @@ export default class StyleRuleManager extends EventEmitter {
if (this.isEditing) {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
if (this.conditionSetIdentifier) {
this.applySelectedConditionStyle();
@@ -66,6 +67,7 @@ export default class StyleRuleManager extends EventEmitter {
subscribeToConditionSet() {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {
this.openmct.telemetry.request(conditionSetDomainObject)
@@ -154,8 +156,8 @@ export default class StyleRuleManager extends EventEmitter {
this.applyStaticStyle();
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
delete this.stopProvidingTelemetry;
this.conditionSetIdentifier = undefined;
}

View File

@@ -50,7 +50,7 @@
<span class="c-condition__name">{{ condition.configuration.name }}</span>
<span class="c-condition__summary">
<template v-if="!canEvaluateCriteria">
<template v-if="!condition.isDefault && !canEvaluateCriteria">
Define criteria
</template>
<span v-else>
@@ -250,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;

View File

@@ -152,7 +152,8 @@ export default {
},
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();
});
},

View File

@@ -27,20 +27,20 @@
>
{{ condition.configuration.name }}
</span>
<span v-for="(criterionDescription, index) in criterionDescriptions"
:key="criterionDescription"
<span v-if="!condition.isDefault"
class="c-style__condition-desc__text"
>
<template v-if="!index">When</template>
{{ criterionDescription }}
<template v-if="index < (criterionDescriptions.length-1)">{{ triggerDescription }}</template>
{{ description }}
</span>
<span v-else
class="c-style__condition-desc__text"
>
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

@@ -55,6 +55,7 @@
>
{{ option.name }}
</option>
<option value="dataReceived">any data received</option>
</select>
</span>
<span v-if="criterion.telemetry && criterion.metadata"
@@ -79,10 +80,11 @@
<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>
<span v-if="criterion.metadata === 'dataReceived'">seconds</span>
</template>
<span v-else>
<span v-if="inputCount && criterion.operation"
@@ -108,6 +110,7 @@
<script>
import { OPERATIONS } from '../utils/operations';
import { INPUT_TYPES } from '../utils/operations';
import {TRIGGER_CONJUNCTION} from "../utils/constants";
export default {
inject: ['openmct'],
@@ -143,11 +146,15 @@ 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);
if (this.criterion.metadata === 'dataReceived') {
return this.operations.filter(op => op.name === 'isStale');
} else {
return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1);
}
},
setInputType: function () {
let type = '';
@@ -178,17 +185,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();
}
}
},
@@ -212,6 +220,8 @@ export default {
} else {
this.operationFormat = 'number';
}
} else if (this.criterion.metadata === 'dataReceived') {
this.operationFormat = 'number';
}
this.updateInputVisibilityAndValues();
},
@@ -221,19 +231,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

@@ -197,6 +197,7 @@ export default {
}
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
},
initialize(conditionSetDomainObject) {
@@ -210,6 +211,7 @@ export default {
if (this.isEditing) {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
} else {
this.subscribeToConditionSet();
@@ -307,6 +309,7 @@ export default {
this.persist(domainObjectStyles);
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
},
updateDomainObjectItemStyles(newItems) {
@@ -375,6 +378,7 @@ export default {
subscribeToConditionSet() {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
if (this.conditionSetDomainObject) {
this.openmct.telemetry.request(this.conditionSetDomainObject)

View File

@@ -37,12 +37,13 @@
>
<style-editor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
:is-editing="allowEditing"
:mixed-styles="mixedStyles"
@persist="updateStaticStyle"
/>
</div>
<button
v-if="allowEditing"
id="addConditionSet"
class="c-button c-button--major c-toggle-styling-button labeled"
@click="addConditionSet"
@@ -63,7 +64,7 @@
>
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
</a>
<template v-if="isEditing">
<template v-if="allowEditing">
<button
id="changeConditionSet"
class="c-button labeled"
@@ -96,7 +97,7 @@
/>
<style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:is-editing="isEditing"
:is-editing="allowEditing"
@persist="updateConditionalStyle"
/>
</div>
@@ -137,7 +138,13 @@ export default {
conditions: undefined,
conditionsLoaded: false,
navigateToPath: '',
selectedConditionId: ''
selectedConditionId: '',
locked: false
}
},
computed: {
allowEditing() {
return this.isEditing && !this.locked;
}
},
destroyed() {
@@ -183,6 +190,7 @@ export default {
if (this.isEditing) {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
} else {
this.subscribeToConditionSet();
@@ -224,7 +232,13 @@ export default {
this.selection.forEach((selectionItem) => {
const item = selectionItem[0].context.item;
const layoutItem = selectionItem[0].context.layoutItem;
const layoutDomainObject = selectionItem[0].context.item;
const isChildItem = selectionItem.length > 1;
if (layoutDomainObject && layoutDomainObject.locked) {
this.locked = true;
}
if (!isChildItem) {
domainObject = item;
itemStyle = getApplicableStylesForItem(item);
@@ -312,6 +326,7 @@ export default {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
if (this.unObserveObjects) {
@@ -324,6 +339,7 @@ export default {
subscribeToConditionSet() {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
if (this.conditionSetDomainObject) {
this.openmct.telemetry.request(this.conditionSetDomainObject)
@@ -457,7 +473,7 @@ export default {
domainObjects.forEach(domainObject => {
let objectStyles = (domainObject.configuration && domainObject.configuration.objectStyles) || {};
this.removeConditionalStyles(objectStyles);
if (Object.keys(objectStyles).length <= 0) {
if (objectStyles && Object.keys(objectStyles).length <= 0) {
objectStyles = undefined;
}
this.persist(domainObject, objectStyles);
@@ -467,20 +483,21 @@ export default {
this.items.forEach((item) => {
const itemId = item.id;
this.removeConditionalStyles(domainObjectStyles, itemId);
if (Object.keys(domainObjectStyles[itemId]).length <= 0) {
if (domainObjectStyles[itemId] && Object.keys(domainObjectStyles[itemId]).length <= 0) {
delete domainObjectStyles[itemId];
}
});
} else {
this.removeConditionalStyles(domainObjectStyles);
}
if (Object.keys(domainObjectStyles).length <= 0) {
if (domainObjectStyles && Object.keys(domainObjectStyles).length <= 0) {
domainObjectStyles = undefined;
}
this.persist(this.domainObject, domainObjectStyles);
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
},
removeConditionalStyles(domainObjectStyles, itemId) {
@@ -553,9 +570,9 @@ export default {
let itemConditionalStyle = { styles: []};
if (!this.conditionSetDomainObject) {
if (domainObjectStyles[item.id] && domainObjectStyles[item.id].staticStyle) {
itemStaticStyle = domainObjectStyles[item.id].staticStyle.style;
itemStaticStyle = Object.assign({}, domainObjectStyles[item.id].staticStyle.style);
}
if (item.applicableStyles[property]) {
if (item.applicableStyles[property] !== undefined) {
itemStaticStyle[property] = this.staticStyle.style[property];
}
if (Object.keys(itemStaticStyle).length <= 0) {

View File

@@ -22,7 +22,8 @@
import TelemetryCriterion from './TelemetryCriterion';
import { evaluateResults } from "../utils/evaluator";
import { getLatestTimestamp } from '../utils/time';
import {getLatestTimestamp, subscribeForStaleness} from '../utils/time';
import { getOperatorText } from "@/plugins/condition/utils/operations";
export default class AllTelemetryCriterion extends TelemetryCriterion {
@@ -40,15 +41,44 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
initialize() {
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
this.telemetryDataCache = {};
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(this.telemetryObjects || {});
}
}
subscribeForStaleData(telemetryObjects) {
if (!this.stalenessSubscription) {
this.stalenessSubscription = {};
}
Object.values(telemetryObjects).forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (!this.stalenessSubscription[id]) {
this.stalenessSubscription[id] = subscribeForStaleness((data) => {
this.handleStaleTelemetry(id, data);
}, this.input[0]*1000);
}
})
}
handleStaleTelemetry(id, data) {
if (this.telemetryDataCache) {
this.telemetryDataCache[id] = true;
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
this.emitEvent('telemetryIsStale', data);
}
isValid() {
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
}
updateTelemetry(telemetryObjects) {
updateTelemetryObjects(telemetryObjects) {
this.telemetryObjects = { ...telemetryObjects };
this.removeTelemetryDataCache();
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(this.telemetryObjects || {});
}
}
removeTelemetryDataCache() {
@@ -62,6 +92,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
});
telemetryCacheIds.forEach(id => {
delete (this.telemetryDataCache[id]);
delete (this.stalenessSubscription[id]);
});
}
@@ -95,7 +126,14 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
const validatedData = this.isValid() ? data : {};
if (validatedData) {
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
if (this.isStalenessCheck()) {
if (this.stalenessSubscription && this.stalenessSubscription[validatedData.id]) {
this.stalenessSubscription[validatedData.id].update(validatedData);
}
this.telemetryDataCache[validatedData.id] = false;
} else {
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
}
}
Object.values(telemetryObjects).forEach(telemetryObject => {
@@ -159,8 +197,31 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
});
}
getDescription() {
const telemetryDescription = this.telemetry === 'all' ? 'all telemetry' : 'any telemetry';
let metadataValue = (this.metadata === 'dataReceived' ? '' : this.metadata);
let inputValue = this.input;
if (this.metadata) {
const telemetryObjects = Object.values(this.telemetryObjects);
for (let i=0; i < telemetryObjects.length; i++) {
const telemetryObject = telemetryObjects[i];
const metadataObject = this.getMetaDataObject(telemetryObject, this.metadata);
if (metadataObject) {
metadataValue = this.getMetadataValueFromMetaData(metadataObject) || this.metadata;
inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input;
break;
}
}
}
return `${telemetryDescription} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`;
}
destroy() {
delete this.telemetryObjects;
delete this.telemetryDataCache;
if (this.stalenessSubscription) {
Object.values(this.stalenessSubscription).forEach((subscription) => subscription.clear);
delete this.stalenessSubscription;
}
}
}

View File

@@ -21,7 +21,8 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { OPERATIONS } from '../utils/operations';
import { OPERATIONS, getOperatorText } from '../utils/operations';
import { subscribeForStaleness } from "../utils/time";
export default class TelemetryCriterion extends EventEmitter {
@@ -43,22 +44,49 @@ export default class TelemetryCriterion extends EventEmitter {
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.result = undefined;
this.stalenessSubscription = undefined;
this.initialize();
this.emitEvent('criterionUpdated', this);
}
initialize() {
this.telemetryObject = this.telemetryDomainObjectDefinition.telemetryObject;
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData()
}
}
subscribeForStaleData() {
if (this.stalenessSubscription) {
this.stalenessSubscription.clear();
}
this.stalenessSubscription = subscribeForStaleness(this.handleStaleTelemetry.bind(this), this.input[0]*1000);
}
handleStaleTelemetry(data) {
this.result = true;
this.emitEvent('telemetryIsStale', data);
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
updateTelemetry(telemetryObjects) {
isStalenessCheck() {
return this.metadata && this.metadata === 'dataReceived';
}
isValidInput() {
return this.input instanceof Array && this.input.length;
}
updateTelemetryObjects(telemetryObjects) {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData()
}
}
createNormalizedDatum(telemetryDatum, endpoint) {
@@ -91,7 +119,14 @@ export default class TelemetryCriterion extends EventEmitter {
getResult(data) {
const validatedData = this.isValid() ? data : {};
this.result = this.computeResult(validatedData);
if (this.isStalenessCheck()) {
if (this.stalenessSubscription) {
this.stalenessSubscription.update(validatedData);
}
this.result = false;
} else {
this.result = this.computeResult(validatedData);
}
}
requestLAD() {
@@ -107,12 +142,14 @@ export default class TelemetryCriterion extends EventEmitter {
};
}
let telemetryObject = this.telemetryObject;
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);
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObject);
return {
id: this.id,
@@ -136,7 +173,7 @@ export default class TelemetryCriterion extends EventEmitter {
let comparator = this.findOperation(this.operation);
let params = [];
params.push(data[this.metadata]);
if (this.input instanceof Array && this.input.length) {
if (this.isValidInput()) {
this.input.forEach(input => params.push(input));
}
if (typeof comparator === 'function') {
@@ -153,9 +190,57 @@ export default class TelemetryCriterion extends EventEmitter {
});
}
getMetaDataObject(telemetryObject, metadata) {
let metadataObject;
if (metadata) {
const telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
metadataObject = telemetryMetadata.valueMetadatas.find((valueMetadata) => valueMetadata.key === metadata);
}
return metadataObject;
}
getInputValueFromMetaData(metadataObject, input) {
let inputValue;
if (metadataObject) {
if(metadataObject.enumerations && input.length) {
const enumeration = metadataObject.enumerations[input[0]];
if (enumeration !== undefined && enumeration.string) {
inputValue = [enumeration.string];
}
}
}
return inputValue;
}
getMetadataValueFromMetaData(metadataObject) {
let metadataValue;
if (metadataObject) {
if (metadataObject.name) {
metadataValue = metadataObject.name;
}
}
return metadataValue;
}
getDescription(criterion, index) {
let description;
if (!this.telemetry || !this.telemetryObject || (this.telemetryObject.type === 'unknown')) {
description = `Unknown ${this.metadata} ${getOperatorText(this.operation, this.input)}`;
} else {
const metadataObject = this.getMetaDataObject(this.telemetryObject, this.metadata);
const metadataValue = this.getMetadataValueFromMetaData(metadataObject) || (this.metadata === 'dataReceived' ? '' : this.metadata);
const inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input;
description = `${this.telemetryObject.name} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`;
}
return description;
}
destroy() {
delete this.telemetryObject;
delete this.telemetryObjectIdAsString;
if (this.stalenessSubscription) {
delete this.stalenessSubscription;
}
}
}

View File

@@ -21,12 +21,14 @@
*****************************************************************************/
import TelemetryCriterion from "./TelemetryCriterion";
import { getMockTelemetry } from "utils/testing";
let openmct = {},
mockListener,
testCriterionDefinition,
testTelemetryObject,
telemetryCriterion;
telemetryCriterion,
mockTelemetry = getMockTelemetry();
describe("The telemetry criterion", function () {
@@ -60,7 +62,7 @@ describe("The telemetry criterion", function () {
};
openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']);
openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key);
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter"]);
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter", "request"]);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getValueFormatter.and.returnValue({
@@ -83,7 +85,7 @@ describe("The telemetry criterion", function () {
operation: 'textContains',
metadata: 'value',
input: ['Hell'],
telemetryObject: testTelemetryObject
telemetryObjects: {[testTelemetryObject.identifier.key]: testTelemetryObject}
};
mockListener = jasmine.createSpy('listener');
@@ -110,12 +112,29 @@ describe("The telemetry criterion", function () {
expect(telemetryCriterion.result).toBeTrue();
});
// it("does not return a result on new data from irrelavant telemetry providers", function () {
// telemetryCriterion.getResult({
// value: 'Hello',
// utc: 'Hi',
// id: '1234'
// });
// expect(telemetryCriterion.result).toBeFalse();
// });
describe('the LAD request', () => {
beforeEach(async () => {
let telemetryRequestResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => {
setTimeout(() => {
telemetryRequestResolve(mockTelemetry);
}, 100);
return telemetryRequestPromise;
});
});
it("returns results for slow LAD requests", async function () {
const criteriaRequest = telemetryCriterion.requestLAD();
telemetryCriterion.destroy();
expect(telemetryCriterion.telemetryObject).toBeUndefined();
setTimeout(() => {
criteriaRequest.then((result) => {
expect(result).toBeDefined();
});
}, 300);
});
});
});

View File

@@ -20,17 +20,55 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct } from "testUtils";
import { createOpenMct, resetApplicationState } from "utils/testing";
import ConditionPlugin from "./plugin";
import StylesView from "./components/inspector/StylesView.vue";
import Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils";
import ConditionManager from "@/plugins/condition/ConditionManager";
describe('the plugin', function () {
let conditionSetDefinition;
let mockConditionSetDomainObject;
let mockListener;
let element;
let child;
let openmct;
let testTelemetryObject;
beforeAll(() => {
resetApplicationState(openmct);
});
beforeEach((done) => {
testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"},
type: "test-object",
name: "Test Object",
telemetry: {
valueMetadatas: [{
key: "some-key",
name: "Some attribute",
hints: {
range: 2
}
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
}, {
key: "testSource",
source: "value",
name: "Test",
format: "string"
}]
}
};
beforeAll((done) => {
openmct = createOpenMct();
openmct.install(new ConditionPlugin());
@@ -48,12 +86,18 @@ describe('the plugin', function () {
type: 'conditionSet'
};
mockListener = jasmine.createSpy('mockListener');
conditionSetDefinition.initialize(mockConditionSetDomainObject);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
resetApplicationState(openmct);
});
let mockConditionSetObject = {
name: 'Condition Set',
key: 'conditionSet',
@@ -90,4 +134,368 @@ 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]);
});
});
});
});
});
describe('the condition check for staleness', () => {
let conditionSetDomainObject;
beforeEach(()=>{
conditionSetDomainObject = {
"configuration":{
"conditionTestData":[
{
"telemetry":"",
"metadata":"",
"input":""
}
],
"conditionCollection":[
{
"id":"39584410-cbf9-499e-96dc-76f27e69885d",
"configuration":{
"name":"Unnamed Condition",
"output":"Any stale telemetry",
"trigger":"all",
"criteria":[
{
"id":"35400132-63b0-425c-ac30-8197df7d5862",
"telemetry":"any",
"operation":"isStale",
"input":[
"1"
],
"metadata":"dataReceived"
}
]
},
"summary":"Match if all criteria are met: Any telemetry is stale after 5 seconds"
},
{
"isDefault":true,
"id":"2532d90a-e0d6-4935-b546-3123522da2de",
"configuration":{
"name":"Default",
"output":"Default",
"trigger":"all",
"criteria":[
]
},
"summary":""
}
]
},
"composition":[
{
"namespace":"",
"key":"test-object"
}
],
"telemetry":{
},
"name":"Condition Set",
"type":"conditionSet",
"identifier":{
"namespace":"",
"key":"cf4456a9-296a-4e6b-b182-62ed29cd15b9"
}
};
});
it('should evaluate as stale when telemetry is not received in the allotted time', (done) => {
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
"test-object": testTelemetryObject
};
conditionMgr.updateConditionTelemetryObjects();
setTimeout(() => {
expect(mockListener).toHaveBeenCalledWith({
output: 'Any stale telemetry',
id: { namespace: '', key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' },
conditionId: '39584410-cbf9-499e-96dc-76f27e69885d',
utc: undefined
});
done();
}, 1500);
});
it('should not evaluate as stale when telemetry is received in the allotted time', (done) => {
const date = Date.now();
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = ["2"];
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
"test-object": testTelemetryObject
};
conditionMgr.updateConditionTelemetryObjects();
conditionMgr.telemetryReceived(testTelemetryObject, {
utc: date
});
setTimeout(() => {
expect(mockListener).toHaveBeenCalledWith({
output: 'Default',
id: { namespace: '', key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' },
conditionId: '2532d90a-e0d6-4935-b546-3123522da2de',
utc: undefined
});
done();
}, 1500);
});
});
});

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

@@ -35,7 +35,7 @@ export const evaluateResults = (results, trigger) => {
function matchAll(results) {
for (const result of results) {
if (!result) {
if (result !== true) {
return false;
}
}
@@ -45,7 +45,7 @@ function matchAll(results) {
function matchAny(results) {
for (const result of results) {
if (result) {
if (result === true) {
return true;
}
}
@@ -56,7 +56,7 @@ function matchAny(results) {
function matchExact(results, target) {
let matches = 0;
for (const result of results) {
if (result) {
if (result === true) {
matches++;
}
if (matches > target) {

View File

@@ -250,12 +250,12 @@ export const OPERATIONS = [
}
},
{
name: 'valueIs',
name: 'isOneOf',
operation: function (input) {
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
if (input[1]) {
const values = input[1].split(',');
return values.find((value) => lhsValue === value.toString().trim());
return values.some((value) => lhsValue === value.toString().trim());
}
return false;
},
@@ -267,12 +267,12 @@ export const OPERATIONS = [
}
},
{
name: 'valueIsNot',
name: 'isNotOneOf',
operation: function (input) {
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
if (input[1]) {
const values = input[1].split(',');
const found = values.find((value) => lhsValue === value.toString().trim());
const found = values.some((value) => lhsValue === value.toString().trim());
return !found;
}
return false;
@@ -283,6 +283,18 @@ export const OPERATIONS = [
getDescription: function (values) {
return ' is not one of ' + values[0];
}
},
{
name: 'isStale',
operation: function () {
return false;
},
text: 'is older than',
appliesTo: ["number"],
inputCount: 1,
getDescription: function (values) {
return ` is older than ${values[0] || ''} seconds`;
}
}
];
@@ -290,3 +302,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,8 +21,8 @@
*****************************************************************************/
import { OPERATIONS } from "./operations";
let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIs');
let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIsNot');
let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'isOneOf');
let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'isNotOneOf');
let isBetween = OPERATIONS.find((operation) => operation.name === 'between');
let isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween');
let enumIsOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIs');

View File

@@ -50,3 +50,26 @@ function updateLatestTimeStamp(timestamp, timeSystems) {
return latest;
}
export const subscribeForStaleness = (callback, timeout) => {
let stalenessTimer = setTimeout(() => {
clearTimeout(stalenessTimer);
callback();
}, timeout);
return {
update: (data) => {
if (stalenessTimer) {
clearTimeout(stalenessTimer);
}
stalenessTimer = setTimeout(() => {
clearTimeout(stalenessTimer);
callback(data);
}, timeout);
},
clear: () => {
if (stalenessTimer) {
clearTimeout(stalenessTimer);
}
}
}
};

View File

@@ -0,0 +1,64 @@
/*****************************************************************************
* 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 { subscribeForStaleness } from "./time";
describe('time related utils', () => {
let subscription;
let mockListener;
beforeEach(() => {
mockListener = jasmine.createSpy('listener');
subscription = subscribeForStaleness(mockListener, 100);
});
describe('subscribe for staleness', () => {
it('should call listeners when stale', (done) => {
setTimeout(() => {
expect(mockListener).toHaveBeenCalled();
done();
}, 200);
});
it('should update the subscription', (done) => {
function updated() {
setTimeout(() => {
expect(mockListener).not.toHaveBeenCalled();
done();
}, 50);
}
setTimeout(() => {
subscription.update();
updated();
}, 50);
});
it('should clear the subscription', (done) => {
subscription.clear();
setTimeout(() => {
expect(mockListener).not.toHaveBeenCalled();
done();
}, 200);
});
});
});

View File

@@ -0,0 +1,29 @@
/*****************************************************************************
* 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 RootObjectProvider from '../../api/objects/RootObjectProvider.js';
export default function (name) {
return function (openmct) {
let rootObjectProvider = new RootObjectProvider();
rootObjectProvider.updateName(name);
};
}

View File

@@ -0,0 +1,99 @@
/*****************************************************************************
* 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';
xdescribe("the plugin", () => {
let openmct,
compositionAPI,
newFolderAction,
mockObjectPath,
mockDialogService,
mockComposition,
mockPromise,
newFolderName = 'New Folder';
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
newFolderAction = openmct.contextMenu._allActions.filter(action => {
return action.key === 'newFolder';
})[0];
});
afterEach(() => {
resetApplicationState(openmct);
});
it('installs the new folder action', () => {
expect(newFolderAction).toBeDefined();
});
describe('when invoked', () => {
beforeEach((done) => {
compositionAPI = openmct.composition;
mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
mockPromise = {
then: (callback) => {
callback({name: newFolderName});
done();
}
};
mockDialogService = jasmine.createSpyObj('dialogService', ['getUserInput']);
mockComposition = jasmine.createSpyObj('composition', ['add']);
mockDialogService.getUserInput.and.returnValue(mockPromise);
spyOn(openmct.$injector, 'get').and.returnValue(mockDialogService);
spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'mutate');
newFolderAction.invoke(mockObjectPath);
});
it('gets user input for folder name', () => {
expect(mockDialogService.getUserInput).toHaveBeenCalled();
});
it('creates a new folder object', () => {
expect(openmct.objects.mutate).toHaveBeenCalled();
});
it('adds new folder object to parent composition', () => {
expect(mockComposition.add).toHaveBeenCalled();
});
});
});

View File

@@ -29,11 +29,15 @@ define([
function isTelemetryObject(selectionPath) {
let selectedObject = selectionPath[0].context.item;
let parentObject = selectionPath[1].context.item;
let selectedLayoutItem = selectionPath[0].context.layoutItem;
return parentObject &&
parentObject.type === 'layout' &&
selectedObject &&
selectedLayoutItem &&
selectedLayoutItem.type === 'telemetry-view' &&
openmct.telemetry.isTelemetryObject(selectedObject) &&
!options.showAsView.includes(selectedObject.type)
!options.showAsView.includes(selectedObject.type);
}
return {

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.
*
@@ -41,38 +41,90 @@ define(['lodash'], function (_) {
},
toolbar: function (selectedObjects) {
const DIALOG_FORM = {
'text': {
name: "Text Element Properties",
sections: [
{
rows: [
{
key: "text",
control: "textfield",
name: "Text",
required: true
}
]
}
]
'text': {
name: "Text Element Properties",
sections: [
{
rows: [
{
key: "text",
control: "textfield",
name: "Text",
required: true
}
]
}
]
},
'image': {
name: "Image Properties",
sections: [
{
rows: [
{
key: "url",
control: "textfield",
name: "Image URL",
"cssClass": "l-input-lg",
required: true
}
]
}
]
}
},
'image': {
name: "Image Properties",
sections: [
{
rows: [
{
key: "url",
control: "textfield",
name: "Image URL",
"cssClass": "l-input-lg",
required: true
}
]
}
VIEW_TYPES = {
'telemetry-view': {
value: 'telemetry-view',
name: 'Alphanumeric',
class: 'icon-alphanumeric'
},
'telemetry.plot.overlay': {
value: 'telemetry.plot.overlay',
name: 'Overlay Plot',
class: "icon-plot-overlay"
},
'telemetry.plot.stacked': {
value: "telemetry.plot.stacked",
name: "Stacked Plot",
class: "icon-plot-stacked"
},
'table': {
value: 'table',
name: 'Table',
class: 'icon-tabular-realtime'
}
},
APPLICABLE_VIEWS = {
'telemetry-view': [
VIEW_TYPES['telemetry.plot.overlay'],
VIEW_TYPES['telemetry.plot.stacked'],
VIEW_TYPES.table
],
'telemetry.plot.overlay': [
VIEW_TYPES['telemetry.plot.stacked'],
VIEW_TYPES.table,
VIEW_TYPES['telemetry-view']
],
'telemetry.plot.stacked': [
VIEW_TYPES['telemetry.plot.overlay'],
VIEW_TYPES.table,
VIEW_TYPES['telemetry-view']
],
'table': [
VIEW_TYPES['telemetry.plot.overlay'],
VIEW_TYPES['telemetry.plot.stacked'],
VIEW_TYPES['telemetry-view']
],
'telemetry-view-multi': [
VIEW_TYPES['telemetry.plot.overlay'],
VIEW_TYPES['telemetry.plot.stacked'],
VIEW_TYPES.table
],
'telemetry.plot.overlay-multi': [
VIEW_TYPES['telemetry.plot.stacked']
]
}
};
};
function getUserInput(form) {
return openmct.$injector.get('dialogService').getUserInput(form, {});
@@ -415,6 +467,100 @@ define(['lodash'], function (_) {
}
}
function getDuplicateButton(selectedParent, selectionPath, selection) {
return {
control: "button",
domainObject: selectedParent,
icon: "icon-duplicate",
title: "Duplicate the selected object",
method: function () {
let duplicateItem = selectionPath[1].context.duplicateItem;
duplicateItem(selection);
}
};
}
function getPropertyFromPath(object, path) {
let splitPath = path.split('.'),
property = Object.assign({}, object);
while (splitPath.length && property) {
property = property[splitPath.shift()];
}
return property;
}
function areAllViews(type, path, selection) {
let allTelemetry = true;
selection.forEach(selectedItem => {
let selectedItemContext = selectedItem[0].context;
if (getPropertyFromPath(selectedItemContext, path) !== type) {
allTelemetry = false;
}
});
return allTelemetry;
}
function getViewSwitcherMenu(selectedParent, selectionPath, selection) {
if (selection.length === 1) {
let displayLayoutContext = selectionPath[1].context,
selectedItemContext = selectionPath[0].context,
selectedItemType = selectedItemContext.item.type;
if (selectedItemContext.layoutItem.type === 'telemetry-view') {
selectedItemType = 'telemetry-view';
}
let viewOptions = APPLICABLE_VIEWS[selectedItemType];
if (viewOptions) {
return {
control: "menu",
domainObject: selectedParent,
icon: "icon-object",
title: "Switch the way this telemetry is displayed",
options: viewOptions,
method: function (option) {
displayLayoutContext.switchViewType(selectedItemContext, option.value, selection);
}
};
}
} else if (selection.length > 1) {
if (areAllViews('telemetry-view', 'layoutItem.type', selection)) {
let displayLayoutContext = selectionPath[1].context;
return {
control: "menu",
domainObject: selectedParent,
icon: "icon-object",
title: "Merge into a telemetry table or plot",
options: APPLICABLE_VIEWS['telemetry-view-multi'],
method: function (option) {
displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value);
}
};
} else if (areAllViews('telemetry.plot.overlay', 'item.type', selection)) {
let displayLayoutContext = selectionPath[1].context;
return {
control: "menu",
domainObject: selectedParent,
icon: "icon-object",
title: "Merge into a stacked plot",
options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'],
method: function (option) {
displayLayoutContext.mergeMultipleOverlayPlots(selection, option.value);
}
};
}
}
}
function getSeparator() {
return {
control: "separator"
@@ -435,12 +581,14 @@ define(['lodash'], function (_) {
'add-menu': [],
'text': [],
'url': [],
'viewSwitcher': [],
'toggle-frame': [],
'display-mode': [],
'telemetry-value': [],
'style': [],
'text-style': [],
'position': [],
'duplicate': [],
'remove': []
};
@@ -448,7 +596,7 @@ define(['lodash'], function (_) {
let selectedParent = selectionPath[1].context.item;
let layoutItem = selectionPath[0].context.layoutItem;
if (!layoutItem) {
if (!layoutItem || selectedParent.locked) {
return;
}
@@ -471,6 +619,9 @@ define(['lodash'], function (_) {
if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
}
if (toolbar.viewSwitcher.length === 0) {
toolbar.viewSwitcher = [getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects)];
}
} else if (layoutItem.type === 'telemetry-view') {
if (toolbar['display-mode'].length === 0) {
toolbar['display-mode'] = [getDisplayModeMenu(selectedParent, selectedObjects)];
@@ -495,6 +646,9 @@ define(['lodash'], function (_) {
if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
}
if (toolbar.viewSwitcher.length === 0) {
toolbar.viewSwitcher = [getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects)];
}
} else if (layoutItem.type === 'text-view') {
if (toolbar['text-style'].length === 0) {
toolbar['text-style'] = [
@@ -556,6 +710,9 @@ define(['lodash'], function (_) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
}
}
if(toolbar.duplicate.length === 0) {
toolbar.duplicate = [getDuplicateButton(selectedParent, selectionPath, selectedObjects)];
}
});
let toolbarArray = Object.values(toolbar);

View File

@@ -24,6 +24,7 @@
<layout-frame
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
@@ -70,7 +71,11 @@ export default {
type: Number,
required: true
},
initSelect: Boolean
initSelect: Boolean,
isEditing: {
type: Boolean,
required: true
}
},
computed: {
style() {
@@ -91,6 +96,13 @@ export default {
}
this.context.index = newIndex;
},
item(newItem) {
if (!this.context) {
return;
}
this.context.layoutItem = newItem;
}
},
mounted() {

View File

@@ -24,14 +24,18 @@
<div
class="l-layout"
:class="{
'is-multi-selected': selectedLayoutItems.length > 1
'is-multi-selected': selectedLayoutItems.length > 1,
'allow-editing': isEditing
}"
@dragover="handleDragOver"
@click.capture="bypassSelection"
@drop="handleDrop"
>
<!-- Background grid -->
<div class="l-layout__grid-holder c-grid">
<div
v-if="isEditing"
class="l-layout__grid-holder c-grid"
>
<div
v-if="gridSize[0] >= 3"
class="c-grid__x l-grid l-grid-x"
@@ -47,11 +51,13 @@
:is="item.type"
v-for="(item, index) in layoutItems"
:key="item.id"
:ref="`layout-item-${item.id}`"
:item="item"
:grid-size="gridSize"
:init-select="initSelectIndex === index"
:index="index"
:multi-select="selectedLayoutItems.length > 1"
:is-editing="isEditing"
@move="move"
@endMove="endMove"
@endLineResize="endLineResize"
@@ -77,6 +83,30 @@ import ImageView from './ImageView.vue'
import EditMarquee from './EditMarquee.vue'
import _ from 'lodash'
const TELEMETRY_IDENTIFIER_FUNCTIONS = {
'table': (domainObject) => {
return Promise.resolve(domainObject.composition);
},
'telemetry.plot.overlay': (domainObject) => {
return Promise.resolve(domainObject.composition);
},
'telemetry.plot.stacked': (domainObject, openmct) => {
let composition = openmct.composition.get(domainObject);
return composition.load().then((objects) => {
let identifiers = [];
objects.forEach(object => {
if (object.type === 'telemetry.plot.overlay') {
identifiers.push(...object.composition);
} else {
identifiers.push(object.identifier);
}
});
return Promise.resolve(identifiers);
});
}
}
const ITEM_TYPE_VIEW_MAP = {
'subobject-view': SubobjectView,
'telemetry-view': TelemetryView,
@@ -92,6 +122,7 @@ const ORDERS = {
bottom: Number.NEGATIVE_INFINITY
};
const DRAG_OBJECT_TRANSFER_PREFIX = 'openmct/domain-object/';
const DUPLICATE_OFFSET = 3;
let components = ITEM_TYPE_VIEW_MAP;
components['edit-marquee'] = EditMarquee;
@@ -112,6 +143,10 @@ export default {
domainObject: {
type: Object,
required: true
},
isEditing: {
type: Boolean,
required: true
}
},
data() {
@@ -138,7 +173,7 @@ export default {
let selectionPath = this.selection[0];
let singleSelectedLine = this.selection.length === 1 &&
selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type === 'line-view';
return selectionPath && selectionPath.length > 1 && !singleSelectedLine;
return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine;
}
},
inject: ['openmct', 'options', 'objectPath'],
@@ -301,9 +336,9 @@ export default {
if (this.isTelemetry(domainObject)) {
this.addItem('telemetry-view', domainObject, droppedObjectPosition);
} else {
let identifier = this.openmct.objects.makeKeyString(domainObject.identifier);
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.objectViewMap[identifier]) {
if (!this.objectViewMap[keyString]) {
this.addItem('subobject-view', domainObject, droppedObjectPosition);
} else {
let prompt = this.openmct.overlays.dialog({
@@ -326,6 +361,9 @@ export default {
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
},
handleDragOver($event) {
if (this.internalDomainObject.locked) {
return;
}
// Get the ID of the dragged object
let draggedKeyString = $event.dataTransfer.types
.filter(type => type.startsWith(DRAG_OBJECT_TRANSFER_PREFIX))
@@ -365,7 +403,8 @@ export default {
let count = this.telemetryViewMap[keyString] || 0;
this.telemetryViewMap[keyString] = ++count;
} else if (item.type === "subobject-view") {
this.objectViewMap[keyString] = true;
let count = this.objectViewMap[keyString] || 0;
this.objectViewMap[keyString] = ++count;
}
},
removeItem(selectedItems) {
@@ -384,17 +423,25 @@ export default {
return;
}
let keyString = this.openmct.objects.makeKeyString(item.identifier);
let keyString = this.openmct.objects.makeKeyString(item.identifier),
telemetryViewCount = this.telemetryViewMap[keyString],
objectViewCount = this.objectViewMap[keyString];
if (item.type === 'telemetry-view') {
let count = --this.telemetryViewMap[keyString];
telemetryViewCount = --this.telemetryViewMap[keyString];
if (count === 0) {
if (telemetryViewCount === 0) {
delete this.telemetryViewMap[keyString];
this.removeFromComposition(keyString);
}
} else if (item.type === 'subobject-view') {
delete this.objectViewMap[keyString];
objectViewCount = --this.objectViewMap[keyString];
if (objectViewCount === 0) {
delete this.objectViewMap[keyString];
}
}
if (!telemetryViewCount && !objectViewCount) {
this.removeFromComposition(keyString);
}
},
@@ -410,16 +457,44 @@ export default {
this.objectViewMap = {};
this.layoutItems.forEach(this.trackItem);
},
addChild(child) {
let identifier = this.openmct.objects.makeKeyString(child.identifier);
if (this.isTelemetry(child)) {
if (!this.telemetryViewMap[identifier]) {
this.addItem('telemetry-view', child);
isItemAlreadyTracked(child) {
let found = false,
keyString = this.openmct.objects.makeKeyString(child.identifier);
this.layoutItems.forEach(item => {
if (item.identifier) {
let itemKeyString = this.openmct.objects.makeKeyString(item.identifier);
if (itemKeyString === keyString) {
found = true;
return;
}
}
} else if (!this.objectViewMap[identifier]) {
this.addItem('subobject-view', child);
});
if (found) {
return true;
} else if (this.isTelemetry(child)) {
return this.telemetryViewMap[keyString] && this.objectViewMap[keyString];
} else {
return this.objectViewMap[keyString];
}
},
addChild(child) {
if (this.isItemAlreadyTracked(child)) {
return;
}
let type;
if (this.isTelemetry(child)) {
type = 'telemetry-view';
} else {
type = 'subobject-view';
}
this.addItem(type, child);
},
removeChild(identifier) {
let keyString = this.openmct.objects.makeKeyString(identifier);
@@ -512,9 +587,197 @@ export default {
}
},
updateTelemetryFormat(item, format) {
let index = this.layoutItems.findIndex(item);
let index = this.layoutItems.findIndex((layoutItem) => {
return layoutItem.id === item.id;
});
item.format = format;
this.mutate(`configuration.items[${index}]`, item);
},
createNewDomainObject(domainObject, composition, viewType, nameExtension, model) {
let identifier = {
key: uuid(),
namespace: this.internalDomainObject.identifier.namespace
},
type = this.openmct.types.get(viewType),
parentKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier),
objectName = nameExtension ? `${domainObject.name}-${nameExtension}` : domainObject.name,
object = {};
if (model) {
object = _.cloneDeep(model);
} else {
object.type = viewType;
type.definition.initialize(object);
object.composition.push(...composition);
}
object.name = objectName;
object.identifier = identifier;
object.location = parentKeyString;
this.openmct.objects.mutate(object, 'created', Date.now());
return object;
},
convertToTelemetryView(identifier, position) {
this.openmct.objects.get(identifier).then((domainObject) => {
this.composition.add(domainObject);
this.addItem('telemetry-view', domainObject, position);
});
},
dispatchMultipleSelection(selectItemsArray) {
let event = new MouseEvent('click', {
bubbles: true,
shiftKey: true,
cancelable: true,
view: window
})
selectItemsArray.forEach((id) => {
let refId = `layout-item-${id}`,
component = this.$refs[refId] && this.$refs[refId][0];
if (component) {
component.immediatelySelect = event;
component.$el.dispatchEvent(event);
}
});
},
duplicateItem(selectedItems) {
let objectStyles = this.internalDomainObject.configuration.objectStyles || {},
selectItemsArray = [],
newDomainObjectsArray = [];
selectedItems.forEach(selectedItem => {
let layoutItem = selectedItem[0].context.layoutItem,
domainObject = selectedItem[0].context.item,
layoutItemStyle = objectStyles[layoutItem.id],
copy = _.cloneDeep(layoutItem);
copy.id = uuid();
selectItemsArray.push(copy.id);
let offsetKeys = ['x', 'y'];
if (copy.type === 'line-view') {
offsetKeys = offsetKeys.concat(['x2', 'y2']);
}
if (copy.type === 'subobject-view') {
let newDomainObject = this.createNewDomainObject(domainObject, domainObject.composition, domainObject.type, 'duplicate', domainObject);
newDomainObjectsArray.push(newDomainObject);
copy.identifier = newDomainObject.identifier;
}
offsetKeys.forEach(key => {
copy[key] += DUPLICATE_OFFSET
});
if (layoutItemStyle) {
objectStyles[copy.id] = layoutItemStyle;
}
this.trackItem(copy);
this.layoutItems.push(copy);
});
this.$nextTick(() => {
this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems);
this.openmct.objects.mutate(this.internalDomainObject, "configuration.objectStyles", objectStyles);
this.$el.click(); //clear selection;
newDomainObjectsArray.forEach(domainObject => {
this.composition.add(domainObject);
});
this.dispatchMultipleSelection(selectItemsArray);
});
},
mergeMultipleTelemetryViews(selection, viewType) {
let identifiers = selection.map(selectedItem => {
return selectedItem[0].context.layoutItem.identifier;
}),
firstDomainObject = selection[0][0].context.item,
firstLayoutItem = selection[0][0].context.layoutItem,
position = [firstLayoutItem.x, firstLayoutItem.y],
mockDomainObject = {
name: 'Merged Telemetry Views',
identifier: firstDomainObject.identifier
},
newDomainObject = this.createNewDomainObject(mockDomainObject, identifiers, viewType);
this.composition.add(newDomainObject);
this.addItem('subobject-view', newDomainObject, position);
this.removeItem(selection);
this.initSelectIndex = this.layoutItems.length - 1;
},
mergeMultipleOverlayPlots(selection, viewType) {
let overlayPlots = selection.map(selectedItem => selectedItem[0].context.item),
overlayPlotIdentifiers = overlayPlots.map(overlayPlot => overlayPlot.identifier),
firstOverlayPlot = overlayPlots[0],
firstLayoutItem = selection[0][0].context.layoutItem,
position = [firstLayoutItem.x, firstLayoutItem.y],
mockDomainObject = {
name: 'Merged Overlay Plots',
identifier: firstOverlayPlot.identifier
},
newDomainObject = this.createNewDomainObject(mockDomainObject, overlayPlotIdentifiers, viewType),
newDomainObjectKeyString = this.openmct.objects.makeKeyString(newDomainObject.identifier),
internalDomainObjectKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier);
this.composition.add(newDomainObject);
this.addItem('subobject-view', newDomainObject, position);
overlayPlots.forEach(overlayPlot => {
if (overlayPlot.location === internalDomainObjectKeyString) {
this.openmct.objects.mutate(overlayPlot, 'location', newDomainObjectKeyString);
}
});
this.removeItem(selection);
this.initSelectIndex = this.layoutItems.length - 1;
},
getTelemetryIdentifiers(domainObject) {
let method = TELEMETRY_IDENTIFIER_FUNCTIONS[domainObject.type];
if (method) {
return method(domainObject, this.openmct);
} else {
throw 'No method identified for domainObject type';
}
},
switchViewType(context, viewType, selection) {
let domainObject = context.item,
layoutItem = context.layoutItem,
position = [layoutItem.x, layoutItem.y],
layoutType = 'subobject-view';
if (layoutItem.type === 'telemetry-view') {
let newDomainObject = this.createNewDomainObject(domainObject, [domainObject.identifier], viewType);
this.composition.add(newDomainObject);
this.addItem(layoutType, newDomainObject, position);
} else {
this.getTelemetryIdentifiers(domainObject).then((identifiers) => {
if (viewType === 'telemetry-view') {
identifiers.forEach((identifier, index) => {
let positionX = position[0] + (index * DUPLICATE_OFFSET),
positionY = position[1] + (index * DUPLICATE_OFFSET);
this.convertToTelemetryView(identifier, [positionX, positionY]);
});
} else {
let newDomainObject = this.createNewDomainObject(domainObject, identifiers, viewType);
this.composition.add(newDomainObject);
this.addItem(layoutType, newDomainObject, position);
}
});
}
this.removeItem(selection);
this.initSelectIndex = this.layoutItems.length - 1; //restore selection
}
}
}

View File

@@ -24,6 +24,7 @@
<layout-frame
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
@@ -70,7 +71,11 @@ export default {
type: Number,
required: true
},
initSelect: Boolean
initSelect: Boolean,
isEditing: {
type: Boolean,
required: true
}
},
computed: {
style() {
@@ -95,6 +100,13 @@ export default {
}
this.context.index = newIndex;
},
item(newItem) {
if (!this.context) {
return;
}
this.context.layoutItem = newItem;
}
},
mounted() {

View File

@@ -25,15 +25,15 @@
class="l-layout__frame c-frame"
:class="{
'no-frame': !item.hasFrame,
'u-inspectable': inspectable
'u-inspectable': inspectable,
'is-in-small-container': size.width < 600 || size.height < 600
}"
:style="style"
>
<slot></slot>
<div
class="c-frame-edit__move"
@mousedown="startMove([1,1], [0,0], $event)"
class="c-frame__move-bar"
@mousedown="isEditing ? startMove([1,1], [0,0], $event) : null"
></div>
</div>
</template>
@@ -54,9 +54,20 @@ export default {
required: true,
validator: (arr) => arr && arr.length === 2
&& arr.every(el => typeof el === 'number')
},
isEditing: {
type: Boolean,
required: true
}
},
computed: {
size() {
let {width, height} = this.item;
return {
width: (this.gridSize[0] * width),
height: (this.gridSize[1] * height)
};
},
style() {
let {x, y, width, height} = this.item;
return {

View File

@@ -22,7 +22,7 @@
<template>
<div
class="l-layout__frame c-frame no-frame"
class="l-layout__frame c-frame no-frame c-line-view"
:class="[styleClass]"
:style="style"
>
@@ -31,14 +31,20 @@
height="100%"
>
<line
class="c-line-view__hover-indicator"
v-bind="linePosition"
vector-effect="non-scaling-stroke"
/>
<line
class="c-line-view__line"
v-bind="linePosition"
:stroke="stroke"
stroke-width="2"
vector-effect="non-scaling-stroke"
/>
</svg>
<div
class="c-frame-edit__move"
class="c-frame__move-bar"
@mousedown="startDrag($event)"
></div>
<div
@@ -49,7 +55,8 @@
class="c-frame-edit__handle"
:class="startHandleClass"
@mousedown="startDrag($event, 'start')"
></div>
>
</div>
<div
class="c-frame-edit__handle"
:class="endHandleClass"
@@ -68,14 +75,18 @@ const START_HANDLE_QUADRANTS = {
1: 'c-frame-edit__handle--sw',
2: 'c-frame-edit__handle--se',
3: 'c-frame-edit__handle--ne',
4: 'c-frame-edit__handle--nw'
4: 'c-frame-edit__handle--nw',
5: 'c-frame-edit__handle--nw',
6: 'c-frame-edit__handle--ne'
};
const END_HANDLE_QUADRANTS = {
1: 'c-frame-edit__handle--ne',
2: 'c-frame-edit__handle--nw',
3: 'c-frame-edit__handle--sw',
4: 'c-frame-edit__handle--se'
4: 'c-frame-edit__handle--se',
5: 'c-frame-edit__handle--sw',
6: 'c-frame-edit__handle--nw'
};
export default {
@@ -158,6 +169,12 @@ export default {
},
vectorQuadrant() {
let {x, y, x2, y2} = this.position;
if (x2 === x) {
return 5; // Vertical line
}
if (y2 === y) {
return 6; // Horizontal line
}
if (x2 > x) {
if (y2 < y) {
return 1;
@@ -170,21 +187,27 @@ export default {
return 3;
},
linePosition() {
return this.vectorQuadrant % 2 !== 0
// odd vectorQuadrant slopes up
? {
x1: '0%',
y1: '100%',
x2: '100%',
y2: '0%'
}
// even vectorQuadrant slopes down
: {
x1: '0%',
y1: '0%',
x2: '100%',
y2: '100%'
};
let pos = {};
switch(this.vectorQuadrant) {
case 1:
case 3:
// slopes up
pos = {x1: '0%', y1: '100%', x2: '100%', y2: '0%'};
break;
case 5:
// vertical
pos = {x1: '0%', y1: '0%', x2: '0%', y2: '100%'};
break;
case 6:
// horizontal
pos = {x1: '0%', y1: '0%', x2: '100%', y2: '0%'};
break;
default:
// slopes down
pos = {x1: '0%', y1: '0%', x2: '100%', y2: '100%'};
break;
}
return pos;
}
},
watch: {
@@ -194,6 +217,13 @@ export default {
}
this.context.index = newIndex;
},
item(newItem) {
if (!this.context) {
return;
}
this.context.layoutItem = newItem;
}
},
mounted() {
@@ -202,8 +232,7 @@ export default {
layoutItem: this.item,
index: this.index
};
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.initSelect);
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context, this.initSelect);
},
destroyed() {
if (this.removeSelectable) {
@@ -217,12 +246,17 @@ export default {
document.body.addEventListener('mousemove', this.continueDrag);
document.body.addEventListener('mouseup', this.endDrag);
this.startPosition = [event.pageX, event.pageY];
this.dragPosition = {
x: this.item.x,
y: this.item.y,
x2: this.item.x2,
y2: this.item.y2
};
let {x, y, x2, y2} = this.item;
this.dragPosition = {x, y, x2, y2};
if (x === x2 || y === y2) {
if (y > y2 || x < x2) {
if (this.dragging === 'start') {
this.dragging = 'end';
} else if (this.dragging === 'end') {
this.dragging = 'start';
}
}
}
event.preventDefault();
},
continueDrag(event) {
@@ -256,7 +290,7 @@ export default {
},
calculateDragPosition(pxDeltaX, pxDeltaY) {
let gridDeltaX = Math.round(pxDeltaX / this.gridSize[0]);
let gridDeltaY = Math.round(pxDeltaY / this.gridSize[0]); // TODO: should this be gridSize[1]?
let gridDeltaY = Math.round(pxDeltaY / this.gridSize[1]);
let {x, y, x2, y2} = this.item;
let dragPosition = {x, y, x2, y2};

View File

@@ -24,6 +24,7 @@
:item="item"
:grid-size="gridSize"
:title="domainObject && domainObject.name"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
@@ -61,7 +62,7 @@ function hasFrameByDefault(type) {
}
export default {
makeDefinition(openmct, gridSize, domainObject, position) {
makeDefinition(openmct, gridSize, domainObject, position, viewKey) {
let defaultDimensions = getDefaultDimensions(gridSize);
position = position || DEFAULT_POSITION;
@@ -71,7 +72,8 @@ export default {
x: position[0],
y: position[1],
identifier: domainObject.identifier,
hasFrame: hasFrameByDefault(domainObject.type)
hasFrame: hasFrameByDefault(domainObject.type),
viewKey
};
},
inject: ['openmct', 'objectPath'],
@@ -94,6 +96,10 @@ export default {
index: {
type: Number,
required: true
},
isEditing: {
type: Boolean,
required: true
}
},
data() {
@@ -109,6 +115,13 @@ export default {
}
this.context.index = newIndex;
},
item(newItem) {
if (!this.context) {
return;
}
this.context.layoutItem = newItem;
}
},
mounted() {
@@ -131,7 +144,8 @@ export default {
childContext.index = this.index;
this.context = childContext;
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.initSelect);
this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect;
});
}
}

View File

@@ -24,16 +24,23 @@
<layout-frame
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
<div
v-if="domainObject"
class="c-telemetry-view"
:class="styleClass"
:class="{
styleClass,
'is-missing': domainObject.status === 'missing'
}"
:style="styleObject"
@contextmenu.prevent="showContextMenu"
>
<div class="is-missing__indicator"
title="This item is missing"
></div>
<div
v-if="showLabel"
class="c-telemetry-view__label"
@@ -105,6 +112,10 @@ export default {
index: {
type: Number,
required: true
},
isEditing: {
type: Boolean,
required: true
}
},
data() {
@@ -168,6 +179,10 @@ export default {
this.context.index = newIndex;
},
item(newItem) {
if (!this.context) {
return;
}
this.context.layoutItem = newItem;
}
},
@@ -242,7 +257,8 @@ export default {
updateTelemetryFormat: this.updateTelemetryFormat
};
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.initSelect);
this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect;
},
updateTelemetryFormat(format) {
this.$emit('formatChanged', this.item, format);

View File

@@ -24,6 +24,7 @@
<layout-frame
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
@@ -75,7 +76,11 @@ export default {
type: Number,
required: true
},
initSelect: Boolean
initSelect: Boolean,
isEditing: {
type: Boolean,
required: true
}
},
computed: {
style() {
@@ -91,6 +96,13 @@ export default {
}
this.context.index = newIndex;
},
item(newItem) {
if (!this.context) {
return;
}
this.context.layoutItem = newItem;
}
},
mounted() {

View File

@@ -0,0 +1,60 @@
.c-box-view {
border-width: $drawingObjBorderW !important;
display: flex;
align-items: stretch;
.c-frame & {
@include abs();
}
}
.c-line-view {
&.c-frame {
box-shadow: none !important;
}
.c-frame-edit {
border: none;
}
.c-handle-info {
background: rgba(#999, 0.2);
padding: 2px;
position: absolute;
top: 5px; left: 5px;
white-space: nowrap;
}
svg {
// Prevent clipping when line is horizontal and vertical
min-height: 1px;
min-width: 1px;
// Must use !important to counteract setting in normalize.min.css
overflow: visible;
}
&__line {
stroke-linecap: round;
stroke-width: $drawingObjBorderW;
}
&__hover-indicator {
display: none;
opacity: 0.5;
stroke: $editFrameColorHov;
stroke-width: $drawingObjBorderW + 4;
}
.is-editing & {
// Needed to allow line to be moved
$w: 4px;
min-width: $w;
min-height: $w;
&:hover {
[class*='__hover-indicator'] {
display: inline;
}
}
}
}

View File

@@ -1,8 +0,0 @@
.c-box-view {
display: flex;
align-items: stretch;
.c-frame & {
@include abs();
}
}

View File

@@ -45,8 +45,7 @@
&[s-selected],
&[s-selected-parent] {
// Display grid and allow edit marquee to display in nested layouts when editing
> * > * > .l-layout {
background: $editUIGridColorBg;
> * > * > .l-layout + .allow-editing {
box-shadow: inset $editUIGridColorFg 0 0 2px 1px;
> [class*='grid-holder'] {

View File

@@ -7,9 +7,13 @@
> *:first-child {
flex: 1 1 auto;
}
&.is-in-small-container {
//background: rgba(blue, 0.1);
}
}
.c-frame-edit__move {
.c-frame__move-bar {
display: none;
}
@@ -29,7 +33,7 @@
border: $editFrameSelectedBorder;
box-shadow: $editFrameSelectedShdw;
.c-frame-edit__move {
.c-frame__move-bar {
cursor: move;
}
}
@@ -37,7 +41,7 @@
/******************* DEFAULT STYLES FOR -EDIT__MOVE */
// All object types
.c-frame-edit__move {
.c-frame__move-bar {
@include abs();
display: block;
}
@@ -52,7 +56,7 @@
transition-delay: $moveBarOutDelay;
}
+ .c-frame-edit__move {
+ .c-frame__move-bar {
display: none;
}
@@ -61,14 +65,14 @@
.l-layout {
/******************* 0 - 1 ITEM SELECTED */
&:not(.is-multi-selected) {
> .l-layout__frame[s-selected] {
> .l-layout__frame {
> .c-so-view.has-complex-content {
> .c-so-view__local-controls {
transition: transform $transOutTime ease-in-out;
transition-delay: $moveBarOutDelay;
}
+ .c-frame-edit__move {
+ .c-frame__move-bar {
transition: $transOut;
transition-delay: $moveBarOutDelay;
@include userSelectNone();
@@ -89,7 +93,7 @@
$lrOffset: 25%;
@include grippy($editFrameMovebarColorFg);
content: '';
display: block;
display: none;
position: absolute;
top: $tbOffset;
right: $lrOffset;
@@ -111,7 +115,7 @@
transition-delay: 0s;
}
+ .c-frame-edit__move {
+ .c-frame__move-bar {
transition: $transIn;
transition-delay: 0s;
height: $editFrameMovebarH;
@@ -119,12 +123,19 @@
}
}
}
> .l-layout__frame[s-selected] {
> .c-so-view.has-complex-content {
+ .c-frame__move-bar:before {
display: block;
}
}
}
}
/******************* > 1 ITEMS SELECTED */
&.is-multi-selected {
.l-layout__frame[s-selected] {
> .c-so-view.has-complex-content + .c-frame-edit__move {
> .c-so-view.has-complex-content + .c-frame__move-bar {
display: block;
}
}

View File

@@ -26,4 +26,15 @@
@include abs();
border: 1px solid transparent;
}
@include isMissing($absPos: true);
.is-missing__indicator {
top: 0;
left: 0;
}
&.is-missing {
border: $borderMissing;
}
}

View File

@@ -54,10 +54,11 @@ export default function DisplayLayoutPlugin(options) {
},
data() {
return {
domainObject: domainObject
domainObject: domainObject,
isEditing: openmct.editor.isEditing()
};
},
template: '<layout ref="displayLayout" :domain-object="domainObject"></layout>'
template: '<layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></layout>'
});
},
getSelectionContext() {
@@ -66,8 +67,15 @@ export default function DisplayLayoutPlugin(options) {
supportsMultiSelect: true,
addElement: component && component.$refs.displayLayout.addElement,
removeItem: component && component.$refs.displayLayout.removeItem,
orderItem: component && component.$refs.displayLayout.orderItem
}
orderItem: component && component.$refs.displayLayout.orderItem,
duplicateItem: component && component.$refs.displayLayout.duplicateItem,
switchViewType: component && component.$refs.displayLayout.switchViewType,
mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews,
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots
};
},
onEditModeChange: function (isEditing) {
component.isEditing = isEditing;
},
destroy() {
component.$destroy();

View File

@@ -53,6 +53,7 @@
:index="i"
:container-index="index"
:is-editing="isEditing"
:object-path="objectPath"
/>
<drop-hint
@@ -105,6 +106,14 @@ export default {
isEditing: {
type: Boolean,
default: false
},
locked: {
type: Boolean,
default: false
},
objectPath: {
type: Array,
required: true
}
},
computed: {
@@ -130,6 +139,10 @@ export default {
},
methods: {
allowDrop(event, index) {
if (this.locked) {
return false;
}
if (event.dataTransfer.types.includes('openmct/domain-object-path')) {
return true;
}

View File

@@ -57,6 +57,8 @@
:container="container"
:rows-layout="rowsLayout"
:is-editing="isEditing"
:locked="domainObject.locked"
:object-path="objectPath"
@move-frame="moveFrame"
@new-frame="setFrameLocation"
@persist="persist"
@@ -136,7 +138,7 @@ function sizeToFill(items) {
}
export default {
inject: ['openmct', 'layoutObject'],
inject: ['openmct', 'objectPath', 'layoutObject'],
components: {
ContainerComponent,
ResizeHandle,

View File

@@ -37,7 +37,7 @@
v-if="domainObject"
ref="objectFrame"
:domain-object="domainObject"
:object-path="objectPath"
:object-path="currentObjectPath"
:has-frame="hasFrame"
:show-edit-view="false"
/>
@@ -77,12 +77,16 @@ export default {
isEditing: {
type: Boolean,
default: false
},
objectPath: {
type: Array,
required: true
}
},
data() {
return {
domainObject: undefined,
objectPath: undefined
currentObjectPath: undefined
}
},
computed: {
@@ -107,7 +111,7 @@ export default {
methods: {
setDomainObject(object) {
this.domainObject = object;
this.objectPath = [object];
this.currentObjectPath = [object].concat(this.objectPath);
this.setSelection();
},
setSelection() {

View File

@@ -38,7 +38,7 @@ define([
canEdit: function (domainObject) {
return domainObject.type === 'flexible-layout';
},
view: function (domainObject) {
view: function (domainObject, objectPath) {
let component;
return {
@@ -46,6 +46,7 @@ define([
component = new Vue({
provide: {
openmct,
objectPath,
layoutObject: domainObject
},
el: element,

View File

@@ -70,6 +70,10 @@ function ToolbarProvider(openmct) {
}
if (primary.context.type === 'frame') {
if (secondary.context.item.locked) {
return [];
}
let frameId = primary.context.frameId;
let layoutObject = tertiary.context.item;
let containers = layoutObject
@@ -143,6 +147,9 @@ function ToolbarProvider(openmct) {
toggleContainer.domainObject = secondary.context.item;
} else if (primary.context.type === 'container') {
if (primary.context.item.locked) {
return [];
}
deleteContainer = {
control: "button",
@@ -187,6 +194,9 @@ function ToolbarProvider(openmct) {
};
} else if (primary.context.type === 'flexible-layout') {
if (primary.context.item.locked) {
return [];
}
addContainer = {
control: "button",

View File

@@ -1,13 +1,18 @@
<template>
<a
class="l-grid-view__item c-grid-item"
:class="{ 'is-alias': item.isAlias === true }"
:class="{
'is-alias': item.isAlias === true,
'is-missing': item.model.status === 'missing',
'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1
}"
:href="objectLink"
>
<div
class="c-grid-item__type-icon"
:class="(item.type.cssClass != undefined) ? 'bg-' + item.type.cssClass : 'bg-icon-object-unknown'"
></div>
>
</div>
<div class="c-grid-item__details">
<!-- Name and metadata -->
<div
@@ -22,6 +27,9 @@
</div>
</div>
<div class="c-grid-item__controls">
<div class="is-missing__indicator"
title="This item is missing"
></div>
<div
class="icon-people"
title="Shared"

View File

@@ -7,13 +7,19 @@
<td class="c-list-item__name">
<a
ref="objectLink"
class="c-object-label"
:class="{ 'is-missing': item.model.status === 'missing' }"
:href="objectLink"
>
<div
class="c-list-item__type-icon"
class="c-object-label__type-icon c-list-item__type-icon"
:class="item.type.cssClass"
></div>
<div class="c-list-item__name-value">{{ item.model.name }}</div>
>
<span class="is-missing__indicator"
title="This item is missing"
></span>
</div>
<div class="c-object-label__name c-list-item__name">{{ item.model.name }}</div>
</a>
</td>
<td class="c-list-item__type">

View File

@@ -38,7 +38,15 @@
// Object is an alias to an original.
[class*='__type-icon'] {
@include isAlias();
color: $colorIconAliasForKeyFilter;
}
}
&.is-missing {
@include isMissing();
[class*='__type-icon'],
[class*='__details'] {
opacity: $opacityMissing;
}
}
@@ -85,15 +93,14 @@
body.desktop & {
$transOutMs: 300ms;
flex-flow: column nowrap;
transition: background $transOutMs ease-in-out;
transition: $transOutMs ease-in-out;
&:hover {
background: $colorItemBgHov;
filter: $filterItemHoverFg;
transition: $transIn;
.c-grid-item__type-icon {
filter: $colorKeyFilterHov;
transform: scale(1);
transform: scale(1.1);
transition: $transInBounce;
}
}
@@ -103,7 +110,7 @@
}
&__controls {
align-items: start;
align-items: baseline;
flex: 0 0 auto;
order: 1;
.c-info-button,
@@ -115,7 +122,6 @@
font-size: floor($gridItemDesk / 3);
margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%;
order: 2;
transform: scale(0.9);
transform-origin: center;
transition: all $transOutMs ease-in-out;
}

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