Compare commits

...

97 Commits

Author SHA1 Message Date
f04c274d33 fix lint issues 2020-11-12 16:02:57 -08:00
3624236c26 Merge branch 'three-dot-menu-proto' of https://github.com/nasa/openmct into openmct-status-api 2020-11-12 15:05:28 -08:00
0126542411 merge latest master 2020-11-12 15:04:09 -08:00
2c49e62863 merge with latest three-dots 2020-11-12 15:01:37 -08:00
c2df3cdd14 CSS and markup refactoring to support addition of 'suspect' telemetry (#3499)
* CSS and markup refactoring to support addition of 'suspect' telemetry

- Significant refactoring of CSS classing: `is-missing` is now
`is-status--missing` and `is-missing__indicator` is now simply
`is-status__indicator`, allowing the wrapping `is-missing--*` class to
control what is displayed;
- New SCSS mixin @isStatus, and changes to mixin @isMissing to support
new `is-status--suspect` class;
- Changed titling for missing objects from 'This item is missing' to
'This item is missing or suspect'. **IMPORTANT NOTE** This is temporary
and should be replaced with a more robust approach to titling that
allows title strings to be defined in configuration and dynamically
applied;
- Refactored computed property `statusClass` across multiple components
to return an empty string when status is undefined - this was
previously erroneously returning `is-undefined` in that circumstance;
- Removed commented code;

* CSS and markup refactoring to support addition of 'suspect' telemetry

- Refinements to broaden capability of `is-status*` mixin;
2020-11-12 14:58:51 -08:00
b0203f2272 Preparing master for the next sprint v1.4.1-SNAPSHOT (#3508) 2020-11-09 13:10:50 -08:00
77b720d00d Fix Imagery for VERVE #266 (#3507)
* Fairly extensive refactoring to fix layout in Safari for VERVE #266

- VERY WIP at this time!
- Many instances of `height: 100%` converted or amended to include
`flex: 1 1 auto`;
- Some high-use containers like `c-so-view__object-view` converted to use
flex layout;
- Views fixed generally for sub-object view, and specifically for
Conditionals, Folder grid view and Imagery;
- Imagery background image holder converted to use absolute positioning;
- TODO: Notebook has a problem where the side nav pane isn't overlaying
in Safari - it's a JS thing, c-drawer--push isn't be replaced with
c-drawer--overlays as it should;

* CSS and markup refactoring to support addition of 'suspect' telemetry

- Remove commented code;
2020-11-09 09:33:25 -08:00
ba982671b2 Quick idea on a splash screen that will not increase load time (#3376)
* New splash screen

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-11-06 13:58:57 -08:00
02be8a9875 merge with master 2020-11-03 13:48:28 -08:00
3bd57c8fff add tests for actionsAPI 2020-11-03 13:43:45 -08:00
5df7d92d64 [Navigation Tree] Fix tree loading issue (#3500)
* added resize observer for FIRST load of mainTree

* new Promise driven height calculation test

* cleaning up code, sticking with promise height caclcuations

* more cleanup

* returning from the initialize function
2020-11-03 12:06:49 -08:00
a8228406de [Inspector] Allow styles (including font and font size) to be saved and reused (#3432)
* working proto for font size

* wip

* Font styling

 - Base classes for font-size and font;
 - WIP!

* working data attribute for fontsize

* Font styling

 - Add `js-style-receiver` to markup, refine style targeting JS for
 better application of styles;
 - Refinements to font and size CSS;
 - WIP!

* Font styling

 - Redo CSS to use `data-*` attributes;
 - New `u-style-receiver` class for use as font-size and font-family CSS
 selector target;
 - New `js-style-receiver` class for use as JS target by ObjectView.vue;
 - New classes added to markup in all Open MCT views;
 - Changed font-size values from 'is-font-size--*' to just the number;
 - Some refinement to individual views to account for font-sizing
 capability;
 - Removed automatic font-size 13px being set by SubobjectView.vue;
 - WIP!

* working mixed styles

* Font styling

 - Added `u-style-receiver` to TelemetryView.vue;
 - Added `icon-font-size` to Font Size dropdown button;
 - TODO: better font-size icon;

* working font-family

* Font styling

 - Art for `icon-font-size` glyph updated;
 - Redefined glyph usage in some Layout toolbar buttons;
 - Updated font-size and font dropdown menus options text;

* Font styling

 - Refined font-size and font dropdown values;
 - Fixed toolbar-select-menu.vue to remove 'px' from non-specific option
  return;

* dont allow font styling on layouts that contain other layouts

* fix lint warning

* add sizing row

* fix bug with column width sizing

* fix bug with header style

* add saved styles inspector view

* WIP

* add vue component for selector

* WIP styles manager to communicate between vue components

* WIP saving and persisting styles

* no duplicate styles prevention

* fix props syntax

* WIP can apply conditional styles

* static styles do not work yet

* display border color in saved styles swatch

* allow deleting styles except default style

* WIP apply static style works but also to layout...

* prevent additional StylesView from being created

* delete style message

* change save order

* move applystyle to selector component

* rename for consistency

* naming refactor

* add style description

* update style properties only if they exist and do not erase properties

* refactor singleton usage

refactor save method

* show save and delete only on hover

* do not show delete icon if not in edit mode

* normalize styles before saving

prevent apply style if conditional and static styles are simultaneously selected

* remove default style

tweak selector display

* allow conditional and static styles to have saved style applied

limit saved styles to 20

* refactor styles manager

remove openmct dependency

use provide/inject

* resolve merge conflicts

* lint fix

* reorganize styles

* add font style editor to styles view

* save and display border correctly in saved styles view

* WIP add font styling controls to inspector styles view

* add font constants

* WIP refactor to provide reactive props

fix locked for edit

* WIP display consolidated font styles for selection in editor

* WIP font styles saved to layout

* WIP persisting font styles from inspector works

* fix styleable check

* move logic up to stylesview because save is two part

* apply font style to thumb

* there can be only one

* show font style for native views

* linting fix

* push stylesManager work to StylesView

* move method to computed

* move constant definition outside of function call

* Styling for saved styles functionality WIP

- Simplified and removed unnecessary markup;
- Standardized style applied to saved style element and toolbar control;
- Removed saved style expand arrow and description, replaced with item
title / tooltip approach;
- Standardized width of `c-style-thumb` element;
- Moved font size and style controls to the designed location;

* Styling for saved styles functionality WIP

- Layout and CSS normalization between style editor control and saved
style preview element;
- Control alignment refined;
- Moved font size and style controls to the designed location;

* Styling for saved styles functionality WIP

- Update font size icon art to normalize size;
- Sanding, tweaking, alignin and layout in style controls area of
Inspector;

* Styling for saved styles functionality WIP

- Hide the font size and style menu buttons unless the user is editing;

* remove font controls from toolbar

* turn styles tab into multipane element

* lint fix

* no font style should not be viewed as non-specific

* delete saved style by index not style

* cleanup

* view and inspector view updates on initial font change

* revert computed back to method

* set initial height

* fix test after removing 2 buttons from toolbar

* fix hidden lint error

* fix lint

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-11-02 12:35:43 -08:00
2401473012 [#3465] Intercept drag start event for imagery controls (#3485) 2020-11-02 11:26:33 -08:00
94091b25ec remove focused describe 2020-11-02 11:09:59 -08:00
c191ffb37d add tests for MenuAPI 2020-11-02 11:06:59 -08:00
e502fb88fa Fix Imagery brightness and contrast controls (#3473)
* Fix imagery #3467

- Move location of imagery controls in markup;
- Refine vertical placement;

* Fix imagery #3467

- Fix Firefox-related slider problems: bring over slider fixes and
markup from branch `imagery-view-layers`;

* Fix imagery #3467

- Fix linting problem;

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2020-11-02 08:38:13 -08:00
37a52cb011 Notebook fixes for NT10 'click-to-edit entry' (#3475)
* Notebook fixes for NT10 'click-to-edit entry'

- Hovering over entries now displays a subtle background change, and
only displays the 'inline input' look when clicked into;
- Changed default styling and behavior to not apply default text
content: new entries now start with a blank entry, and do not include
'placeholder' formatting;
- Refactored styles associated with `c-input-inline`, `c-ne__input` and
`reactive-input` mixin;
- New mixin `inlineInput`;
- Removed unused CSS classes, general cleanups;

* fixed defaultText as blank issue and some cleanup

* Update _mixins.scss

- Remove commented code;

Co-authored-by: Nikhil Mandlik <nikhil.k.mandlik@nasa.gov>
2020-10-30 16:47:29 -07:00
04fb4e8a82 [Tables] Object names should appear in tables (#3466)
* [Tables] Object names should appear in tables #3312

* updated tests to include name header.

* fixed lint issue.

* Removed Name from data.

* renamed 'addColunmName'  to 'addNameColumn'.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-10-30 15:10:31 -07:00
5f03dc45ee fix style application in telemetry vue 2020-10-29 15:44:57 -07:00
14ac758760 Merge branch 'three-dot-menu-proto' of https://github.com/nasa/openmct into openmct-status-api 2020-10-29 15:30:48 -07:00
eb709a60cb Merge branch 'master' of https://github.com/nasa/openmct into three-dot-menu-proto 2020-10-29 15:24:51 -07:00
eba1a48a44 fix lint errors 2020-10-29 15:19:42 -07:00
4a0654dbcb add tests for status API 2020-10-29 15:07:02 -07:00
9b6d339d69 add status to tabs view 2020-10-29 13:48:51 -07:00
f90afb9277 simplifying class applications 2020-10-29 13:35:19 -07:00
018dfb1e28 Merge branch 'master' of https://github.com/nasa/openmct into openmct-status-api 2020-10-29 11:59:03 -07:00
5646a252f7 [Navigation Tree] Simplify logic (#3474)
* added new navigation method for tracking, lots of optimizations

* updated indicator logic, tweaked objectPath/navigationPath, removed old code

* added temporary ancestors variable to be used while building new tree ui during navigation

* removed observer for ancestors, all handled in composition watch now

* updates from PR comments

* fixing testing errors

* checking for older format of saved path, update if old
2020-10-29 11:58:45 -07:00
c72a02aaa3 wip 2020-10-29 11:07:49 -07:00
0e6ce7f58b [Time Conductor] Realtime presets and history tracking (#3270)
Time conductor realtime preset/history updates

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-10-28 17:46:28 -07:00
8cd6a4c6a3 [Notebook] Link to snapshot should not be a fully qualified url #3445 (#3460)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-10-28 16:46:54 -07:00
02fc162197 Save subobject styles to container/layout if the object cannot be persisted (#3471)
* styles for Subobjects that can't be persisted should be saved on the container/layout

* Add tests for suboject styles that should be saved on the display layout
2020-10-26 15:58:42 -07:00
84d21a3695 [Display Layout] User should be able to set outer dimensions (#3333)
* Display Layout grid toggle and dimensions

- Added toggle grid button;
- Added Layout 'size' properties;
- Very WIP!

* Display Layout grid toggle and dimensions

- Cleanup toolbar;

* new configuration layoutDimensions

* add outer dimensions

* content dimensions not needed

* show/hide layout dimensions based on selection

* push non-dynamic styles to class definition

* remove grid code for other display layout feature

* reorder to match master

* layoutDimensionsStyle computed prop should return an object

* Styling for Display Layout dimensions box

- Mods to markup and SCSS;
- New ``$editDimensionsColor` theme constant;

* Styling for Display Layout dimensions box

- Refined styling;
- Fixed selector for nested sub-layouts;

* Styling for Display Layout dimensions box

- Added v-if that now only displays the dimensions indicator if both
width and height are greater than 0;

* fix lint issues

* fix merge issues

* fix display layout dimensions logic

* fix display layout dimensions check

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-10-23 12:19:16 -07:00
1a6369c2b9 [Display Layout] Grid lines should show and hide appropriately for nested layouts (#3330)
* change selector from sibling to same element

* hide gridlines for selected layout if is multi selection
2020-10-23 10:02:18 -07:00
bf3fd66942 fix broken export marked rows 2020-10-20 16:54:53 -07:00
8414ded1ec Merge branch 'master' into three-dot-menu-proto 2020-10-19 18:01:44 -07:00
646c871c76 Merge branch 'master' of https://github.com/nasa/openmct into three-dot-menu-proto 2020-10-19 10:29:38 -07:00
ba401b3341 fix lint errors 2020-10-18 15:55:39 -07:00
5ef02ec4a2 git enable legacy toolbar for legacy tables 2020-10-15 11:55:21 -07:00
d788031019 Merge branch 'master' of https://github.com/nasa/openmct into three-dot-menu-proto 2020-10-15 09:49:32 -07:00
d870874649 Alphanumeric notebook entry (#3387)
* [Notebook] Copy label and value from alphanumeric in Layout directly to a Notebook entry #3299

* changes to use updated action API.

* added 'copyToNotebook' + some refactor.

* Added current default notebook name to 'Copy to Notebook' string.

* string delimiter updated.

* cleanup

* updated per review comments

* updated as per review comments.

* fixed rebase issue.

* corrected case in import.

* fixed lint errors.

* fixed test error, Unhandled promise rejection: TypeError: 'clipboard-write'

* removed navigator.permissions.query check
2020-10-07 14:01:36 -07:00
711a7a2eb5 fix console error on changing types and canceling edit 2020-10-07 12:30:07 -07:00
c105a08cfe add tests for viewDatum action plugin 2020-10-06 15:35:08 -07:00
b87375a809 remove vue style 2020-10-06 12:37:28 -07:00
9fed056d22 add a viewDatum Action 2020-10-06 12:37:00 -07:00
251bf21933 Merge branch 'master' into three-dot-menu-proto 2020-10-05 11:25:55 -07:00
a180bf7c02 fix lint error 2020-10-05 11:16:34 -07:00
ed8a54f0f9 fix broken spec files 2020-10-05 11:12:07 -07:00
ff3c2da0f9 fix broken tests and add update on isEditing for actionCollections 2020-10-05 10:31:01 -07:00
28d5821120 fix lint issues 2020-10-02 15:08:07 -07:00
f5ee457274 Merge branch 'master' into three-dot-menu-proto 2020-10-02 15:02:20 -07:00
9d2770e4d2 update according to andrews comments 2020-10-02 15:01:04 -07:00
8b25009816 reverse check for viewProvider.getViewContext 2020-10-02 11:48:47 -07:00
074fe4481a linting fixes 2020-10-02 11:42:41 -07:00
fbd928b842 Merge branch 'master' of https://github.com/nasa/openmct into three-dot-menu-proto 2020-10-02 11:25:43 -07:00
110947db09 update according to review comments 2020-10-02 11:22:18 -07:00
ef91e92fbc wip 2020-09-30 13:13:50 -07:00
d201cac4ac update changes 2020-09-30 13:07:18 -07:00
dcb3ccfec7 use weak map to cache actionCollections 2020-09-30 12:44:28 -07:00
78522cd4f1 remove unecessary computed properties 2020-09-16 10:07:20 -07:00
ca232d45cc notebook and menu switchers to use menu api 2020-09-16 09:57:29 -07:00
df495c841a merge master 2020-09-16 09:07:26 -07:00
92a37ef36b Merge branch 'master' into three-dot-menu-proto 2020-09-14 09:52:10 -07:00
fd731ca430 fix ladrow and tablerow use of contextMenus 2020-09-09 14:18:20 -07:00
263b1cd3d5 fix telemetry view view historical 2020-09-09 13:58:28 -07:00
978fc8b5a3 performance enhancements 2020-09-09 11:37:49 -07:00
698ccc5a35 fix bug with object update 2020-09-09 10:15:19 -07:00
e5aa5b5a5f fix onDestroy error 2020-09-08 16:06:45 -07:00
b942988ef8 Merge branch 'master' of https://github.com/nasa/openmct into three-dot-menu-proto 2020-09-08 16:00:10 -07:00
1eec20f2ea 3 dot menu refactor (#3360)
* refactoring actions api

* wip

* wip

* almost done

* fix lint issues
2020-09-08 15:59:42 -07:00
767a2048eb Three dot menu implementation WIP
- Hide frame control labels when object is within a Flexible Layout;
2020-09-03 13:04:48 -07:00
e65cf1661c Three dot menu implementation WIP
- Recast "Preview" action as "View";
2020-09-01 10:08:36 -07:00
0eae48646c Three dot menu implementation WIP
- Merge latest master, fix conflicts;
2020-08-31 14:09:01 -07:00
0ba8a275d2 Three dot menu implementation WIP
- Fixed button title;
2020-08-27 08:56:32 -07:00
d8d32cc3ac Three dot menu implementation WIP
- Changed Snapshot-related buttons and menus to use `icon-camera`;
- Normalized padding for c-icon-buttons;
- Added a CSS class `c-so-view--<domainObject.type>` to allow
sub-layouts with hidden frames to completely hide their headers and
buttons - this is needed to avoid overlap collisions with further
sub-objects;
- Changed button styling in main view to be more in line with 'iconic'
approach used elsewhere, enabled button labels where applicable;
- Better, more consistent hover approach for `c-button` and
`c-icon-button` controls;
- Changed Snow theme constant hover `filter` value for
better color matching;
- Fixed `c-object-label` type-icon opacity;
- Changed Snow theme constant for object name to fix inline editing of
object name being too dark;
2020-08-25 18:47:55 -07:00
a800848fe1 Three dot menu implementation WIP
- Make button labels hide/show based on frame size in Layout;
2020-08-25 14:30:40 -07:00
6881d98ba6 Three dot menu implementation WIP
- Initial styling and hover behavior for Layout frame controls;
- Defined default color for c-icon-buttons;
- Changed buttons from `c-button` to `c-icon-button`;
2020-08-21 18:42:32 -07:00
48d077cd2e Three dot menu implementation WIP
- Merge in `add-glyphs-082020`;
2020-08-20 14:08:15 -07:00
030dd93c91 Add new glyphs
- Grid on, grid off and camera;
2020-08-20 11:10:22 -07:00
03bf6fc0a3 Three dot menu implementation WIP
- Add icomoon config JSON file;
2020-08-20 10:09:03 -07:00
ef0a2ed5d2 Three dot menu implementation WIP
- Add new `icon-3-dots` glyph;
2020-08-20 10:08:52 -07:00
a40aa84752 Three dot menu implementation WIP
- Add icomoon config JSON file;
2020-08-19 14:40:22 -07:00
d3b69dda82 Three dot menu implementation WIP
- Add new `icon-3-dots` glyph;
2020-08-19 14:38:47 -07:00
d3126ebf5c Three dot menu implementation WIP
- Replace inline styling;
- Style for c-menu `__section-separator` and `__section-hint` refined;
2020-08-19 12:59:20 -07:00
4479cbc7a2 abstract logic into actionAPI 2020-08-18 16:26:20 -07:00
f8ff44dac0 wip 2020-08-18 15:52:59 -07:00
8f4280d15b fix table row marking 2020-08-18 15:43:00 -07:00
6daa27ff31 merge with master 2020-08-18 15:24:15 -07:00
43f6c3f85d wip 2020-08-18 15:12:39 -07:00
1a7c76cf3e updated to new actions API 2020-08-18 13:56:26 -07:00
cee9cd7bd1 working groups 2020-08-14 10:15:13 -07:00
c42df20281 combine domainObject actions and view actions 2020-08-13 18:42:58 -07:00
b4149bd2b3 working in objectFrame 2020-08-13 17:39:35 -07:00
f436ac9ba0 working proto 2020-08-13 16:26:29 -07:00
8493b481dd make reviewer requested changes 2020-08-13 14:46:18 -07:00
28723b59b7 Merge branch 'master' into context-menu-option 2020-06-10 16:36:33 -07:00
9fa7de0b77 remove unused files 2020-06-10 16:30:24 -07:00
54bfc84ada replaced contextMenu with overlay menu 2020-06-10 16:06:42 -07:00
148 changed files with 4641 additions and 1345 deletions

View File

@ -76,6 +76,7 @@ define([
workerRequest[prop] = Number(workerRequest[prop]); workerRequest[prop] = Number(workerRequest[prop]);
}); });
workerRequest.name = domainObject.name; workerRequest.name = domainObject.name;
return workerRequest; return workerRequest;

View File

@ -108,7 +108,6 @@
for (; nextStep < end && data.length < 5000; nextStep += step) { for (; nextStep < end && data.length < 5000; nextStep += step) {
data.push({ data.push({
name: request.name,
utc: nextStep, utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000, yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness), sin: sin(nextStep, period, amplitude, offset, phase, randomness),

View File

@ -30,12 +30,50 @@
<link rel="icon" type="image/png" href="dist/favicons/favicon-96x96.png" sizes="96x96" type="image/x-icon"> <link rel="icon" type="image/png" href="dist/favicons/favicon-96x96.png" sizes="96x96" type="image/x-icon">
<link rel="icon" type="image/png" href="dist/favicons/favicon-32x32.png" sizes="32x32" type="image/x-icon"> <link rel="icon" type="image/png" href="dist/favicons/favicon-32x32.png" sizes="32x32" type="image/x-icon">
<link rel="icon" type="image/png" href="dist/favicons/favicon-16x16.png" sizes="16x16" type="image/x-icon"> <link rel="icon" type="image/png" href="dist/favicons/favicon-16x16.png" sizes="16x16" type="image/x-icon">
<style type="text/css">
@keyframes splash-spinner {
0% {
transform: translate(-50%, -50%) rotate(0deg); }
100% {
transform: translate(-50%, -50%) rotate(360deg); } }
#splash-screen {
background-color: black;
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
z-index: 10000;
}
#splash-screen:before {
animation-name: splash-spinner;
animation-duration: 0.5s;
animation-iteration-count: infinite;
animation-timing-function: linear;
border-radius: 50%;
border-color: rgba(255,255,255,0.25);
border-top-color: white;
border-style: solid;
border-width: 10px;
content: '';
display: block;
opacity: 0.25;
position: absolute;
left: 50%; top: 50%;
height: 100px; width: 100px;
}
</style>
</head> </head>
<body> <body>
</body> </body>
<script> <script>
const THIRTY_SECONDS = 30 * 1000; const THIRTY_SECONDS = 30 * 1000;
const THIRTY_MINUTES = THIRTY_SECONDS * 60; const ONE_MINUTE = THIRTY_SECONDS * 2;
const FIVE_MINUTES = ONE_MINUTE * 5;
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
const ONE_HOUR = THIRTY_MINUTES * 2;
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
[ [
'example/eventGenerator' 'example/eventGenerator'
@ -73,21 +111,21 @@
{ {
label: 'Last Day', label: 'Last Day',
bounds: { bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 24, start: () => Date.now() - ONE_DAY,
end: () => Date.now() end: () => Date.now()
} }
}, },
{ {
label: 'Last 2 hours', label: 'Last 2 hours',
bounds: { bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 2, start: () => Date.now() - TWO_HOURS,
end: () => Date.now() end: () => Date.now()
} }
}, },
{ {
label: 'Last hour', label: 'Last hour',
bounds: { bounds: {
start: () => Date.now() - 1000 * 60 * 60, start: () => Date.now() - ONE_HOUR,
end: () => Date.now() end: () => Date.now()
} }
} }
@ -96,7 +134,7 @@
records: 10, records: 10,
// maximum duration between start and end bounds // maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds // for utc-based time systems this is in milliseconds
limit: 1000 * 60 * 60 * 24 limit: ONE_DAY
}, },
{ {
name: "Realtime", name: "Realtime",
@ -105,7 +143,44 @@
clockOffsets: { clockOffsets: {
start: - THIRTY_MINUTES, start: - THIRTY_MINUTES,
end: THIRTY_SECONDS end: THIRTY_SECONDS
},
presets: [
{
label: '1 Hour',
bounds: {
start: - ONE_HOUR,
end: THIRTY_SECONDS
} }
},
{
label: '30 Minutes',
bounds: {
start: - THIRTY_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '15 Minutes',
bounds: {
start: - FIFTEEN_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '5 Minutes',
bounds: {
start: - FIVE_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '1 Minute',
bounds: {
start: - ONE_MINUTE,
end: THIRTY_SECONDS
}
}
]
} }
] ]
})); }));

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "1.3.3-SNAPSHOT", "version": "1.4.1-SNAPSHOT",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {

View File

@ -143,8 +143,8 @@ define([
"$window" "$window"
], ],
"group": "windowing", "group": "windowing",
"cssClass": "icon-new-window", "priority": 10,
"priority": "preferred" "cssClass": "icon-new-window"
} }
], ],
"runs": [ "runs": [

View File

@ -139,7 +139,9 @@ define([
], ],
"description": "Edit", "description": "Edit",
"category": "view-control", "category": "view-control",
"cssClass": "major icon-pencil" "cssClass": "major icon-pencil",
"group": "action",
"priority": 10
}, },
{ {
"key": "properties", "key": "properties",
@ -150,6 +152,8 @@ define([
"implementation": PropertiesAction, "implementation": PropertiesAction,
"cssClass": "major icon-pencil", "cssClass": "major icon-pencil",
"name": "Edit Properties...", "name": "Edit Properties...",
"group": "action",
"priority": 10,
"description": "Edit properties of this object.", "description": "Edit properties of this object.",
"depends": [ "depends": [
"dialogService" "dialogService"

View File

@ -20,12 +20,12 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div class="c-object-label" <div class="c-object-label"
ng-class="{ 'is-missing': model.status === 'missing' }" ng-class="{ 'is-status--missing': model.status === 'missing' }"
> >
<div class="c-object-label__type-icon {{type.getCssClass()}}" <div class="c-object-label__type-icon {{type.getCssClass()}}"
ng-class="{ 'l-icon-link':location.isLink() }" ng-class="{ 'l-icon-link':location.isLink() }"
> >
<span class="is-missing__indicator" title="This item is missing"></span> <span class="is-status__indicator" title="This item is missing or suspect"></span>
</div> </div>
<div class='c-object-label__name'>{{model.name}}</div> <div class='c-object-label__name'>{{model.name}}</div>
</div> </div>

View File

@ -66,6 +66,8 @@ define([
"description": "Move object to another location.", "description": "Move object to another location.",
"cssClass": "icon-move", "cssClass": "icon-move",
"category": "contextual", "category": "contextual",
"group": "action",
"priority": 9,
"implementation": MoveAction, "implementation": MoveAction,
"depends": [ "depends": [
"policyService", "policyService",
@ -79,6 +81,8 @@ define([
"description": "Duplicate object to another location.", "description": "Duplicate object to another location.",
"cssClass": "icon-duplicate", "cssClass": "icon-duplicate",
"category": "contextual", "category": "contextual",
"group": "action",
"priority": 8,
"implementation": CopyAction, "implementation": CopyAction,
"depends": [ "depends": [
"$log", "$log",
@ -95,6 +99,8 @@ define([
"description": "Create Link to object in another location.", "description": "Create Link to object in another location.",
"cssClass": "icon-link", "cssClass": "icon-link",
"category": "contextual", "category": "contextual",
"group": "action",
"priority": 7,
"implementation": LinkAction, "implementation": LinkAction,
"depends": [ "depends": [
"policyService", "policyService",

View File

@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div class="c-clock l-time-display" ng-controller="ClockController as clock"> <div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock">
<div class="c-clock__timezone"> <div class="c-clock__timezone">
{{clock.zone()}} {{clock.zone()}}
</div> </div>

View File

@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div class="c-timer is-{{timer.timerState}}" ng-controller="TimerController as timer"> <div class="c-timer u-style-receiver js-style-receiver is-{{timer.timerState}}" ng-controller="TimerController as timer">
<div class="c-timer__controls"> <div class="c-timer__controls">
<button ng-click="timer.clickStopButton()" <button ng-click="timer.clickStopButton()"
ng-hide="timer.timerState == 'stopped'" ng-hide="timer.timerState == 'stopped'"

View File

@ -47,6 +47,8 @@ define([
"implementation": ExportAsJSONAction, "implementation": ExportAsJSONAction,
"category": "contextual", "category": "contextual",
"cssClass": "icon-export", "cssClass": "icon-export",
"group": "json",
"priority": 2,
"depends": [ "depends": [
"openmct", "openmct",
"exportService", "exportService",
@ -61,6 +63,8 @@ define([
"implementation": ImportAsJSONAction, "implementation": ImportAsJSONAction,
"category": "contextual", "category": "contextual",
"cssClass": "icon-import", "cssClass": "icon-import",
"group": "json",
"priority": 2,
"depends": [ "depends": [
"exportService", "exportService",
"identifierService", "identifierService",

View File

@ -242,7 +242,11 @@ define([
this.overlays = new OverlayAPI.default(); this.overlays = new OverlayAPI.default();
this.contextMenu = new api.ContextMenuRegistry(); this.menus = new api.MenuAPI(this);
this.actions = new api.ActionsAPI(this);
this.status = new api.StatusAPI(this);
this.router = new ApplicationRouter(); this.router = new ApplicationRouter();
@ -271,6 +275,7 @@ define([
this.install(this.plugins.URLTimeSettingsSynchronizer()); this.install(this.plugins.URLTimeSettingsSynchronizer());
this.install(this.plugins.NotificationIndicator()); this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction()); this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
} }
MCT.prototype = Object.create(EventEmitter.prototype); MCT.prototype = Object.create(EventEmitter.prototype);

View File

@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
legacyActions.filter(contextualCategoryOnly) legacyActions.filter(contextualCategoryOnly)
.map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction)) .map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction))
.forEach(openmct.contextMenu.registerAction); .forEach(openmct.actions.register);
} }

View File

@ -31,6 +31,8 @@ export default class LegacyContextMenuAction {
this.description = LegacyAction.definition.description; this.description = LegacyAction.definition.description;
this.cssClass = LegacyAction.definition.cssClass; this.cssClass = LegacyAction.definition.cssClass;
this.LegacyAction = LegacyAction; this.LegacyAction = LegacyAction;
this.group = LegacyAction.definition.group;
this.priority = LegacyAction.definition.priority;
} }
invoke(objectPath) { invoke(objectPath) {

View File

@ -0,0 +1,178 @@
/*****************************************************************************
* 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 EventEmitter from 'EventEmitter';
import _ from 'lodash';
class ActionCollection extends EventEmitter {
constructor(applicableActions, objectPath, view, openmct) {
super();
this.applicableActions = applicableActions;
this.openmct = openmct;
this.objectPath = objectPath;
this.view = view;
this.objectUnsubscribes = [];
let debounceOptions = {
leading: false,
trailing: true
};
this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions);
this._update = _.debounce(this._update.bind(this), 150, debounceOptions);
this._observeObjectPath();
this._initializeActions();
this.openmct.editor.on('isEditing', this._updateActions);
}
disable(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isDisabled = true;
}
});
this._update();
}
enable(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isDisabled = false;
}
});
this._update();
}
hide(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isHidden = true;
}
});
this._update();
}
show(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isHidden = false;
}
});
this._update();
}
destroy() {
this.objectUnsubscribes.forEach(unsubscribe => {
unsubscribe();
});
this.openmct.editor.off('isEditing', this._updateActions);
this.emit('destroy', this.view);
}
getVisibleActions() {
let actionsArray = Object.keys(this.applicableActions);
let visibleActions = [];
actionsArray.forEach(actionKey => {
let action = this.applicableActions[actionKey];
if (!action.isHidden) {
visibleActions.push(action);
}
});
return visibleActions;
}
getStatusBarActions() {
let actionsArray = Object.keys(this.applicableActions);
let statusBarActions = [];
actionsArray.forEach(actionKey => {
let action = this.applicableActions[actionKey];
if (action.showInStatusBar && !action.isDisabled && !action.isHidden) {
statusBarActions.push(action);
}
});
return statusBarActions;
}
_update() {
this.emit('update', this.applicableActions);
}
_observeObjectPath() {
let actionCollection = this;
function updateObject(oldObject, newObject) {
Object.assign(oldObject, newObject);
actionCollection._updateActions();
}
this.objectPath.forEach(object => {
if (object) {
let unsubscribe = this.openmct.objects.observe(object, '*', updateObject.bind(this, object));
this.objectUnsubscribes.push(unsubscribe);
}
});
}
_initializeActions() {
Object.keys(this.applicableActions).forEach(key => {
this.applicableActions[key].callBack = () => {
return this.applicableActions[key].invoke(this.objectPath, this.view);
};
});
}
_updateActions() {
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
this._initializeActions();
this._update();
}
_mergeOldAndNewActions(oldActions, newActions) {
let mergedActions = {};
Object.keys(newActions).forEach(key => {
if (oldActions[key]) {
mergedActions[key] = oldActions[key];
} else {
mergedActions[key] = newActions[key];
}
});
return mergedActions;
}
}
export default ActionCollection;

View File

@ -0,0 +1,145 @@
/*****************************************************************************
* 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 EventEmitter from 'EventEmitter';
import ActionCollection from './ActionCollection';
import _ from 'lodash';
class ActionsAPI extends EventEmitter {
constructor(openmct) {
super();
this._allActions = {};
this._actionCollections = new WeakMap();
this._openmct = openmct;
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
this.register = this.register.bind(this);
this.get = this.get.bind(this);
this._applicableActions = this._applicableActions.bind(this);
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
}
register(actionDefinition) {
this._allActions[actionDefinition.key] = actionDefinition;
}
get(objectPath, view) {
let viewContext = view && view.getViewContext && view.getViewContext() || {};
if (view && !viewContext.skipCache) {
let cachedActionCollection = this._actionCollections.get(view);
if (cachedActionCollection) {
return cachedActionCollection;
} else {
let applicableActions = this._applicableActions(objectPath, view);
let actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct);
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
return actionCollection;
}
} else {
let applicableActions = this._applicableActions(objectPath, view);
Object.keys(applicableActions).forEach(key => {
let action = applicableActions[key];
action.callBack = () => {
return action.invoke(objectPath, view);
};
});
return applicableActions;
}
}
updateGroupOrder(groupArray) {
this._groupOrder = groupArray;
}
_updateCachedActionCollections(key) {
if (this._actionCollections.has(key)) {
let actionCollection = this._actionCollections.get(key);
actionCollection.off('destroy', this._updateCachedActionCollections);
this._actionCollections.delete(key);
}
}
_applicableActions(objectPath, view) {
let actionsObject = {};
let keys = Object.keys(this._allActions).filter(key => {
let actionDefinition = this._allActions[key];
if (actionDefinition.appliesTo === undefined) {
return true;
} else {
return actionDefinition.appliesTo(objectPath, view);
}
});
keys.forEach(key => {
let action = _.clone(this._allActions[key]);
actionsObject[key] = action;
});
return actionsObject;
}
_groupAndSortActions(actionsArray) {
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
}
let actionsObject = {};
let groupedSortedActionsArray = [];
function sortDescending(a, b) {
return b.priority - a.priority;
}
actionsArray.forEach(action => {
if (actionsObject[action.group] === undefined) {
actionsObject[action.group] = [action];
} else {
actionsObject[action.group].push(action);
}
});
this._groupOrder.forEach(group => {
let groupArray = actionsObject[group];
if (groupArray) {
groupedSortedActionsArray.push(groupArray.sort(sortDescending));
}
});
return groupedSortedActionsArray;
}
}
export default ActionsAPI;

View File

@ -0,0 +1,119 @@
/*****************************************************************************
* 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 ActionsAPI from './ActionsAPI';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe('The Actions API', () => {
let openmct;
let actionsAPI;
let mockAction;
let mockObjectPath;
let mockViewContext1;
beforeEach(() => {
openmct = createOpenMct();
actionsAPI = new ActionsAPI(openmct);
mockAction = {
name: 'Test Action',
key: 'test-action',
cssClass: 'test-action',
description: 'This is a test action',
group: 'action',
priority: 9,
appliesTo: (objectPath, view = {}) => {
if (view.getViewContext) {
let viewContext = view.getViewContext();
return viewContext.onlyAppliesToTestCase;
} else if (objectPath.length) {
return objectPath[0].type === 'fake-folder';
}
return false;
},
invoke: () => {
}
};
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'fake-folder',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
mockViewContext1 = {
getViewContext: () => {
return {
onlyAppliesToTestCase: true,
skipCache: true
};
}
};
});
afterEach(() => {
resetApplicationState(openmct);
});
describe("register method", () => {
it("adds action to ActionsAPI", () => {
actionsAPI.register(mockAction);
let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
});
});
describe("get method", () => {
beforeEach(() => {
actionsAPI.register(mockAction);
});
it("returns an object with relevant actions when invoked with objectPath only", () => {
let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
});
it("returns an object with relevant actions when invoked with viewContext and skipCache", () => {
let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
});
});
});

View File

@ -28,9 +28,10 @@ define([
'./telemetry/TelemetryAPI', './telemetry/TelemetryAPI',
'./indicators/IndicatorAPI', './indicators/IndicatorAPI',
'./notifications/NotificationAPI', './notifications/NotificationAPI',
'./contextMenu/ContextMenuAPI', './Editor',
'./Editor' './menu/MenuAPI',
'./actions/ActionsAPI',
'./status/StatusAPI'
], function ( ], function (
TimeAPI, TimeAPI,
ObjectAPI, ObjectAPI,
@ -39,8 +40,10 @@ define([
TelemetryAPI, TelemetryAPI,
IndicatorAPI, IndicatorAPI,
NotificationAPI, NotificationAPI,
ContextMenuAPI, EditorAPI,
EditorAPI MenuAPI,
ActionsAPI,
StatusAPI
) { ) {
return { return {
TimeAPI: TimeAPI, TimeAPI: TimeAPI,
@ -51,6 +54,8 @@ define([
IndicatorAPI: IndicatorAPI, IndicatorAPI: IndicatorAPI,
NotificationAPI: NotificationAPI.default, NotificationAPI: NotificationAPI.default,
EditorAPI: EditorAPI, EditorAPI: EditorAPI,
ContextMenuRegistry: ContextMenuAPI.default MenuAPI: MenuAPI.default,
ActionsAPI: ActionsAPI.default,
StatusAPI: StatusAPI.default
}; };
}); });

View File

@ -1,24 +0,0 @@
<template>
<div class="c-menu">
<ul>
<li
v-for="action in actions"
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.invoke(objectPath)"
>
{{ action.name }}
</li>
<li v-if="actions.length === 0">
No actions defined.
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['actions', 'objectPath']
};
</script>

View File

@ -1,159 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ContextMenuComponent from './ContextMenu.vue';
import Vue from 'vue';
/**
* The ContextMenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
* custom HTML elements.
* @interface ContextMenuAPI
* @memberof module:openmct
*/
class ContextMenuAPI {
constructor() {
this._allActions = [];
this._activeContextMenu = undefined;
this._hideActiveContextMenu = this._hideActiveContextMenu.bind(this);
this.registerAction = this.registerAction.bind(this);
}
/**
* Defines an item to be added to context menus. Allows specification of text, appearance, and behavior when
* selected. Applicabilioty can be restricted by specification of an `appliesTo` function.
*
* @interface ContextMenuAction
* @memberof module:openmct
* @property {string} name the human-readable name of this view
* @property {string} description a longer-form description (typically
* a single sentence or short paragraph) of this kind of view
* @property {string} cssClass the CSS class to apply to labels for this
* view (to add icons, for instance)
* @property {string} key unique key to identify the context menu action
* (used in custom context menu eg table rows, to identify which actions to include)
* @property {boolean} hideInDefaultMenu optional flag to hide action from showing in the default context menu (tree item)
*/
/**
* @method appliesTo
* @memberof module:openmct.ContextMenuAction#
* @param {DomainObject[]} objectPath the path of the object that the context menu has been invoked on.
* @returns {boolean} true if the action applies to the objects specified in the 'objectPath', otherwise false.
*/
/**
* Code to be executed when the action is selected from a context menu
* @method invoke
* @memberof module:openmct.ContextMenuAction#
* @param {DomainObject[]} objectPath the path of the object to invoke the action on.
*/
/**
* @param {ContextMenuAction} actionDefinition
*/
registerAction(actionDefinition) {
this._allActions.push(actionDefinition);
}
/**
* @private
*/
_showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) {
let applicableActions = this._allActions.filter((action) => {
if (actionsToBeIncluded) {
if (action.appliesTo === undefined && actionsToBeIncluded.includes(action.key)) {
return true;
}
return action.appliesTo(objectPath, actionsToBeIncluded) && actionsToBeIncluded.includes(action.key);
} else {
if (action.appliesTo === undefined) {
return true;
}
return action.appliesTo(objectPath) && !action.hideInDefaultMenu;
}
});
if (this._activeContextMenu) {
this._hideActiveContextMenu();
}
this._activeContextMenu = this._createContextMenuForObject(objectPath, applicableActions);
this._activeContextMenu.$mount();
document.body.appendChild(this._activeContextMenu.$el);
let position = this._calculatePopupPosition(x, y, this._activeContextMenu.$el);
this._activeContextMenu.$el.style.left = `${position.x}px`;
this._activeContextMenu.$el.style.top = `${position.y}px`;
document.addEventListener('click', this._hideActiveContextMenu);
}
/**
* @private
*/
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: eventPosX,
y: eventPosY
};
}
/**
* @private
*/
_hideActiveContextMenu() {
document.removeEventListener('click', this._hideActiveContextMenu);
document.body.removeChild(this._activeContextMenu.$el);
this._activeContextMenu.$destroy();
this._activeContextMenu = undefined;
}
/**
* @private
*/
_createContextMenuForObject(objectPath, actions) {
return new Vue({
components: {
ContextMenu: ContextMenuComponent
},
provide: {
actions: actions,
objectPath: objectPath
},
template: '<ContextMenu></ContextMenu>'
});
}
}
export default ContextMenuAPI;

67
src/api/menu/MenuAPI.js Normal file
View File

@ -0,0 +1,67 @@
/*****************************************************************************
* 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 Menu from './menu.js';
/**
* The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
* custom HTML elements.
* @interface MenuAPI
* @memberof module:openmct
*/
class MenuAPI {
constructor(openmct) {
this.openmct = openmct;
this.showMenu = this.showMenu.bind(this);
this._clearMenuComponent = this._clearMenuComponent.bind(this);
this._showObjectMenu = this._showObjectMenu.bind(this);
}
showMenu(x, y, actions) {
if (this.menuComponent) {
this.menuComponent.dismiss();
}
let options = {
x,
y,
actions
};
this.menuComponent = new Menu(options);
this.menuComponent.once('destroy', this._clearMenuComponent);
}
_clearMenuComponent() {
this.menuComponent = undefined;
delete this.menuComponent;
}
_showObjectMenu(objectPath, x, y, actionsToBeIncluded) {
let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded);
this.showMenu(x, y, applicableActions);
}
}
export default MenuAPI;

125
src/api/menu/MenuAPISpec.js Normal file
View File

@ -0,0 +1,125 @@
/*****************************************************************************
* 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 MenuAPI from './MenuAPI';
import Menu from './menu';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe ('The Menu API', () => {
let openmct;
let menuAPI;
let actionsArray;
let x;
let y;
let result;
beforeEach(() => {
openmct = createOpenMct();
menuAPI = new MenuAPI(openmct);
actionsArray = [
{
name: 'Test Action 1',
cssClass: 'test-css-class-1',
description: 'This is a test action',
callBack: () => {
result = 'Test Action 1 Invoked';
}
},
{
name: 'Test Action 2',
cssClass: 'test-css-class-2',
description: 'This is a test action',
callBack: () => {
result = 'Test Action 2 Invoked';
}
}
];
x = 8;
y = 16;
});
afterEach(() => {
resetApplicationState(openmct);
});
describe("showMenu method", () => {
it("creates an instance of Menu when invoked", () => {
menuAPI.showMenu(x, y, actionsArray);
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
});
describe("creates a menu component", () => {
let menuComponent;
let vueComponent;
beforeEach(() => {
menuAPI.showMenu(x, y, actionsArray);
vueComponent = menuAPI.menuComponent.component;
menuComponent = document.querySelector(".c-menu");
spyOn(vueComponent, '$destroy');
});
it("renders a menu component in the expected x and y coordinates", () => {
let boundingClientRect = menuComponent.getBoundingClientRect();
let left = boundingClientRect.left;
let top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
});
it("with all the actions passed in", () => {
expect(menuComponent).toBeDefined();
let listItems = menuComponent.children[0].children;
expect(listItems.length).toEqual(actionsArray.length);
});
it("with click-able menu items, that will invoke the correct callBacks", () => {
let listItem1 = menuComponent.children[0].children[0];
listItem1.click();
expect(result).toEqual("Test Action 1 Invoked");
});
it("dismisses the menu when action is clicked on", () => {
let listItem1 = menuComponent.children[0].children[0];
listItem1.click();
let menu = document.querySelector('.c-menu');
expect(menu).toBeNull();
});
it("invokes the destroy method when menu is dismissed", () => {
document.body.click();
expect(vueComponent.$destroy).toHaveBeenCalled();
});
});
});
});

View File

@ -0,0 +1,52 @@
<template>
<div class="c-menu">
<ul v-if="actions.length && actions[0].length">
<template
v-for="(actionGroups, index) in actions"
>
<li
v-for="action in actionGroups"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
@click="action.callBack"
>
{{ action.name }}
</li>
<div
v-if="index !== actions.length - 1"
:key="index"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</template>
</ul>
<ul v-else>
<li
v-for="action in actions"
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.callBack"
>
{{ action.name }}
</li>
<li v-if="actions.length === 0">
No actions defined.
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['actions']
};
</script>

94
src/api/menu/menu.js Normal file
View File

@ -0,0 +1,94 @@
/*****************************************************************************
* 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 EventEmitter from 'EventEmitter';
import MenuComponent from './components/Menu.vue';
import Vue from 'vue';
class Menu extends EventEmitter {
constructor(options) {
super();
this.options = options;
this.component = new Vue({
provide: {
actions: options.actions
},
components: {
MenuComponent
},
template: '<menu-component />'
});
if (options.onDestroy) {
this.once('destroy', options.onDestroy);
}
this.dismiss = this.dismiss.bind(this);
this.show = this.show.bind(this);
this.show();
}
dismiss() {
this.emit('destroy');
document.body.removeChild(this.component.$el);
document.removeEventListener('click', this.dismiss);
this.component.$destroy();
}
show() {
this.component.$mount();
document.body.appendChild(this.component.$el);
let position = this._calculatePopupPosition(this.options.x, this.options.y, this.component.$el);
this.component.$el.style.left = `${position.x}px`;
this.component.$el.style.top = `${position.y}px`;
document.addEventListener('click', this.dismiss);
}
/**
* @private
*/
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: eventPosX,
y: eventPosY
};
}
}
export default Menu;

View File

@ -22,6 +22,7 @@ class OverlayAPI {
this.dismissLastOverlay(); this.dismissLastOverlay();
} }
}); });
} }
/** /**
@ -127,6 +128,7 @@ class OverlayAPI {
return progressDialog; return progressDialog;
} }
} }
export default OverlayAPI; export default OverlayAPI;

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* 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 EventEmitter from 'EventEmitter';
export default class StatusAPI extends EventEmitter {
constructor(openmct) {
super();
this._openmct = openmct;
this._statusCache = {};
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.observe = this.observe.bind(this);
}
get(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
return this._statusCache[keyString];
}
set(identifier, value) {
let keyString = this._openmct.objects.makeKeyString(identifier);
this._statusCache[keyString] = value;
this.emit(keyString, value);
}
delete(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
this._statusCache[keyString] = undefined;
this.emit(keyString, undefined);
delete this._statusCache[keyString];
}
observe(identifier, callback) {
let key = this._openmct.objects.makeKeyString(identifier);
this.on(key, callback);
return () => {
this.off(key, callback);
};
}
}

View File

@ -0,0 +1,85 @@
import StatusAPI from './StatusAPI.js';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe("The Status API", () => {
let statusAPI;
let openmct;
let identifier;
let status;
let status2;
let callback;
beforeEach(() => {
openmct = createOpenMct();
statusAPI = new StatusAPI(openmct);
identifier = {
namespace: "test-namespace",
key: "test-key"
};
status = "test-status";
status2 = 'test-status-deux';
callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate);
});
afterEach(() => {
resetApplicationState(openmct);
});
describe("set function", () => {
it("sets status for identifier", () => {
statusAPI.set(identifier, status);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status);
});
});
describe("get function", () => {
it("returns status for identifier", () => {
statusAPI.set(identifier, status2);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status2);
});
});
describe("delete function", () => {
it("deletes status for identifier", () => {
statusAPI.set(identifier, status);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status);
statusAPI.delete(identifier);
resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toBeUndefined();
});
});
describe("observe function", () => {
it("allows callbacks to be attached to status set and delete events", () => {
let unsubscribe = statusAPI.observe(identifier, callback);
statusAPI.set(identifier, status);
expect(callback).toHaveBeenCalledWith(status);
statusAPI.delete(identifier);
expect(callback).toHaveBeenCalledWith(undefined);
unsubscribe();
});
it("returns a unsubscribe function", () => {
let unsubscribe = statusAPI.observe(identifier, callback);
unsubscribe();
statusAPI.set(identifier, status);
expect(callback).toHaveBeenCalledTimes(0);
});
});
});

View File

@ -176,7 +176,10 @@ export default {
this.timestampKey = timeSystem.key; this.timestampKey = timeSystem.key;
}, },
showContextMenu(event) { showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); let allActions = this.openmct.actions.get(this.currentObjectPath, {}, {viewHistoricalData: true});
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
}, },
resetValues() { resetValues() {
this.value = '---'; this.value = '---';

View File

@ -21,7 +21,7 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div class="c-lad-table-wrapper"> <div class="c-lad-table-wrapper u-style-receiver js-style-receiver">
<table class="c-table c-lad-table"> <table class="c-table c-lad-table">
<thead> <thead>
<tr> <tr>

View File

@ -23,6 +23,7 @@
export default class ClearDataAction { export default class ClearDataAction {
constructor(openmct, appliesToObjects) { constructor(openmct, appliesToObjects) {
this.name = 'Clear Data for Object'; this.name = 'Clear Data for Object';
this.key = 'clear-data-action';
this.description = 'Clears current data for object, unsubscribes and resubscribes to data'; this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
this.cssClass = 'icon-clear-data'; this.cssClass = 'icon-clear-data';

View File

@ -53,7 +53,7 @@ define([
openmct.indicators.add(indicator); openmct.indicators.add(indicator);
} }
openmct.contextMenu.registerAction(new ClearDataAction.default(openmct, appliesToObjects)); openmct.actions.register(new ClearDataAction.default(openmct, appliesToObjects));
}; };
}; };
}); });

View File

@ -26,12 +26,12 @@ import ClearDataAction from '../clearDataAction.js';
describe('When the Clear Data Plugin is installed,', function () { describe('When the Clear Data Plugin is installed,', function () {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']); const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']); const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']); const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const openmct = { const openmct = {
objectViews: mockObjectViews, objectViews: mockObjectViews,
indicators: mockIndicatorProvider, indicators: mockIndicatorProvider,
contextMenu: mockContextMenuProvider, actions: mockActionsProvider,
install: function (plugin) { install: function (plugin) {
plugin(this); plugin(this);
} }
@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () {
it('Clear Data context menu action is installed', function () { it('Clear Data context menu action is installed', function () {
openmct.install(ClearDataActionPlugin([])); openmct.install(ClearDataActionPlugin([]));
expect(mockContextMenuProvider.registerAction).toHaveBeenCalled(); expect(mockActionsProvider.register).toHaveBeenCalled();
}); });
it('clear data action emits a clearData event when invoked', function () { it('clear data action emits a clearData event when invoked', function () {

View File

@ -50,6 +50,7 @@
.c-cs { .c-cs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 auto;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;

View File

@ -21,9 +21,10 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div class="c-style"> <div class="c-style has-local-controls c-toolbar">
<span :class="[ <div class="c-style__controls">
{ 'is-style-invisible': styleItem.style.isStyleInvisible }, <div :class="[
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 } { 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]" ]"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]" :style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
@ -34,8 +35,8 @@
> >
ABC ABC
</span> </span>
</span> </div>
<span class="c-toolbar">
<toolbar-color-picker v-if="hasProperty(styleItem.style.border)" <toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
class="c-style__toolbar-button--border-color u-menu-to--center" class="c-style__toolbar-button--border-color u-menu-to--center"
:options="borderColorOption" :options="borderColorOption"
@ -61,7 +62,14 @@
:options="isStyleInvisibleOption" :options="isStyleInvisibleOption"
@change="updateStyleValue" @change="updateStyleValue"
/> />
</span> </div>
<!-- Save Styles -->
<toolbar-button v-if="canSaveStyle"
class="c-style__toolbar-button--save c-local-controls--show-on-hover c-icon-button c-icon-button--major"
:options="saveOptions"
@click="saveItemStyle()"
/>
</div> </div>
</template> </template>
@ -80,12 +88,11 @@ export default {
ToolbarColorPicker, ToolbarColorPicker,
ToolbarToggleButton ToolbarToggleButton
}, },
inject: [ inject: ['openmct'],
'openmct'
],
props: { props: {
isEditing: { isEditing: {
type: Boolean type: Boolean,
required: true
}, },
mixedStyles: { mixedStyles: {
type: Array, type: Array,
@ -93,6 +100,10 @@ export default {
return []; return [];
} }
}, },
nonSpecificFontProperties: {
type: Array,
required: true
},
styleItem: { styleItem: {
type: Object, type: Object,
required: true required: true
@ -182,7 +193,16 @@ export default {
} }
] ]
}; };
},
saveOptions() {
return {
icon: 'icon-save',
title: 'Save style',
isEditing: this.isEditing
};
},
canSaveStyle() {
return this.isEditing && !this.mixedStyles.length && !this.nonSpecificFontProperties.length;
} }
}, },
methods: { methods: {
@ -216,6 +236,9 @@ export default {
} }
this.$emit('persist', this.styleItem, item.property); this.$emit('persist', this.styleItem, item.property);
},
saveItemStyle() {
this.$emit('save-style', this.itemStyle);
} }
} }
}; };

View File

@ -31,6 +31,11 @@
<div class="c-inspect-styles__header"> <div class="c-inspect-styles__header">
Object Style Object Style
</div> </div>
<FontStyleEditor
v-if="canStyleFont"
:font-style="consolidatedFontStyle"
@set-font-property="setFontProperty"
/>
<div class="c-inspect-styles__content"> <div class="c-inspect-styles__content">
<div v-if="staticStyle" <div v-if="staticStyle"
class="c-inspect-styles__style" class="c-inspect-styles__style"
@ -39,7 +44,9 @@
:style-item="staticStyle" :style-item="staticStyle"
:is-editing="allowEditing" :is-editing="allowEditing"
:mixed-styles="mixedStyles" :mixed-styles="mixedStyles"
:non-specific-font-properties="nonSpecificFontProperties"
@persist="updateStaticStyle" @persist="updateStaticStyle"
@save-style="saveStyle"
/> />
</div> </div>
<button <button
@ -58,10 +65,11 @@
</div> </div>
<div class="c-inspect-styles__content c-inspect-styles__condition-set"> <div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject" <a v-if="conditionSetDomainObject"
class="c-object-label icon-conditional" class="c-object-label"
:href="navigateToPath" :href="navigateToPath"
@click="navigateOrPreview" @click="navigateOrPreview"
> >
<span class="c-object-label__type-icon icon-conditional"></span>
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span> <span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
</a> </a>
<template v-if="allowEditing"> <template v-if="allowEditing">
@ -80,6 +88,12 @@
</template> </template>
</div> </div>
<FontStyleEditor
v-if="canStyleFont"
:font-style="consolidatedFontStyle"
@set-font-property="setFontProperty"
/>
<div v-if="conditionsLoaded" <div v-if="conditionsLoaded"
class="c-inspect-styles__conditions" class="c-inspect-styles__conditions"
> >
@ -97,8 +111,10 @@
/> />
<style-editor class="c-inspect-styles__editor" <style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle" :style-item="conditionStyle"
:non-specific-font-properties="nonSpecificFontProperties"
:is-editing="allowEditing" :is-editing="allowEditing"
@persist="updateConditionalStyle" @persist="updateConditionalStyle"
@save-style="saveStyle"
/> />
</div> </div>
</div> </div>
@ -108,6 +124,7 @@
<script> <script>
import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue';
import StyleEditor from "./StyleEditor.vue"; import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js"; import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils"; import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";
@ -116,16 +133,30 @@ import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue"; import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
import Vue from 'vue'; import Vue from 'vue';
const NON_SPECIFIC = '??';
const NON_STYLEABLE_CONTAINER_TYPES = [
'layout',
'flexible-layout',
'tabs'
];
const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [
'line-view',
'box-view',
'image-view'
];
export default { export default {
name: 'StylesView', name: 'StylesView',
components: { components: {
FontStyleEditor,
StyleEditor, StyleEditor,
ConditionError, ConditionError,
ConditionDescription ConditionDescription
}, },
inject: [ inject: [
'openmct', 'openmct',
'selection' 'selection',
'stylesManager'
], ],
data() { data() {
return { return {
@ -139,19 +170,80 @@ export default {
conditionsLoaded: false, conditionsLoaded: false,
navigateToPath: '', navigateToPath: '',
selectedConditionId: '', selectedConditionId: '',
locked: false items: [],
domainObject: undefined,
consolidatedFontStyle: {}
}; };
}, },
computed: { computed: {
locked() {
return this.selection.some(selectionPath => {
const self = selectionPath[0].context.item;
const parent = selectionPath.length > 1 ? selectionPath[1].context.item : undefined;
return (self && self.locked) || (parent && parent.locked);
});
},
allowEditing() { allowEditing() {
return this.isEditing && !this.locked; return this.isEditing && !this.locked;
},
styleableFontItems() {
return this.selection.filter(selectionPath => {
const item = selectionPath[0].context.item;
const itemType = item && item.type;
const layoutItem = selectionPath[0].context.layoutItem;
const layoutItemType = layoutItem && layoutItem.type;
if (itemType && NON_STYLEABLE_CONTAINER_TYPES.includes(itemType)) {
return false;
}
if (layoutItemType && NON_STYLEABLE_LAYOUT_ITEM_TYPES.includes(layoutItemType)) {
return false;
}
return true;
});
},
computedconsolidatedFontStyle() {
let consolidatedFontStyle;
const styles = [];
this.styleableFontItems.forEach(styleable => {
const fontStyle = this.getFontStyle(styleable[0]);
styles.push(fontStyle);
});
if (styles.length) {
const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize);
const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font);
consolidatedFontStyle = {
fontSize: hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC,
font: hasConsolidatedFont ? styles[0].font : NON_SPECIFIC
};
}
return consolidatedFontStyle;
},
nonSpecificFontProperties() {
if (!this.consolidatedFontStyle) {
return [];
}
return Object.keys(this.consolidatedFontStyle).filter(property => this.consolidatedFontStyle[property] === NON_SPECIFIC);
},
canStyleFont() {
return this.styleableFontItems.length && this.allowEditing;
} }
}, },
destroyed() { destroyed() {
this.removeListeners(); this.removeListeners();
this.openmct.editor.off('isEditing', this.setEditState);
this.stylesManager.off('styleSelected', this.applyStyleToSelection);
}, },
mounted() { mounted() {
this.items = [];
this.previewAction = new PreviewAction(this.openmct); this.previewAction = new PreviewAction(this.openmct);
this.isMultipleSelection = this.selection.length > 1; this.isMultipleSelection = this.selection.length > 1;
this.getObjectsAndItemsFromSelection(); this.getObjectsAndItemsFromSelection();
@ -166,7 +258,10 @@ export default {
this.initializeStaticStyle(); this.initializeStaticStyle();
} }
this.setConsolidatedFontStyle();
this.openmct.editor.on('isEditing', this.setEditState); this.openmct.editor.on('isEditing', this.setEditState);
this.stylesManager.on('styleSelected', this.applyStyleToSelection);
}, },
methods: { methods: {
getObjectStyles() { getObjectStyles() {
@ -178,10 +273,10 @@ export default {
} }
} else if (this.items.length) { } else if (this.items.length) {
const itemId = this.items[0].id; const itemId = this.items[0].id;
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) { if (this.domainObject && this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) {
objectStyles = this.domainObject.configuration.objectStyles[itemId]; objectStyles = this.domainObject.configuration.objectStyles[itemId];
} }
} else if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) { } else if (this.domainObject && this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
objectStyles = this.domainObject.configuration.objectStyles; objectStyles = this.domainObject.configuration.objectStyles;
} }
@ -219,6 +314,18 @@ export default {
isItemType(type, item) { isItemType(type, item) {
return item && (item.type === type); return item && (item.type === type);
}, },
canPersistObject(item) {
// for now the only way to tell if an object can be persisted is if it is creatable.
let creatable = false;
if (item) {
const type = this.openmct.types.get(item.type);
if (type && type.definition) {
creatable = (type.definition.creatable === true);
}
}
return creatable;
},
hasConditionalStyle(domainObject, layoutItem) { hasConditionalStyle(domainObject, layoutItem) {
const id = layoutItem ? layoutItem.id : undefined; const id = layoutItem ? layoutItem.id : undefined;
@ -235,13 +342,8 @@ export default {
this.selection.forEach((selectionItem) => { this.selection.forEach((selectionItem) => {
const item = selectionItem[0].context.item; const item = selectionItem[0].context.item;
const layoutItem = selectionItem[0].context.layoutItem; const layoutItem = selectionItem[0].context.layoutItem;
const layoutDomainObject = selectionItem[0].context.item;
const isChildItem = selectionItem.length > 1; const isChildItem = selectionItem.length > 1;
if (layoutDomainObject && layoutDomainObject.locked) {
this.locked = true;
}
if (!isChildItem) { if (!isChildItem) {
domainObject = item; domainObject = item;
itemStyle = getApplicableStylesForItem(item); itemStyle = getApplicableStylesForItem(item);
@ -251,7 +353,7 @@ export default {
} else { } else {
this.canHide = true; this.canHide = true;
domainObject = selectionItem[1].context.item; domainObject = selectionItem[1].context.item;
if (item && !layoutItem || this.isItemType('subobject-view', layoutItem)) { if (item && !layoutItem || (this.isItemType('subobject-view', layoutItem) && this.canPersistObject(item))) {
subObjects.push(item); subObjects.push(item);
itemStyle = getApplicableStylesForItem(item); itemStyle = getApplicableStylesForItem(item);
if (this.hasConditionalStyle(item)) { if (this.hasConditionalStyle(item)) {
@ -275,7 +377,7 @@ export default {
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles); const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
this.initialStyles = styles; this.initialStyles = styles;
this.mixedStyles = mixedStyles; this.mixedStyles = mixedStyles;
// main layout
this.domainObject = domainObject; this.domainObject = domainObject;
this.removeListeners(); this.removeListeners();
if (this.domainObject) { if (this.domainObject) {
@ -298,6 +400,7 @@ export default {
isKeyItemId(key) { isKeyItemId(key) {
return (key !== 'styles') return (key !== 'styles')
&& (key !== 'staticStyle') && (key !== 'staticStyle')
&& (key !== 'fontStyle')
&& (key !== 'defaultConditionId') && (key !== 'defaultConditionId')
&& (key !== 'selectedConditionId') && (key !== 'selectedConditionId')
&& (key !== 'conditionSetIdentifier'); && (key !== 'conditionSetIdentifier');
@ -637,6 +740,124 @@ export default {
}, },
persist(domainObject, style) { persist(domainObject, style) {
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style); this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
},
applyStyleToSelection(style) {
if (!this.allowEditing) {
return;
}
this.updateSelectionFontStyle(style);
this.updateSelectionStyle(style);
},
updateSelectionFontStyle(style) {
const fontSizeProperty = {
fontSize: style.fontSize
};
const fontProperty = {
font: style.font
};
this.setFontProperty(fontSizeProperty);
this.setFontProperty(fontProperty);
},
updateSelectionStyle(style) {
const foundStyle = this.findStyleByConditionId(this.selectedConditionId);
if (foundStyle && !this.isStaticAndConditionalStyles) {
Object.entries(style).forEach(([property, value]) => {
if (foundStyle.style[property] !== undefined && foundStyle.style[property] !== value) {
foundStyle.style[property] = value;
}
});
this.getAndPersistStyles();
} else {
this.removeConditionSet();
Object.entries(style).forEach(([property, value]) => {
if (this.staticStyle.style[property] !== undefined && this.staticStyle.style[property] !== value) {
this.staticStyle.style[property] = value;
this.getAndPersistStyles(property);
}
});
}
},
saveStyle(style) {
const styleToSave = {
...style,
...this.consolidatedFontStyle
};
this.stylesManager.save(styleToSave);
},
setConsolidatedFontStyle() {
const styles = [];
this.styleableFontItems.forEach(styleable => {
const fontStyle = this.getFontStyle(styleable[0]);
styles.push(fontStyle);
});
if (styles.length) {
const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize);
const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font);
const fontSize = hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC;
const font = hasConsolidatedFont ? styles[0].font : NON_SPECIFIC;
this.$set(this.consolidatedFontStyle, 'fontSize', fontSize);
this.$set(this.consolidatedFontStyle, 'font', font);
}
},
getFontStyle(selectionPath) {
const item = selectionPath.context.item;
const layoutItem = selectionPath.context.layoutItem;
let fontStyle = item && item.configuration && item.configuration.fontStyle;
// support for legacy where font styling in layouts only
if (!fontStyle) {
fontStyle = {
fontSize: layoutItem && layoutItem.fontSize || 'default',
font: layoutItem && layoutItem.font || 'default'
};
}
return fontStyle;
},
setFontProperty(fontStyleObject) {
let layoutDomainObject;
const [property, value] = Object.entries(fontStyleObject)[0];
this.styleableFontItems.forEach(styleable => {
if (!this.isLayoutObject(styleable)) {
const fontStyle = this.getFontStyle(styleable[0]);
fontStyle[property] = value;
this.openmct.objects.mutate(styleable[0].context.item, 'configuration.fontStyle', fontStyle);
} else {
// all layoutItems in this context will share same parent layout
if (!layoutDomainObject) {
layoutDomainObject = styleable[1].context.item;
}
// save layout item font style to parent layout configuration
const layoutItemIndex = styleable[0].context.index;
const layoutItemConfiguration = layoutDomainObject.configuration.items[layoutItemIndex];
layoutItemConfiguration[property] = value;
}
});
if (layoutDomainObject) {
this.openmct.objects.mutate(layoutDomainObject, 'configuration.items', layoutDomainObject.configuration.items);
}
// sync vue component on font update
this.$set(this.consolidatedFontStyle, property, value);
},
isLayoutObject(selectionPath) {
const layoutItemType = selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type;
return layoutItemType && layoutItemType !== 'subobject-view';
} }
} }
}; };

View File

@ -40,9 +40,11 @@
} }
&__condition-set { &__condition-set {
align-items: baseline;
border-bottom: 1px solid $colorInteriorBorder;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; padding-bottom: $interiorMargin;
.c-object-label { .c-object-label {
flex: 1 1 auto; flex: 1 1 auto;
@ -53,7 +55,10 @@
} }
} }
&__style, &__style {
padding-bottom: $interiorMargin;
}
&__condition { &__condition {
padding: $interiorMargin; padding: $interiorMargin;
} }

View File

@ -146,6 +146,8 @@ describe('the plugin', function () {
let displayLayoutItem; let displayLayoutItem;
let lineLayoutItem; let lineLayoutItem;
let boxLayoutItem; let boxLayoutItem;
let notCreatableObjectItem;
let notCreatableObject;
let selection; let selection;
let component; let component;
let styleViewComponentObject; let styleViewComponentObject;
@ -264,6 +266,19 @@ describe('the plugin', function () {
"stroke": "#717171", "stroke": "#717171",
"type": "line-view", "type": "line-view",
"id": "57d49a28-7863-43bd-9593-6570758916f0" "id": "57d49a28-7863-43bd-9593-6570758916f0"
},
{
"width": 32,
"height": 18,
"x": 36,
"y": 8,
"identifier": {
"key": "~TEST~image",
"namespace": "test-space"
},
"hasFrame": true,
"type": "subobject-view",
"id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85"
} }
], ],
"layoutGrid": [ "layoutGrid": [
@ -297,6 +312,52 @@ describe('the plugin', function () {
"type": "box-view", "type": "box-view",
"id": "89b88746-d325-487b-aec4-11b79afff9e8" "id": "89b88746-d325-487b-aec4-11b79afff9e8"
}; };
notCreatableObjectItem = {
"width": 32,
"height": 18,
"x": 36,
"y": 8,
"identifier": {
"key": "~TEST~image",
"namespace": "test-space"
},
"hasFrame": true,
"type": "subobject-view",
"id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85"
};
notCreatableObject = {
"identifier": {
"key": "~TEST~image",
"namespace": "test-space"
},
"name": "test~image",
"location": "test-space:~TEST",
"type": "test.image",
"telemetry": {
"values": [
{
"key": "value",
"name": "Value",
"hints": {
"image": 1,
"priority": 0
},
"format": "image",
"source": "value"
},
{
"key": "utc",
"source": "timestamp",
"name": "Timestamp",
"format": "iso",
"hints": {
"domain": 1,
"priority": 1
}
}
]
}
};
selection = [ selection = [
[{ [{
context: { context: {
@ -316,6 +377,19 @@ describe('the plugin', function () {
"index": 0 "index": 0
} }
}, },
{
context: {
item: displayLayoutItem,
"supportsMultiSelect": true
}
}],
[{
context: {
"item": notCreatableObject,
"layoutItem": notCreatableObjectItem,
"index": 2
}
},
{ {
context: { context: {
item: displayLayoutItem, item: displayLayoutItem,
@ -344,7 +418,7 @@ describe('the plugin', function () {
}); });
it('initializes the items in the view', () => { it('initializes the items in the view', () => {
expect(styleViewComponentObject.items.length).toBe(2); expect(styleViewComponentObject.items.length).toBe(3);
}); });
it('initializes conditional styles', () => { it('initializes conditional styles', () => {
@ -363,7 +437,7 @@ describe('the plugin', function () {
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
[boxLayoutItem, lineLayoutItem].forEach((item) => { [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles; const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles;
expect(itemStyles.length).toBe(2); expect(itemStyles.length).toBe(2);
const foundStyle = itemStyles.find((style) => { const foundStyle = itemStyles.find((style) => {
@ -385,7 +459,7 @@ describe('the plugin', function () {
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
[boxLayoutItem, lineLayoutItem].forEach((item) => { [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle; const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle;
expect(itemStyle).toBeDefined(); expect(itemStyle).toBeDefined();
const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item); const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item);

View File

@ -22,7 +22,7 @@
<template> <template>
<component :is="urlDefined ? 'a' : 'span'" <component :is="urlDefined ? 'a' : 'span'"
class="c-condition-widget" class="c-condition-widget u-style-receiver js-style-receiver"
:href="urlDefined ? internalDomainObject.url : null" :href="urlDefined ? internalDomainObject.url : null"
> >
<div class="c-condition-widget__label"> <div class="c-condition-widget__label">

View File

@ -64,9 +64,16 @@ define([
components: { components: {
AlphanumericFormatView: AlphanumericFormatView.default AlphanumericFormatView: AlphanumericFormatView.default
}, },
template: '<alphanumeric-format-view></alphanumeric-format-view>' template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
}); });
}, },
getViewContext() {
if (component) {
return component.$refs.alphanumericFormatView.getViewContext();
} else {
return {};
}
},
destroy: function () { destroy: function () {
component.$destroy(); component.$destroy();
component = undefined; component = undefined;

View File

@ -73,7 +73,6 @@ define(['lodash'], function (_) {
] ]
} }
}; };
const VIEW_TYPES = { const VIEW_TYPES = {
'telemetry-view': { 'telemetry-view': {
value: 'telemetry-view', value: 'telemetry-view',
@ -96,7 +95,6 @@ define(['lodash'], function (_) {
class: 'icon-tabular-realtime' class: 'icon-tabular-realtime'
} }
}; };
const APPLICABLE_VIEWS = { const APPLICABLE_VIEWS = {
'telemetry-view': [ 'telemetry-view': [
VIEW_TYPES['telemetry.plot.overlay'], VIEW_TYPES['telemetry.plot.overlay'],
@ -390,29 +388,6 @@ define(['lodash'], function (_) {
} }
} }
function getTextSizeMenu(selectedParent, selection) {
const TEXT_SIZE = [8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 24, 30, 36, 48, 72, 96, 128];
return {
control: "select-menu",
domainObject: selectedParent,
applicableSelectedItems: selection.filter(selectionPath => {
let type = selectionPath[0].context.layoutItem.type;
return type === 'text-view' || type === 'telemetry-view';
}),
property: function (selectionPath) {
return getPath(selectionPath) + ".size";
},
title: "Set text size",
options: TEXT_SIZE.map(size => {
return {
value: size + "px"
};
})
};
}
function getTextButton(selectedParent, selection) { function getTextButton(selectedParent, selection) {
return { return {
control: "button", control: "button",
@ -423,7 +398,7 @@ define(['lodash'], function (_) {
property: function (selectionPath) { property: function (selectionPath) {
return getPath(selectionPath); return getPath(selectionPath);
}, },
icon: "icon-font", icon: "icon-pencil",
title: "Edit text properties", title: "Edit text properties",
dialog: DIALOG_FORM.text dialog: DIALOG_FORM.text
}; };
@ -678,7 +653,6 @@ define(['lodash'], function (_) {
'display-mode': [], 'display-mode': [],
'telemetry-value': [], 'telemetry-value': [],
'style': [], 'style': [],
'text-style': [],
'position': [], 'position': [],
'duplicate': [], 'duplicate': [],
'unit-toggle': [], 'unit-toggle': [],
@ -729,12 +703,6 @@ define(['lodash'], function (_) {
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)]; toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
} }
if (toolbar['text-style'].length === 0) {
toolbar['text-style'] = [
getTextSizeMenu(selectedParent, selectedObjects)
];
}
if (toolbar.position.length === 0) { if (toolbar.position.length === 0) {
toolbar.position = [ toolbar.position = [
getStackOrder(selectedParent, selectionPath), getStackOrder(selectedParent, selectionPath),
@ -760,12 +728,6 @@ define(['lodash'], function (_) {
} }
} }
} else if (layoutItem.type === 'text-view') { } else if (layoutItem.type === 'text-view') {
if (toolbar['text-style'].length === 0) {
toolbar['text-style'] = [
getTextSizeMenu(selectedParent, selectedObjects)
];
}
if (toolbar.position.length === 0) { if (toolbar.position.length === 0) {
toolbar.position = [ toolbar.position = [
getStackOrder(selectedParent, selectionPath), getStackOrder(selectedParent, selectionPath),

View File

@ -56,6 +56,28 @@ define(function () {
1 1
], ],
required: true required: true
},
{
name: "Horizontal size (px)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
property: [
"configuration",
"layoutDimensions",
0
],
required: false
},
{
name: "Vertical size (px)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
property: [
"configuration",
"layoutDimensions",
1
],
required: false
} }
] ]
}; };

View File

@ -0,0 +1,33 @@
import clipboard from '@/utils/clipboard';
export default class CopyToClipboardAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-duplicate';
this.description = 'Copy to Clipboard action';
this.group = "action";
this.key = 'copyToClipboard';
this.name = 'Copy to Clipboard';
this.priority = 9;
}
invoke(objectPath, viewContext) {
const formattedValue = viewContext.formattedValueForCopy();
clipboard.updateClipboard(formattedValue)
.then(() => {
this.openmct.notifications.info(`Success : copied to clipboard '${formattedValue}'`);
})
.catch(() => {
this.openmct.notifications.error(`Failed : to copy to clipboard '${formattedValue}'`);
});
}
appliesTo(objectPath, viewContext) {
if (viewContext && viewContext.getViewKey) {
return viewContext.getViewKey().includes('alphanumeric-format');
}
return false;
}
}

View File

@ -29,7 +29,7 @@
@endMove="() => $emit('endMove')" @endMove="() => $emit('endMove')"
> >
<div <div
class="c-box-view" class="c-box-view u-style-receiver js-style-receiver"
:class="[styleClass]" :class="[styleClass]"
:style="style" :style="style"
></div> ></div>

View File

@ -22,7 +22,7 @@
<template> <template>
<div <div
class="l-layout" class="l-layout u-style-receiver js-style-receiver"
:class="{ :class="{
'is-multi-selected': selectedLayoutItems.length > 1, 'is-multi-selected': selectedLayoutItems.length > 1,
'allow-editing': isEditing 'allow-editing': isEditing
@ -36,7 +36,15 @@
:grid-size="gridSize" :grid-size="gridSize"
:show-grid="showGrid" :show-grid="showGrid"
/> />
<div
v-if="shouldDisplayLayoutDimensions"
class="l-layout__dimensions"
:style="layoutDimensionsStyle"
>
<div class="l-layout__dimensions-vals">
{{ layoutDimensions[0] }},{{ layoutDimensions[1] }}
</div>
</div>
<component <component
:is="item.type" :is="item.type"
v-for="(item, index) in layoutItems" v-for="(item, index) in layoutItems"
@ -165,6 +173,23 @@ export default {
return this.itemIsInCurrentSelection(item); return this.itemIsInCurrentSelection(item);
}); });
}, },
layoutDimensions() {
return this.internalDomainObject.configuration.layoutDimensions;
},
shouldDisplayLayoutDimensions() {
return this.layoutDimensions
&& this.layoutDimensions[0] > 0
&& this.layoutDimensions[1] > 0;
},
layoutDimensionsStyle() {
const width = `${this.layoutDimensions[0]}px`;
const height = `${this.layoutDimensions[1]}px`;
return {
width,
height
};
},
showMarquee() { showMarquee() {
let selectionPath = this.selection[0]; let selectionPath = this.selection[0];
let singleSelectedLine = this.selection.length === 1 let singleSelectedLine = this.selection.length === 1

View File

@ -81,6 +81,7 @@ export default {
style() { style() {
let backgroundImage = 'url(' + this.item.url + ')'; let backgroundImage = 'url(' + this.item.url + ')';
let border = '1px solid ' + this.item.stroke; let border = '1px solid ' + this.item.stroke;
if (this.itemStyle) { if (this.itemStyle) {
if (this.itemStyle.imageUrl !== undefined) { if (this.itemStyle.imageUrl !== undefined) {
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')'; backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';

View File

@ -35,6 +35,8 @@
:object-path="currentObjectPath" :object-path="currentObjectPath"
:has-frame="item.hasFrame" :has-frame="item.hasFrame"
:show-edit-view="false" :show-edit-view="false"
:layout-font-size="item.fontSize"
:layout-font="item.font"
/> />
</layout-frame> </layout-frame>
</template> </template>
@ -73,6 +75,8 @@ export default {
y: position[1], y: position[1],
identifier: domainObject.identifier, identifier: domainObject.identifier,
hasFrame: hasFrameByDefault(domainObject.type), hasFrame: hasFrameByDefault(domainObject.type),
fontSize: 'default',
font: 'default',
viewKey viewKey
}; };
}, },
@ -138,6 +142,9 @@ export default {
this.domainObject = domainObject; this.domainObject = domainObject;
this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice()); this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
this.$nextTick(() => { this.$nextTick(() => {
let reference = this.$refs.objectFrame;
if (reference) {
let childContext = this.$refs.objectFrame.getSelectionContext(); let childContext = this.$refs.objectFrame.getSelectionContext();
childContext.item = domainObject; childContext.item = domainObject;
childContext.layoutItem = this.item; childContext.layoutItem = this.item;
@ -146,6 +153,7 @@ export default {
this.removeSelectable = this.openmct.selection.selectable( this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.immediatelySelect || this.initSelect); this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect; delete this.immediatelySelect;
}
}); });
} }
} }

View File

@ -31,15 +31,14 @@
<div <div
v-if="domainObject" v-if="domainObject"
class="c-telemetry-view" class="c-telemetry-view"
:class="{ :class="[statusClass]"
styleClass,
'is-missing': domainObject.status === 'missing'
}"
:style="styleObject" :style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
@contextmenu.prevent="showContextMenu" @contextmenu.prevent="showContextMenu"
> >
<div class="is-missing__indicator" <div class="is-status__indicator"
title="This item is missing" title="This item is missing or suspect"
></div> ></div>
<div <div
v-if="showLabel" v-if="showLabel"
@ -74,10 +73,11 @@
import LayoutFrame from './LayoutFrame.vue'; import LayoutFrame from './LayoutFrame.vue';
import printj from 'printj'; import printj from 'printj';
import conditionalStylesMixin from "../mixins/objectStyles-mixin"; import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5]; const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1]; const DEFAULT_POSITION = [1, 1];
const CONTEXT_MENU_ACTIONS = ['viewHistoricalData']; const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
export default { export default {
makeDefinition(openmct, gridSize, domainObject, position) { makeDefinition(openmct, gridSize, domainObject, position) {
@ -95,7 +95,8 @@ export default {
stroke: "", stroke: "",
fill: "", fill: "",
color: "", color: "",
size: "13px" fontSize: 'default',
font: 'default'
}; };
}, },
inject: ['openmct', 'objectPath'], inject: ['openmct', 'objectPath'],
@ -126,13 +127,18 @@ export default {
}, },
data() { data() {
return { return {
currentObjectPath: undefined,
datum: undefined, datum: undefined,
formats: undefined,
domainObject: undefined, domainObject: undefined,
currentObjectPath: undefined formats: undefined,
viewKey: `alphanumeric-format-${Math.random()}`,
status: ''
}; };
}, },
computed: { computed: {
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
},
showLabel() { showLabel() {
let displayMode = this.item.displayMode; let displayMode = this.item.displayMode;
@ -150,10 +156,15 @@ export default {
return unit; return unit;
}, },
styleObject() { styleObject() {
return Object.assign({}, { let size;
fontSize: this.item.size //for legacy size support
}, this.itemStyle); if (!this.item.fontSize) {
size = this.item.size;
}
return Object.assign({}, {
size
}, this.itemStyle);
}, },
fieldName() { fieldName() {
return this.valueMetadata && this.valueMetadata.name; return this.valueMetadata && this.valueMetadata.name;
@ -205,9 +216,13 @@ export default {
this.openmct.objects.get(this.item.identifier) this.openmct.objects.get(this.item.identifier)
.then(this.setObject); .then(this.setObject);
this.openmct.time.on("bounds", this.refreshData); this.openmct.time.on("bounds", this.refreshData);
this.status = this.openmct.status.get(this.item.identifier);
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
}, },
destroyed() { destroyed() {
this.removeSubscription(); this.removeSubscription();
this.removeStatusListener();
if (this.removeSelectable) { if (this.removeSelectable) {
this.removeSelectable(); this.removeSelectable();
@ -216,6 +231,18 @@ export default {
this.openmct.time.off("bounds", this.refreshData); this.openmct.time.off("bounds", this.refreshData);
}, },
methods: { methods: {
getViewContext() {
return {
getViewKey: () => this.viewKey,
formattedValueForCopy: this.formattedValueForCopy
};
},
formattedValueForCopy() {
const timeFormatterKey = this.openmct.time.timeSystem().key;
const timeFormatter = this.formats[timeFormatterKey];
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue} ${this.unit}`;
},
requestHistoricalData() { requestHistoricalData() {
let bounds = this.openmct.time.bounds(); let bounds = this.openmct.time.bounds();
let options = { let options = {
@ -253,6 +280,16 @@ export default {
this.requestHistoricalData(this.domainObject); this.requestHistoricalData(this.domainObject);
} }
}, },
getView() {
return {
getViewContext() {
return {
viewHistoricalData: true,
skipCache: true
};
}
};
},
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
@ -276,12 +313,40 @@ export default {
this.removeSelectable = this.openmct.selection.selectable( this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.immediatelySelect || this.initSelect); this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect; delete this.immediatelySelect;
let allActions = this.openmct.actions.get(this.currentObjectPath, this.getView());
this.applicableActions = CONTEXT_MENU_ACTIONS.map(actionKey => {
return allActions[actionKey];
});
}, },
updateTelemetryFormat(format) { updateTelemetryFormat(format) {
this.$emit('formatChanged', this.item, format); this.$emit('formatChanged', this.item, format);
}, },
showContextMenu(event) { async getContextMenuActions() {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); const defaultNotebook = getDefaultNotebook();
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
const actionsObject = this.openmct.actions.get(this.currentObjectPath, this.getViewContext(), { viewHistoricalData: true }).applicableActions;
let applicableActionKeys = Object.keys(actionsObject)
.filter(key => {
const isCopyToNotebook = actionsObject[key].key === 'copyToNotebook';
if (defaultNotebook && isCopyToNotebook) {
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
actionsObject[key].name = `Copy to Notebook ${defaultPath}`;
}
return CONTEXT_MENU_ACTIONS.includes(actionsObject[key].key);
});
return applicableActionKeys.map(key => actionsObject[key]);
},
async showContextMenu(event) {
const contextMenuActions = await this.getContextMenuActions();
this.openmct.menus.showMenu(event.x, event.y, contextMenuActions);
},
setStatus(status) {
this.status = status;
} }
} }
}; };

View File

@ -29,7 +29,9 @@
@endMove="() => $emit('endMove')" @endMove="() => $emit('endMove')"
> >
<div <div
class="c-text-view" class="c-text-view u-style-receiver js-style-receiver"
:data-font-size="item.fontSize"
:data-font="item.font"
:class="[styleClass]" :class="[styleClass]"
:style="style" :style="style"
> >
@ -47,13 +49,14 @@ export default {
return { return {
fill: '', fill: '',
stroke: '', stroke: '',
size: '13px',
color: '', color: '',
x: 1, x: 1,
y: 1, y: 1,
width: 10, width: 10,
height: 5, height: 5,
text: element.text text: element.text,
fontSize: 'default',
font: 'default'
}; };
}, },
inject: ['openmct'], inject: ['openmct'],
@ -84,8 +87,14 @@ export default {
}, },
computed: { computed: {
style() { style() {
let size;
//legacy size support
if (!this.item.fontSize) {
size = this.item.size;
}
return Object.assign({ return Object.assign({
fontSize: this.item.size size
}, this.itemStyle); }, this.itemStyle);
} }
}, },

View File

@ -17,10 +17,29 @@
flex-direction: column; flex-direction: column;
overflow: auto; overflow: auto;
&__grid-holder { &__grid-holder,
&__dimensions {
display: none; display: none;
} }
&__dimensions {
$b: 1px dashed $editDimensionsColor;
border-right: $b;
border-bottom: $b;
pointer-events: none;
position: absolute;
&-vals {
$p: 2px;
color: $editDimensionsColor;
display: inline-block;
font-style: italic;
position: absolute;
bottom: $p; right: $p;
opacity: 0.7;
}
}
&__frame { &__frame {
position: absolute; position: absolute;
} }
@ -34,6 +53,10 @@
> .l-layout { > .l-layout {
background: $editUIGridColorBg; background: $editUIGridColorBg;
> [class*="__dimensions"] {
display: block;
}
> [class*="__grid-holder"] { > [class*="__grid-holder"] {
display: block; display: block;
} }
@ -42,12 +65,16 @@
} }
.l-layout__frame { .l-layout__frame {
&[s-selected], &[s-selected]:not([multi-select="true"]),
&[s-selected-parent] { &[s-selected-parent] {
// Display grid and allow edit marquee to display in nested layouts when editing // Display grid and allow edit marquee to display in nested layouts when editing
> * > * > .l-layout + .allow-editing { > * > * > .l-layout.allow-editing {
box-shadow: inset $editUIGridColorFg 0 0 2px 1px; box-shadow: inset $editUIGridColorFg 0 0 2px 1px;
> [class*="__dimensions"] {
display: block;
}
> [class*='grid-holder'] { > [class*='grid-holder'] {
display: block; display: block;
} }

View File

@ -29,12 +29,12 @@
@include isMissing($absPos: true); @include isMissing($absPos: true);
.is-missing__indicator { .is-status__indicator {
top: 0; top: 0;
left: 0; left: 0;
} }
&.is-missing { &.is-status--missing {
border: $borderMissing; border: $borderMissing;
} }
} }

View File

@ -27,6 +27,7 @@ export default {
inject: ['openmct'], inject: ['openmct'],
data() { data() {
return { return {
objectStyle: undefined,
itemStyle: undefined, itemStyle: undefined,
styleClass: '' styleClass: ''
}; };

View File

@ -26,9 +26,12 @@ import objectUtils from 'objectUtils';
import DisplayLayoutType from './DisplayLayoutType.js'; import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js'; import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js'; import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
import CopyToClipboardAction from './actions/CopyToClipboardAction';
export default function DisplayLayoutPlugin(options) { export default function DisplayLayoutPlugin(options) {
return function (openmct) { return function (openmct) {
openmct.actions.register(new CopyToClipboardAction(openmct));
openmct.objectViews.addProvider({ openmct.objectViews.addProvider({
key: 'layout.view', key: 'layout.view',
canView: function (domainObject) { canView: function (domainObject) {

View File

@ -341,7 +341,7 @@ describe('the plugin', function () {
it('provides controls including separators', () => { it('provides controls including separators', () => {
const displayLayoutToolbar = openmct.toolbars.get(selection); const displayLayoutToolbar = openmct.toolbars.get(selection);
expect(displayLayoutToolbar.length).toBe(11); expect(displayLayoutToolbar.length).toBe(9);
}); });
}); });
}); });

View File

@ -1,11 +1,10 @@
<template> <template>
<a <a
class="l-grid-view__item c-grid-item" class="l-grid-view__item c-grid-item"
:class="{ :class="[{
'is-alias': item.isAlias === true, '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 'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1
}" }, statusClass]"
:href="objectLink" :href="objectLink"
> >
<div <div
@ -27,8 +26,8 @@
</div> </div>
</div> </div>
<div class="c-grid-item__controls"> <div class="c-grid-item__controls">
<div class="is-missing__indicator" <div class="is-status__indicator"
title="This item is missing" title="This item is missing or suspect"
></div> ></div>
<div <div
class="icon-people" class="icon-people"
@ -46,9 +45,10 @@
<script> <script>
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture'; import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
import objectLink from '../../../ui/mixins/object-link'; import objectLink from '../../../ui/mixins/object-link';
import statusListener from './status-listener';
export default { export default {
mixins: [contextMenuGesture, objectLink], mixins: [contextMenuGesture, objectLink, statusListener],
props: { props: {
item: { item: {
type: Object, type: Object,

View File

@ -8,18 +8,18 @@
<a <a
ref="objectLink" ref="objectLink"
class="c-object-label" class="c-object-label"
:class="{ 'is-missing': item.model.status === 'missing' }" :class="[statusClass]"
:href="objectLink" :href="objectLink"
> >
<div <div
class="c-object-label__type-icon c-list-item__type-icon" class="c-object-label__type-icon c-list-item__name__type-icon"
:class="item.type.cssClass" :class="item.type.cssClass"
> >
<span class="is-missing__indicator" <span class="is-status__indicator"
title="This item is missing" title="This item is missing or suspect"
></span> ></span>
</div> </div>
<div class="c-object-label__name c-list-item__name">{{ item.model.name }}</div> <div class="c-object-label__name c-list-item__name__name">{{ item.model.name }}</div>
</a> </a>
</td> </td>
<td class="c-list-item__type"> <td class="c-list-item__type">
@ -39,9 +39,10 @@
import moment from 'moment'; import moment from 'moment';
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture'; import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
import objectLink from '../../../ui/mixins/object-link'; import objectLink from '../../../ui/mixins/object-link';
import statusListener from './status-listener';
export default { export default {
mixins: [contextMenuGesture, objectLink], mixins: [contextMenuGesture, objectLink, statusListener],
props: { props: {
item: { item: {
type: Object, type: Object,

View File

@ -11,6 +11,8 @@
body.desktop & { body.desktop & {
flex-flow: row wrap; flex-flow: row wrap;
align-content: flex-start;
&__item { &__item {
height: $gridItemDesk; height: $gridItemDesk;
width: $gridItemDesk; width: $gridItemDesk;
@ -41,7 +43,7 @@
} }
} }
&.is-missing { &.is-status--missing {
@include isMissing(); @include isMissing();
[class*='__type-icon'], [class*='__type-icon'],

View File

@ -1,11 +1,19 @@
/******************************* LIST ITEM */ /******************************* LIST ITEM */
.c-list-item { .c-list-item {
&__type-icon { &__name__type-icon {
color: $colorItemTreeIcon; color: $colorItemTreeIcon;
} }
&__name { &__name__name {
@include ellipsize(); @include ellipsize();
a & {
color: $colorItemFg;
}
}
&:not(.c-list-item__name) {
color: $colorItemFgDetails;
} }
&.is-alias { &.is-alias {

View File

@ -28,9 +28,5 @@
padding-top: $p; padding-top: $p;
padding-bottom: $p; padding-bottom: $p;
width: 25%; width: 25%;
&:not(.c-list-item__name) {
color: $colorItemFgDetails;
}
} }
} }

View File

@ -0,0 +1,33 @@
export default {
inject: ['openmct'],
props: {
item: {
type: Object,
required: true
}
},
computed: {
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
}
},
data() {
return {
status: ''
};
},
methods: {
setStatus(status) {
this.status = status;
}
},
mounted() {
let identifier = this.item.model.identifier;
this.status = this.openmct.status.get(identifier);
this.removeStatusListener = this.openmct.status.observe(identifier, this.setStatus);
},
destroyed() {
this.removeStatusListener();
}
};

View File

@ -25,6 +25,8 @@ export default class GoToOriginalAction {
this.name = 'Go To Original'; this.name = 'Go To Original';
this.key = 'goToOriginal'; this.key = 'goToOriginal';
this.description = 'Go to the original unlinked instance of this object'; this.description = 'Go to the original unlinked instance of this object';
this.group = 'action';
this.priority = 4;
this._openmct = openmct; this._openmct = openmct;
} }

View File

@ -23,6 +23,6 @@ import GoToOriginalAction from './goToOriginalAction';
export default function () { export default function () {
return function (openmct) { return function (openmct) {
openmct.contextMenu.registerAction(new GoToOriginalAction(openmct)); openmct.actions.register(new GoToOriginalAction(openmct));
}; };
} }

View File

@ -7,34 +7,44 @@
@mouseover="focusElement" @mouseover="focusElement"
> >
<div class="c-imagery__main-image-wrapper has-local-controls"> <div class="c-imagery__main-image-wrapper has-local-controls">
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc"> <div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
<span class="holder flex-elem grows c-imagery__lc__sliders"> <span class="c-image-controls__sliders"
draggable="true"
@dragstart="startDrag"
>
<div class="c-image-controls__slider-wrapper icon-brightness">
<input v-model="filters.brightness" <input v-model="filters.brightness"
class="icon-brightness"
type="range" type="range"
min="0" min="0"
max="500" max="500"
> >
</div>
<div class="c-image-controls__slider-wrapper icon-contrast">
<input v-model="filters.contrast" <input v-model="filters.contrast"
class="icon-contrast"
type="range" type="range"
min="0" min="0"
max="500" max="500"
> >
</div>
</span> </span>
<span class="holder flex-elem t-reset-btn-holder c-imagery__lc__reset-btn"> <span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset">
<a class="s-icon-button icon-reset t-btn-reset" <a class="s-icon-button icon-reset t-btn-reset"
@click="filters={brightness: 100, contrast: 100}" @click="filters={brightness: 100, contrast: 100}"
></a> ></a>
</span> </span>
</div> </div>
<div class="main-image s-image-main c-imagery__main-image has-local-controls" <div class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }" :class="{'paused unnsynced': isPaused,'stale':false }"
:style="{'background-image': imageUrl ? `url(${imageUrl})` : 'none', >
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}" <div class="c-imagery__main-image__image"
:style="{
'background-image': imageUrl ? `url(${imageUrl})` : 'none',
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
}"
:data-openmct-image-timestamp="time" :data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString" :data-openmct-object-keystring="keyString"
> ></div>
</div>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons"> <div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev" <button class="c-nav c-nav--prev"
title="Previous image" title="Previous image"
@ -47,18 +57,17 @@
@click="nextImage()" @click="nextImage()"
></button> ></button>
</div> </div>
</div>
<div class="c-imagery__control-bar"> <div class="c-imagery__control-bar">
<div class="c-imagery__time"> <div class="c-imagery__time">
<div class="c-imagery__timestamp">{{ time }}</div> <div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div>
<div <div
v-if="canTrackDuration" v-if="canTrackDuration"
:class="{'c-imagery--new': isImageNew && !refreshCSS}" :class="{'c-imagery--new': isImageNew && !refreshCSS}"
class="c-imagery__age icon-timer" class="c-imagery__age icon-timer"
>{{ formattedDuration }}</div> >{{ formattedDuration }}</div>
</div> </div>
<div class="h-local-controls flex-elem"> <div class="h-local-controls">
<button <button
class="c-button icon-pause pause-play" class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}" :class="{'is-paused': isPaused}"
@ -446,6 +455,10 @@ export default {
this.setFocusedImage(--index, THUMBNAIL_CLICKED); this.setFocusedImage(--index, THUMBNAIL_CLICKED);
} }
}, },
startDrag(e) {
e.preventDefault();
e.stopPropagation();
},
arrowDownHandler(event) { arrowDownHandler(event) {
let key = event.keyCode; let key = event.keyCode;

View File

@ -1,8 +1,8 @@
.c-imagery { .c-imagery {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 auto;
overflow: hidden; overflow: hidden;
height: 100%;
&:focus { &:focus {
outline: none; outline: none;
@ -19,16 +19,24 @@
} }
&__main-image { &__main-image {
background-position: center; &__bg {
background-repeat: no-repeat; background-color: $colorPlotBg;
background-size: contain; border: 1px solid transparent;
height: 100%; flex: 1 1 auto;
&.unnsynced{ &.unnsynced{
@include sUnsynced(); @include sUnsynced();
} }
} }
&__image {
@include abs(); // Safari fix
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
}
&__control-bar, &__control-bar,
&__time { &__time {
display: flex; display: flex;
@ -138,11 +146,6 @@
} }
} }
.s-image-main {
background-color: $colorPlotBg;
border: 1px solid transparent;
}
/*************************************** IMAGERY LOCAL CONTROLS*/ /*************************************** IMAGERY LOCAL CONTROLS*/
.c-imagery { .c-imagery {
.h-local-controls--overlay-content { .h-local-controls--overlay-content {
@ -152,7 +155,7 @@
background: $colorLocalControlOvrBg; background: $colorLocalControlOvrBg;
border-radius: $basicCr; border-radius: $basicCr;
max-width: 200px; max-width: 200px;
min-width: 100px; min-width: 70px;
width: 35%; width: 35%;
align-items: center; align-items: center;
padding: $interiorMargin $interiorMarginLg; padding: $interiorMargin $interiorMarginLg;
@ -173,6 +176,7 @@
&__lc { &__lc {
&__reset-btn { &__reset-btn {
$bc: $scrollbarTrackColorBg; $bc: $scrollbarTrackColorBg;
&:before, &:before,
&:after { &:after {
border-right: 1px solid $bc; border-right: 1px solid $bc;
@ -195,6 +199,46 @@
} }
} }
.c-image-controls {
// Brightness/contrast
&__controls {
// Sliders and reset element
display: flex;
align-items: center;
margin-right: $interiorMargin; // Need some extra space due to proximity to close button
}
&__sliders {
display: flex;
flex: 1 1 auto;
flex-direction: column;
> * + * {
margin-top: 11px;
}
}
&__slider-wrapper {
// A wrapper is needed to add the type icon to left of each range input
display: flex;
align-items: center;
&:before {
color: rgba($colorMenuFg, 0.5);
margin-right: $interiorMarginSm;
}
input[type='range'] {
width: 100px;
}
}
&__btn-reset {
flex: 0 0 auto;
}
}
/*************************************** BUTTONS */ /*************************************** BUTTONS */
.c-button.pause-play { .c-button.pause-play {
// Pause icon set by default in markup // Pause icon set by default in markup
@ -211,14 +255,13 @@
} }
.c-imagery__prev-next-buttons { .c-imagery__prev-next-buttons {
//background: rgba(deeppink, 0.2);
display: flex; display: flex;
width: 100%; width: 100%;
justify-content: space-between; justify-content: space-between;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-75%);
.c-nav { .c-nav {
pointer-events: all; pointer-events: all;

View File

@ -28,6 +28,8 @@ export default class NewFolderAction {
this.key = 'newFolder'; this.key = 'newFolder';
this.description = 'Create a new folder'; this.description = 'Create a new folder';
this.cssClass = 'icon-folder-new'; this.cssClass = 'icon-folder-new';
this.group = "action";
this.priority = 9;
this._openmct = openmct; this._openmct = openmct;
this._dialogForm = { this._dialogForm = {

View File

@ -23,6 +23,6 @@ import NewFolderAction from './newFolderAction';
export default function () { export default function () {
return function (openmct) { return function (openmct) {
openmct.contextMenu.registerAction(new NewFolderAction(openmct)); openmct.actions.register(new NewFolderAction(openmct));
}; };
} }

View File

@ -40,9 +40,7 @@ describe("the plugin", () => {
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.startHeadless();
newFolderAction = openmct.contextMenu._allActions.filter(action => { newFolderAction = openmct.actions._allActions.newFolder;
return action.key === 'newFolder';
})[0];
}); });
afterEach(() => { afterEach(() => {

View File

@ -0,0 +1,39 @@
import { getDefaultNotebook } from '../utils/notebook-storage';
import { addNotebookEntry } from '../utils/notebook-entries';
export default class CopyToNotebookAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-duplicate';
this.description = 'Copy to Notebook action';
this.group = "action";
this.key = 'copyToNotebook';
this.name = 'Copy to Notebook';
this.priority = 9;
}
copyToNotebook(entryText) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText);
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this.openmct.notifications.info(msg);
});
}
invoke(objectPath, viewContext) {
this.copyToNotebook(viewContext.formattedValueForCopy());
}
appliesTo(objectPath, viewContext) {
if (viewContext && viewContext.getViewKey) {
return viewContext.getViewKey().includes('alphanumeric-format');
}
return false;
}
}

View File

@ -111,7 +111,7 @@ import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue'; import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue'; import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage'; import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { DEFAULT_CLASS, addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries'; import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
import objectUtils from 'objectUtils'; import objectUtils from 'objectUtils';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
@ -416,14 +416,7 @@ export default {
return; return;
} }
const classList = domainObject.classList || []; this.openmct.status.delete(domainObject.identifier);
const index = classList.indexOf(DEFAULT_CLASS);
if (!classList.length || index < 0) {
return;
}
classList.splice(index, 1);
this.openmct.objects.mutate(domainObject, 'classList', classList);
}, },
searchItem(input) { searchItem(input) {
this.search = input; this.search = input;

View File

@ -143,7 +143,8 @@ export default {
this.openmct.notifications.alert(message); this.openmct.notifications.alert(message);
} }
window.location.href = link; const url = new URL(link);
window.location.href = url.hash;
}, },
formatTime(unixTime, timeFormat) { formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat); return Moment.utc(unixTime).format(timeFormat);

View File

@ -12,12 +12,11 @@
<div class="c-ne__content"> <div class="c-ne__content">
<div :id="entry.id" <div :id="entry.id"
class="c-ne__text" class="c-ne__text"
:class="{'c-input-inline' : !readOnly }" :class="{'c-ne__input' : !readOnly }"
:contenteditable="!readOnly" :contenteditable="!readOnly"
:style="!entry.text.length ? defaultEntryStyle : ''"
@blur="updateEntryValue($event, entry.id)" @blur="updateEntryValue($event, entry.id)"
@focus="updateCurrentEntryValue($event, entry.id)" @focus="updateCurrentEntryValue($event, entry.id)"
>{{ entry.text.length ? entry.text : defaultText }}</div> >{{ entry.text }}</div>
<div class="c-snapshots c-ne__embeds"> <div class="c-snapshots c-ne__embeds">
<NotebookEmbed v-for="embed in entry.embeds" <NotebookEmbed v-for="embed in entry.embeds"
:key="embed.id" :key="embed.id"
@ -106,12 +105,7 @@ export default {
}, },
data() { data() {
return { return {
currentEntryValue: '', currentEntryValue: ''
defaultEntryStyle: {
fontStyle: 'italic',
color: '#6e6e6e'
},
defaultText: 'add description'
}; };
}, },
computed: { computed: {
@ -235,24 +229,13 @@ export default {
this.entry.embeds.splice(embedPosition, 1); this.entry.embeds.splice(embedPosition, 1);
this.updateEntry(this.entry); this.updateEntry(this.entry);
}, },
selectTextInsideElement(element) {
const range = document.createRange();
range.selectNodeContents(element);
let selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
updateCurrentEntryValue($event) { updateCurrentEntryValue($event) {
if (this.readOnly) { if (this.readOnly) {
return; return;
} }
const target = $event.target; const target = $event.target;
this.currentEntryValue = target ? target.innerText : ''; this.currentEntryValue = target ? target.textContent : '';
if (!this.entry.text.length) {
this.selectTextInsideElement(target);
}
}, },
updateEmbed(newEmbed) { updateEmbed(newEmbed) {
this.entry.embeds.some(e => { this.entry.embeds.some(e => {
@ -292,6 +275,8 @@ export default {
const entryPos = this.entryPosById(entryId); const entryPos = this.entryPosById(entryId);
const value = target.textContent.trim(); const value = target.textContent.trim();
if (this.currentEntryValue !== value) { if (this.currentEntryValue !== value) {
target.textContent = value;
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage); const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries[entryPos].text = value; entries[entryPos].text = value;

View File

@ -1,29 +1,17 @@
<template> <template>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button <button
class="c-button--menu icon-notebook" class="c-icon-button c-button--menu icon-camera"
title="Take a Notebook Snapshot" title="Take a Notebook Snapshot"
@click="setNotebookTypes" @click.stop.prevent="showMenu"
@click.stop="toggleMenu"
> >
<span class="c-button__label"></span> <span
title="Take Notebook Snapshot"
class="c-icon-button__label"
>
Snapshot
</span>
</button> </button>
<div
v-show="showMenu"
class="c-menu"
>
<ul>
<li
v-for="(type, index) in notebookTypes"
:key="index"
:class="type.cssClass"
:title="type.name"
@click="snapshot(type)"
>
{{ type.name }}
</li>
</ul>
</div>
</div> </div>
</template> </template>
@ -57,22 +45,20 @@ export default {
data() { data() {
return { return {
notebookSnapshot: null, notebookSnapshot: null,
notebookTypes: [], notebookTypes: []
showMenu: false
}; };
}, },
mounted() { mounted() {
this.notebookSnapshot = new Snapshot(this.openmct); this.notebookSnapshot = new Snapshot(this.openmct);
this.setDefaultNotebookStatus();
document.addEventListener('click', this.hideMenu);
},
destroyed() {
document.removeEventListener('click', this.hideMenu);
}, },
methods: { methods: {
setNotebookTypes() { showMenu(event) {
const notebookTypes = []; const notebookTypes = [];
const defaultNotebook = getDefaultNotebook(); const defaultNotebook = getDefaultNotebook();
const elementBoundingClientRect = this.$el.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
if (defaultNotebook) { if (defaultNotebook) {
const domainObject = defaultNotebook.domainObject; const domainObject = defaultNotebook.domainObject;
@ -83,35 +69,31 @@ export default {
notebookTypes.push({ notebookTypes.push({
cssClass: 'icon-notebook', cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`, name: `Save to Notebook ${defaultPath}`,
type: NOTEBOOK_DEFAULT callBack: () => {
return this.snapshot(NOTEBOOK_DEFAULT);
}
}); });
} }
} }
notebookTypes.push({ notebookTypes.push({
cssClass: 'icon-notebook', cssClass: 'icon-camera',
name: 'Save to Notebook Snapshots', name: 'Save to Notebook Snapshots',
type: NOTEBOOK_SNAPSHOT callBack: () => {
return this.snapshot(NOTEBOOK_SNAPSHOT);
}
}); });
this.notebookTypes = notebookTypes; this.openmct.menus.showMenu(x, y, notebookTypes);
},
toggleMenu() {
this.showMenu = !this.showMenu;
},
hideMenu() {
this.showMenu = false;
}, },
snapshot(notebook) { snapshot(notebook) {
this.hideMenu();
this.$nextTick(() => { this.$nextTick(() => {
const element = document.querySelector('.c-overlay__contents') const element = document.querySelector('.c-overlay__contents')
|| document.getElementsByClassName('l-shell__main-container')[0]; || document.getElementsByClassName('l-shell__main-container')[0];
const bounds = this.openmct.time.bounds(); const bounds = this.openmct.time.bounds();
const link = !this.ignoreLink const link = !this.ignoreLink
? window.location.href ? window.location.hash
: null; : null;
const objectPath = this.objectPath || this.openmct.router.path; const objectPath = this.objectPath || this.openmct.router.path;
@ -124,6 +106,15 @@ export default {
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element); this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
}); });
},
setDefaultNotebookStatus() {
let defaultNotebookObject = getDefaultNotebook();
if (defaultNotebookObject && defaultNotebookObject.notebookMeta) {
let notebookIdentifier = defaultNotebookObject.notebookMeta.identifier;
this.openmct.status.set(notebookIdentifier, 'notebook-default');
}
} }
} }
}; };

View File

@ -4,7 +4,7 @@
<div class="l-browse-bar__start"> <div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w"> <div class="l-browse-bar__object-name--w">
<div class="l-browse-bar__object-name c-object-label"> <div class="l-browse-bar__object-name c-object-label">
<div class="c-object-label__type-icon icon-notebook"></div> <div class="c-object-label__type-icon icon-camera"></div>
<div class="c-object-label__name"> <div class="c-object-label__name">
Notebook Snapshots Notebook Snapshots
<span v-if="snapshots.length" <span v-if="snapshots.length"

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="c-indicator c-indicator--clickable icon-notebook" <div class="c-indicator c-indicator--clickable icon-camera"
:class="[ :class="[
{ 's-status-off': snapshotCount === 0 }, { 's-status-off': snapshotCount === 0 },
{ 's-status-on': snapshotCount > 0 }, { 's-status-on': snapshotCount > 0 },

View File

@ -1,3 +1,4 @@
import CopyToNotebookAction from './actions/CopyToNotebookAction';
import Notebook from './components/Notebook.vue'; import Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue'; import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import SnapshotContainer from './snapshot-container'; import SnapshotContainer from './snapshot-container';
@ -13,6 +14,8 @@ export default function NotebookPlugin() {
installed = true; installed = true;
openmct.actions.register(new CopyToNotebookAction(openmct));
const notebookType = { const notebookType = {
name: 'Notebook', name: 'Notebook',
description: 'Create and save timestamped notes with embedded object snapshots.', description: 'Create and save timestamped notes with embedded object snapshots.',

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing'; import { createOpenMct, resetApplicationState } from 'utils/testing';
import NotebookPlugin from './plugin'; import NotebookPlugin from './plugin';
import Vue from 'vue'; import Vue from 'vue';
@ -133,90 +133,4 @@ describe("Notebook plugin:", () => {
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
}); });
describe("Notebook Snapshots view:", () => {
let snapshotIndicator;
let drawerElement;
function clickSnapshotIndicator() {
const indicator = element.querySelector('.icon-notebook');
const button = indicator.querySelector('button');
const clickEvent = createMouseEvent('click');
button.dispatchEvent(clickEvent);
}
beforeAll(() => {
snapshotIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
element.append(snapshotIndicator);
return Vue.nextTick();
});
afterAll(() => {
snapshotIndicator.remove();
if (drawerElement) {
drawerElement.remove();
}
});
beforeEach(() => {
drawerElement = document.querySelector('.l-shell__drawer');
});
afterEach(() => {
if (drawerElement) {
drawerElement.classList.remove('is-expanded');
}
});
it("has Snapshots indicator", () => {
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
expect(hasSnapshotIndicator).toBe(true);
});
it("snapshots container has class isExpanded", () => {
let classes = drawerElement.classList;
const isExpandedBefore = classes.contains('is-expanded');
clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterFirstClick = classes.contains('is-expanded');
const success = isExpandedBefore === false
&& isExpandedAfterFirstClick === true;
expect(success).toBe(true);
});
it("snapshots container does not have class isExpanded", () => {
let classes = drawerElement.classList;
const isExpandedBefore = classes.contains('is-expanded');
clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterFirstClick = classes.contains('is-expanded');
clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterSecondClick = classes.contains('is-expanded');
const success = isExpandedBefore === false
&& isExpandedAfterFirstClick === true
&& isExpandedAfterSecondClick === false;
expect(success).toBe(true);
});
it("show notebook snapshots container text", () => {
clickSnapshotIndicator();
const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
const snapshotsText = notebookSnapshots.textContent.trim();
expect(snapshotsText).toBe('Notebook Snapshots');
});
});
}); });

View File

@ -49,7 +49,7 @@ export default class Snapshot {
.then(domainObject => { .then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed); addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
const defaultPath = `${domainObject.name} > ${notebookStorage.section.name} > ${notebookStorage.page.name}`; const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`; const msg = `Saved to Notebook ${defaultPath}`;
this._showNotification(msg); this._showNotification(msg);
}); });

View File

@ -1,6 +1,6 @@
import objectLink from '../../../ui/mixins/object-link'; import objectLink from '../../../ui/mixins/object-link';
export const DEFAULT_CLASS = 'is-notebook-default'; export const DEFAULT_CLASS = 'notebook-default';
const TIME_BOUNDS = { const TIME_BOUNDS = {
START_BOUND: 'tc.startBound', START_BOUND: 'tc.startBound',
END_BOUND: 'tc.endBound', END_BOUND: 'tc.endBound',
@ -103,7 +103,7 @@ export function createNewEmbed(snapshotMeta, snapshot = '') {
}; };
} }
export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null) { export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null, entryText = '') {
if (!openmct || !domainObject || !notebookStorage) { if (!openmct || !domainObject || !notebookStorage) {
return; return;
} }
@ -125,11 +125,11 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
defaultEntries.push({ defaultEntries.push({
id, id,
createdOn: date, createdOn: date,
text: '', text: entryText,
embeds embeds
}); });
addDefaultClass(domainObject); addDefaultClass(domainObject, openmct);
openmct.objects.mutate(domainObject, 'configuration.entries', entries); openmct.objects.mutate(domainObject, 'configuration.entries', entries);
return id; return id;
@ -199,11 +199,6 @@ export function deleteNotebookEntries(openmct, domainObject, selectedSection, se
openmct.objects.mutate(domainObject, 'configuration.entries', entries); openmct.objects.mutate(domainObject, 'configuration.entries', entries);
} }
function addDefaultClass(domainObject) { function addDefaultClass(domainObject, openmct) {
const classList = domainObject.classList || []; openmct.status.set(domainObject.identifier, DEFAULT_CLASS);
if (classList.includes(DEFAULT_CLASS)) {
return;
}
classList.push(DEFAULT_CLASS);
} }

View File

@ -40,7 +40,7 @@
<div class="c-state-indicator__alert-cursor-lock icon-cursor-lock" title="Cursor is point locked. Click anywhere in the plot to unlock."></div> <div class="c-state-indicator__alert-cursor-lock icon-cursor-lock" title="Cursor is point locked. Click anywhere in the plot to unlock."></div>
<div class="plot-legend-item" <div class="plot-legend-item"
ng-class="{ ng-class="{
'is-missing': series.domainObject.status === 'missing' 'is-status--missing': series.domainObject.status === 'missing'
}" }"
ng-repeat="series in series track by $index" ng-repeat="series in series track by $index"
> >
@ -48,7 +48,7 @@
<span class="plot-series-color-swatch" <span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }"> ng-style="{ 'background-color': series.get('color').asHexString() }">
</span> </span>
<span class="is-missing__indicator" title="This item is missing"></span> <span class="is-status__indicator" title="This item is missing or suspect"></span>
<span class="plot-series-name">{{ series.nameWithUnit() }}</span> <span class="plot-series-name">{{ series.nameWithUnit() }}</span>
</div> </div>
<div class="plot-series-value hover-value-enabled value-to-display-{{ legend.get('valueToShowWhenCollapsed') }} {{ series.closest.mctLimitState.cssClass }}" <div class="plot-series-value hover-value-enabled value-to-display-{{ legend.get('valueToShowWhenCollapsed') }} {{ series.closest.mctLimitState.cssClass }}"
@ -95,14 +95,14 @@
<tr ng-repeat="series in series" <tr ng-repeat="series in series"
class="plot-legend-item" class="plot-legend-item"
ng-class="{ ng-class="{
'is-missing': series.domainObject.status === 'missing' 'is-status--missing': series.domainObject.status === 'missing'
}" }"
> >
<td class="plot-series-swatch-and-name"> <td class="plot-series-swatch-and-name">
<span class="plot-series-color-swatch" <span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }"> ng-style="{ 'background-color': series.get('color').asHexString() }">
</span> </span>
<span class="is-missing__indicator" title="This item is missing"></span> <span class="is-status__indicator" title="This item is missing or suspect"></span>
<span class="plot-series-name">{{ series.get('name') }}</span> <span class="plot-series-name">{{ series.get('name') }}</span>
</td> </td>

View File

@ -46,7 +46,7 @@
</button> </button>
</div> </div>
<div class="l-view-section"> <div class="l-view-section u-style-receiver js-style-receiver">
<div class="c-loading--overlay loading" <div class="c-loading--overlay loading"
ng-show="!!pending"></div> ng-show="!!pending"></div>
<mct-plot config="controller.config" <mct-plot config="controller.config"

View File

@ -45,7 +45,7 @@
title="Toggle grid lines"> title="Toggle grid lines">
</button> </button>
</div> </div>
<div class="l-view-section"> <div class="l-view-section u-style-receiver js-style-receiver">
<div class="c-loading--overlay loading" <div class="c-loading--overlay loading"
ng-show="!!currentRequest.pending"></div> ng-show="!!currentRequest.pending"></div>
<div class="gl-plot child-frame u-inspectable" <div class="gl-plot child-frame u-inspectable"

View File

@ -58,7 +58,8 @@ define([
'./newFolderAction/plugin', './newFolderAction/plugin',
'./persistence/couch/plugin', './persistence/couch/plugin',
'./defaultRootName/plugin', './defaultRootName/plugin',
'./timeline/plugin' './timeline/plugin',
'./viewDatumAction/plugin'
], function ( ], function (
_, _,
UTCTimeSystem, UTCTimeSystem,
@ -97,7 +98,8 @@ define([
NewFolderAction, NewFolderAction,
CouchDBPlugin, CouchDBPlugin,
DefaultRootName, DefaultRootName,
Timeline Timeline,
ViewDatumAction
) { ) {
const bundleMap = { const bundleMap = {
LocalStorage: 'platform/persistence/local', LocalStorage: 'platform/persistence/local',
@ -191,6 +193,7 @@ define([
plugins.ISOTimeFormat = ISOTimeFormat.default; plugins.ISOTimeFormat = ISOTimeFormat.default;
plugins.DefaultRootName = DefaultRootName.default; plugins.DefaultRootName = DefaultRootName.default;
plugins.Timeline = Timeline.default; plugins.Timeline = Timeline.default;
plugins.ViewDatumAction = ViewDatumAction.default;
return plugins; return plugins;
}); });

View File

@ -25,6 +25,8 @@ export default class RemoveAction {
this.key = 'remove'; this.key = 'remove';
this.description = 'Remove this object from its containing object.'; this.description = 'Remove this object from its containing object.';
this.cssClass = "icon-trash"; this.cssClass = "icon-trash";
this.group = "action";
this.priority = 1;
this.openmct = openmct; this.openmct = openmct;
} }
@ -103,6 +105,16 @@ export default class RemoveAction {
let parentType = parent && this.openmct.types.get(parent.type); let parentType = parent && this.openmct.types.get(parent.type);
let child = objectPath[0]; let child = objectPath[0];
let locked = child.locked ? child.locked : parent && parent.locked; let locked = child.locked ? child.locked : parent && parent.locked;
let isEditing = this.openmct.editor.isEditing();
if (isEditing) {
let currentItemInView = this.openmct.router.path[0];
let domainObject = objectPath[0];
if (this.openmct.objects.areIdsEqual(currentItemInView.identifier, domainObject.identifier)) {
return false;
}
}
if (locked) { if (locked) {
return false; return false;

View File

@ -23,6 +23,6 @@ import RemoveAction from "./RemoveAction";
export default function () { export default function () {
return function (openmct) { return function (openmct) {
openmct.contextMenu.registerAction(new RemoveAction(openmct)); openmct.actions.register(new RemoveAction(openmct));
}; };
} }

View File

@ -29,13 +29,13 @@
@click="showTab(tab, index)" @click="showTab(tab, index)"
> >
<div class="c-tabs-view__tab__label c-object-label" <div class="c-tabs-view__tab__label c-object-label"
:class="{'is-missing': tab.domainObject.status === 'missing'}" :class="[tab.status ? `is-${tab.status}` : '']"
> >
<div class="c-object-label__type-icon" <div class="c-object-label__type-icon"
:class="tab.type.definition.cssClass" :class="tab.type.definition.cssClass"
> >
<span class="is-missing__indicator" <span class="is-status__indicator"
title="This item is missing" title="This item is missing or suspect"
></span> ></span>
</div> </div>
<span class="c-button__label c-object-label__name">{{ tab.domainObject.name }}</span> <span class="c-button__label c-object-label__name">{{ tab.domainObject.name }}</span>
@ -192,8 +192,10 @@ export default {
}, },
addItem(domainObject) { addItem(domainObject) {
let type = this.openmct.types.get(domainObject.type) || unknownObjectType; let type = this.openmct.types.get(domainObject.type) || unknownObjectType;
let status = this.openmct.status.get(domainObject.identifier);
let tabItem = { let tabItem = {
domainObject, domainObject,
status,
type: type, type: type,
key: this.openmct.objects.makeKeyString(domainObject.identifier) key: this.openmct.objects.makeKeyString(domainObject.identifier)
}; };

View File

@ -25,6 +25,7 @@ define([
'lodash', 'lodash',
'./collections/BoundedTableRowCollection', './collections/BoundedTableRowCollection',
'./collections/FilteredTableRowCollection', './collections/FilteredTableRowCollection',
'./TelemetryTableNameColumn',
'./TelemetryTableRow', './TelemetryTableRow',
'./TelemetryTableColumn', './TelemetryTableColumn',
'./TelemetryTableUnitColumn', './TelemetryTableUnitColumn',
@ -34,6 +35,7 @@ define([
_, _,
BoundedTableRowCollection, BoundedTableRowCollection,
FilteredTableRowCollection, FilteredTableRowCollection,
TelemetryTableNameColumn,
TelemetryTableRow, TelemetryTableRow,
TelemetryTableColumn, TelemetryTableColumn,
TelemetryTableUnitColumn, TelemetryTableUnitColumn,
@ -71,6 +73,24 @@ define([
openmct.time.on('timeSystem', this.refreshData); openmct.time.on('timeSystem', this.refreshData);
} }
/**
* @private
*/
addNameColumn(telemetryObject, metadataValues) {
let metadatum = metadataValues.find(m => m.key === 'name');
if (!metadatum) {
metadatum = {
format: 'string',
key: 'name',
name: 'Name'
};
}
const column = new TelemetryTableNameColumn(this.openmct, telemetryObject, metadatum);
this.configuration.addSingleColumnForObject(telemetryObject, column);
}
initialize() { initialize() {
if (this.domainObject.type === 'table') { if (this.domainObject.type === 'table') {
this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters); this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters);
@ -160,7 +180,6 @@ define([
processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) { processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) {
let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
this.boundedRows.add(telemetryRows); this.boundedRows.add(telemetryRows);
this.emit('historical-rows-processed');
} }
/** /**
@ -212,7 +231,13 @@ define([
addColumnsForObject(telemetryObject) { addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
this.addNameColumn(telemetryObject, metadataValues);
metadataValues.forEach(metadatum => { metadataValues.forEach(metadatum => {
if (metadatum.key === 'name') {
return;
}
let column = this.createColumn(metadatum); let column = this.createColumn(metadatum);
this.configuration.addSingleColumnForObject(telemetryObject, column); this.configuration.addSingleColumnForObject(telemetryObject, column);
// add units column if available // add units column if available

View File

@ -0,0 +1,44 @@
/*****************************************************************************
* 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([
'./TelemetryTableColumn.js'
], function (
TelemetryTableColumn
) {
class TelemetryTableNameColumn extends TelemetryTableColumn {
constructor(openmct, telemetryObject, metadatum) {
super(openmct, metadatum);
this.telemetryObject = telemetryObject;
}
getRawValue() {
return this.telemetryObject.name;
}
getFormattedValue() {
return this.telemetryObject.name;
}
}
return TelemetryTableNameColumn;
});

View File

@ -26,6 +26,7 @@ define([], function () {
this.columns = columns; this.columns = columns;
this.datum = createNormalizedDatum(datum, columns); this.datum = createNormalizedDatum(datum, columns);
this.fullDatum = datum;
this.limitEvaluator = limitEvaluator; this.limitEvaluator = limitEvaluator;
this.objectKeyString = objectKeyString; this.objectKeyString = objectKeyString;
} }
@ -87,7 +88,7 @@ define([], function () {
} }
getContextMenuActions() { getContextMenuActions() {
return []; return ['viewDatumAction'];
} }
} }

View File

@ -54,15 +54,13 @@ define([
view(domainObject, objectPath) { view(domainObject, objectPath) {
let table = new TelemetryTable(domainObject, openmct); let table = new TelemetryTable(domainObject, openmct);
let component; let component;
let markingProp = { let markingProp = {
enable: true, enable: true,
useAlternateControlBar: false, useAlternateControlBar: false,
rowName: '', rowName: '',
rowNamePlural: '' rowNamePlural: ''
}; };
const view = {
return {
show: function (element, editMode) { show: function (element, editMode) {
component = new Vue({ component = new Vue({
el: element, el: element,
@ -72,7 +70,8 @@ define([
data() { data() {
return { return {
isEditing: editMode, isEditing: editMode,
markingProp markingProp,
view
}; };
}, },
provide: { provide: {
@ -80,7 +79,7 @@ define([
table, table,
objectPath objectPath
}, },
template: '<table-component :isEditing="isEditing" :marking="markingProp"/>' template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp" :view="view"/>'
}); });
}, },
onEditModeChange(editMode) { onEditModeChange(editMode) {
@ -89,11 +88,22 @@ define([
onClearData() { onClearData() {
table.clearData(); table.clearData();
}, },
getViewContext() {
if (component) {
return component.$refs.tableComponent.getViewContext();
} else {
return {
type: 'telemetry-table'
};
}
},
destroy: function (element) { destroy: function (element) {
component.$destroy(); component.$destroy();
component = undefined; component = undefined;
} }
}; };
return view;
}, },
priority() { priority() {
return 1; return 1;

View File

@ -0,0 +1,123 @@
/*****************************************************************************
* 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.
*****************************************************************************/
let exportCSV = {
name: 'Export Table Data',
key: 'export-csv-all',
description: "Export this view's data",
cssClass: 'icon-download labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().exportAllDataAsCSV();
},
group: 'view'
};
let exportMarkedRows = {
name: 'Export Marked Rows',
key: 'export-csv-marked',
description: "Export marked rows as CSV",
cssClass: 'icon-download labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().exportMarkedRows();
},
group: 'view'
};
let unmarkAllRows = {
name: 'Unmark All Rows',
key: 'unmark-all-rows',
description: 'Unmark all rows',
cssClass: 'icon-x labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().unmarkAllRows();
},
showInStatusBar: true,
group: 'view'
};
let pause = {
name: 'Pause',
key: 'pause-data',
description: 'Pause real-time data flow',
cssClass: 'icon-pause',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().togglePauseByButton();
},
showInStatusBar: true,
group: 'view'
};
let play = {
name: 'Play',
key: 'play-data',
description: 'Continue real-time data flow',
cssClass: 'c-button pause-play is-paused',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().togglePauseByButton();
},
showInStatusBar: true,
group: 'view'
};
let expandColumns = {
name: 'Expand Columns',
key: 'expand-columns',
description: "Increase column widths to fit currently available data.",
cssClass: 'icon-arrows-right-left labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().expandColumns();
},
showInStatusBar: true,
group: 'view'
};
let autosizeColumns = {
name: 'Autosize Columns',
key: 'autosize-columns',
description: "Automatically size columns to fit the table into the available space.",
cssClass: 'icon-expand labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().autosizeColumns();
},
showInStatusBar: true,
group: 'view'
};
let viewActions = [
exportCSV,
exportMarkedRows,
unmarkAllRows,
pause,
play,
expandColumns,
autosizeColumns
];
viewActions.forEach(action => {
action.appliesTo = (objectPath, viewProvider = {}) => {
let viewContext = viewProvider.getViewContext && viewProvider.getViewContext();
if (viewContext) {
let type = viewContext.type;
return type === 'telemetry-table';
}
return false;
};
});
export default viewActions;

View File

@ -0,0 +1,54 @@
<template>
<tr class="c-telemetry-table__sizing-tr"><td>SIZING ROW</td></tr>
</template>
<script>
export default {
props: {
isEditing: {
type: Boolean,
default: false
}
},
watch: {
isEditing: function (isEditing) {
if (isEditing) {
this.pollForRowHeight();
} else {
this.clearPoll();
}
}
},
mounted() {
this.$nextTick().then(() => {
this.height = this.$el.offsetHeight;
this.$emit('change-height', this.height);
});
if (this.isEditing) {
this.pollForRowHeight();
}
},
destroyed() {
this.clearPoll();
},
methods: {
pollForRowHeight() {
this.clearPoll();
this.pollID = window.setInterval(this.heightPoll, 300);
},
clearPoll() {
if (this.pollID) {
window.clearInterval(this.pollID);
this.pollID = undefined;
}
},
heightPoll() {
let height = this.$el.offsetHeight;
if (height !== this.height) {
this.$emit('change-height', height);
this.height = height;
}
}
}
};
</script>

View File

@ -102,7 +102,17 @@ export default {
selectable[columnKeys] = this.row.columns[columnKeys].selectable; selectable[columnKeys] = this.row.columns[columnKeys].selectable;
return selectable; return selectable;
}, {}) }, {}),
actionsViewContext: {
getViewContext: () => {
return {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: this.getDatum,
skipCache: true
};
}
}
}; };
}, },
computed: { computed: {
@ -170,14 +180,24 @@ export default {
event.stopPropagation(); event.stopPropagation();
} }
}, },
getDatum() {
return this.row.fullDatum;
},
showContextMenu: function (event) { showContextMenu: function (event) {
event.preventDefault(); event.preventDefault();
this.markRow(event);
this.row.getContextualDomainObject(this.openmct, this.row.objectKeyString).then(domainObject => { this.row.getContextualDomainObject(this.openmct, this.row.objectKeyString).then(domainObject => {
let contextualObjectPath = this.objectPath.slice(); let contextualObjectPath = this.objectPath.slice();
contextualObjectPath.unshift(domainObject); contextualObjectPath.unshift(domainObject);
this.openmct.contextMenu._showContextMenuForObjectPath(contextualObjectPath, event.x, event.y, this.row.getContextMenuActions()); let allActions = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext);
let applicableActions = this.row.getContextMenuActions().map(key => allActions[key]);
if (applicableActions.length) {
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
}
}); });
} }
} }

View File

@ -9,6 +9,9 @@
.c-telemetry-table { .c-telemetry-table {
// Table that displays telemetry in a scrolling body area // Table that displays telemetry in a scrolling body area
@include fontAndSize();
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
justify-content: flex-start; justify-content: flex-start;
@ -108,7 +111,7 @@
display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define
align-items: stretch; align-items: stretch;
position: absolute; position: absolute;
height: 18px; // Needed when a row has empty values in its cells min-height: 18px; // Needed when a row has empty values in its cells
.is-editing .l-layout__frame & { .is-editing .l-layout__frame & {
pointer-events: none; pointer-events: none;
@ -151,6 +154,12 @@
} }
} }
&__sizing-tr {
// A row element used to determine sizing of rows based on font size
visibility: hidden;
pointer-events: none;
}
&__footer { &__footer {
$pt: 2px; $pt: 2px;
border-top: 1px solid $colorInteriorBorder; border-top: 1px solid $colorInteriorBorder;

View File

@ -23,8 +23,7 @@
<div class="c-table-wrapper" <div class="c-table-wrapper"
:class="{ 'is-paused': paused }" :class="{ 'is-paused': paused }"
> >
<!-- main contolbar start--> <div v-if="enableLegacyToolbar"
<div v-if="!marking.useAlternateControlBar"
class="c-table-control-bar c-control-bar" class="c-table-control-bar c-control-bar"
> >
<button <button
@ -94,7 +93,6 @@
<slot name="buttons"></slot> <slot name="buttons"></slot>
</div> </div>
<!-- main controlbar end -->
<!-- alternate controlbar start --> <!-- alternate controlbar start -->
<div v-if="marking.useAlternateControlBar" <div v-if="marking.useAlternateControlBar"
@ -113,11 +111,11 @@
<button <button
:class="{'hide-nice': !markedRows.length}" :class="{'hide-nice': !markedRows.length}"
class="c-button icon-x labeled" class="c-icon-button icon-x labeled"
title="Deselect All" title="Deselect All"
@click="unmarkAllRows()" @click="unmarkAllRows()"
> >
<span class="c-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span> <span class="c-icon-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span>
</button> </button>
<slot name="buttons"></slot> <slot name="buttons"></slot>
@ -125,7 +123,7 @@
<!-- alternate controlbar end --> <!-- alternate controlbar end -->
<div <div
class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar" class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar u-style-receiver js-style-receiver"
:class="{ :class="{
'loading': loading, 'loading': loading,
'is-paused' : paused 'is-paused' : paused
@ -234,6 +232,10 @@
class="c-telemetry-table__sizing js-telemetry-table__sizing" class="c-telemetry-table__sizing js-telemetry-table__sizing"
:style="sizingTableWidth" :style="sizingTableWidth"
> >
<sizing-row
:is-editing="isEditing"
@change-height="setRowHeight"
/>
<tr> <tr>
<template v-for="(title, key) in headers"> <template v-for="(title, key) in headers">
<th <th
@ -270,6 +272,7 @@ import TableFooterIndicator from './table-footer-indicator.vue';
import CSVExporter from '../../../exporters/CSVExporter.js'; import CSVExporter from '../../../exporters/CSVExporter.js';
import _ from 'lodash'; import _ from 'lodash';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue'; import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import SizingRow from './sizing-row.vue';
const VISIBLE_ROW_COUNT = 100; const VISIBLE_ROW_COUNT = 100;
const ROW_HEIGHT = 17; const ROW_HEIGHT = 17;
@ -282,7 +285,8 @@ export default {
TableColumnHeader, TableColumnHeader,
search, search,
TableFooterIndicator, TableFooterIndicator,
ToggleSwitch ToggleSwitch,
SizingRow
}, },
inject: ['table', 'openmct', 'objectPath'], inject: ['table', 'openmct', 'objectPath'],
props: { props: {
@ -295,12 +299,12 @@ export default {
default: true default: true
}, },
allowFiltering: { allowFiltering: {
'type': Boolean, type: Boolean,
'default': true default: true
}, },
allowSorting: { allowSorting: {
'type': Boolean, type: Boolean,
'default': true default: true
}, },
marking: { marking: {
type: Object, type: Object,
@ -313,6 +317,17 @@ export default {
rowNamePlural: "" rowNamePlural: ""
}; };
} }
},
enableLegacyToolbar: {
type: Boolean,
default: false
},
view: {
type: Object,
required: false,
default() {
return {};
}
} }
}, },
data() { data() {
@ -388,6 +403,40 @@ export default {
markedRows: { markedRows: {
handler(newVal, oldVal) { handler(newVal, oldVal) {
this.$emit('marked-rows-updated', newVal, oldVal); this.$emit('marked-rows-updated', newVal, oldVal);
if (this.viewActionsCollection) {
if (newVal.length > 0) {
this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']);
} else if (newVal.length === 0) {
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
}
}
}
},
paused: {
handler(newVal) {
if (this.viewActionsCollection) {
if (newVal) {
this.viewActionsCollection.hide(['pause-data']);
this.viewActionsCollection.show(['play-data']);
} else {
this.viewActionsCollection.hide(['play-data']);
this.viewActionsCollection.show(['pause-data']);
}
}
}
},
isAutosizeEnabled: {
handler(newVal) {
if (this.viewActionsCollection) {
if (newVal) {
this.viewActionsCollection.show(['expand-columns']);
this.viewActionsCollection.hide(['autosize-columns']);
} else {
this.viewActionsCollection.show(['autosize-columns']);
this.viewActionsCollection.hide(['expand-columns']);
}
}
} }
} }
}, },
@ -400,6 +449,11 @@ export default {
this.rowsRemoved = _.throttle(this.rowsRemoved, 200); this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
this.scroll = _.throttle(this.scroll, 100); this.scroll = _.throttle(this.scroll, 100);
if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {
this.viewActionsCollection = this.openmct.actions.get(this.objectPath, this.view);
this.initializeViewActions();
}
this.table.on('object-added', this.addObject); this.table.on('object-added', this.addObject);
this.table.on('object-removed', this.removeObject); this.table.on('object-removed', this.removeObject);
this.table.on('outstanding-requests', this.outstandingRequests); this.table.on('outstanding-requests', this.outstandingRequests);
@ -506,7 +560,7 @@ export default {
let columnWidths = {}; let columnWidths = {};
let totalWidth = 0; let totalWidth = 0;
let headerKeys = Object.keys(this.headers); let headerKeys = Object.keys(this.headers);
let sizingTableRow = this.sizingTable.children[0]; let sizingTableRow = this.sizingTable.children[1];
let sizingCells = sizingTableRow.children; let sizingCells = sizingTableRow.children;
headerKeys.forEach((headerKey, headerIndex, array) => { headerKeys.forEach((headerKey, headerIndex, array) => {
@ -840,7 +894,7 @@ export default {
for (let i = firstRowIndex; i <= lastRowIndex; i++) { for (let i = firstRowIndex; i <= lastRowIndex; i++) {
let row = allRows[i]; let row = allRows[i];
row.marked = true; this.$set(row, 'marked', true);
if (row !== baseRow) { if (row !== baseRow) {
this.markedRows.push(row); this.markedRows.push(row);
@ -901,6 +955,46 @@ export default {
this.isAutosizeEnabled = true; this.isAutosizeEnabled = true;
this.$nextTick().then(this.calculateColumnWidths); this.$nextTick().then(this.calculateColumnWidths);
},
getViewContext() {
return {
type: 'telemetry-table',
exportAllDataAsCSV: this.exportAllDataAsCSV,
exportMarkedRows: this.exportMarkedRows,
unmarkAllRows: this.unmarkAllRows,
togglePauseByButton: this.togglePauseByButton,
expandColumns: this.recalculateColumnWidths,
autosizeColumns: this.autosizeColumns
};
},
initializeViewActions() {
if (this.markedRows.length > 0) {
this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']);
} else if (this.markedRows.length === 0) {
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
}
if (this.paused) {
this.viewActionsCollection.hide(['pause-data']);
this.viewActionsCollection.show(['play-data']);
} else {
this.viewActionsCollection.hide(['play-data']);
this.viewActionsCollection.show(['pause-data']);
}
if (this.isAutosizeEnabled) {
this.viewActionsCollection.show(['expand-columns']);
this.viewActionsCollection.hide(['autosize-columns']);
} else {
this.viewActionsCollection.show(['autosize-columns']);
this.viewActionsCollection.hide(['expand-columns']);
}
},
setRowHeight(height) {
this.rowHeight = height;
this.setHeight();
this.calculateTableSize();
this.clearRowsAndRerender();
} }
} }
}; };

View File

@ -23,11 +23,13 @@
define([ define([
'./TelemetryTableViewProvider', './TelemetryTableViewProvider',
'./TableConfigurationViewProvider', './TableConfigurationViewProvider',
'./TelemetryTableType' './TelemetryTableType',
'./ViewActions'
], function ( ], function (
TelemetryTableViewProvider, TelemetryTableViewProvider,
TableConfigurationViewProvider, TableConfigurationViewProvider,
TelemetryTableType TelemetryTableType,
TelemetryTableViewActions
) { ) {
return function plugin() { return function plugin() {
return function install(openmct) { return function install(openmct) {
@ -41,6 +43,10 @@ define([
return true; return true;
} }
}); });
TelemetryTableViewActions.default.forEach(action => {
openmct.actions.register(action);
});
}; };
}; };
}); });

View File

@ -168,6 +168,8 @@ describe("the plugin", () => {
return telemetryPromise; return telemetryPromise;
}); });
openmct.router.path = [testTelemetryObject];
applicableViews = openmct.objectViews.get(testTelemetryObject); applicableViews = openmct.objectViews.get(testTelemetryObject);
tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table'); tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
@ -183,10 +185,11 @@ describe("the plugin", () => {
it("Renders a column for every item in telemetry metadata", () => { it("Renders a column for every item in telemetry metadata", () => {
let headers = element.querySelectorAll('span.c-telemetry-table__headers__label'); let headers = element.querySelectorAll('span.c-telemetry-table__headers__label');
expect(headers.length).toBe(3); expect(headers.length).toBe(4);
expect(headers[0].innerText).toBe('Time'); expect(headers[0].innerText).toBe('Name');
expect(headers[1].innerText).toBe('Some attribute'); expect(headers[1].innerText).toBe('Time');
expect(headers[2].innerText).toBe('Another attribute'); expect(headers[2].innerText).toBe('Some attribute');
expect(headers[3].innerText).toBe('Another attribute');
}); });
it("Supports column reordering via drag and drop", () => { it("Supports column reordering via drag and drop", () => {

View File

@ -141,10 +141,11 @@
<ConductorMode class="c-conductor__mode-select" /> <ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" /> <ConductorTimeSystem class="c-conductor__time-system-select" />
<ConductorHistory <ConductorHistory
v-if="isFixed"
class="c-conductor__history-select" class="c-conductor__history-select"
:offsets="openmct.time.clockOffsets()"
:bounds="bounds" :bounds="bounds"
:time-system="timeSystem" :time-system="timeSystem"
:mode="timeMode"
/> />
</div> </div>
<input <input
@ -210,6 +211,11 @@ export default {
isZooming: false isZooming: false
}; };
}, },
computed: {
timeMode() {
return this.isFixed ? 'fixed' : 'realtime';
}
},
mounted() { mounted() {
document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp); document.addEventListener('keyup', this.handleKeyUp);

View File

@ -66,7 +66,9 @@
<script> <script>
import toggleMixin from '../../ui/mixins/toggle-mixin'; import toggleMixin from '../../ui/mixins/toggle-mixin';
const LOCAL_STORAGE_HISTORY_KEY = 'tcHistory'; const DEFAULT_DURATION_FORMATTER = 'duration';
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
const DEFAULT_RECORDS = 10; const DEFAULT_RECORDS = 10;
export default { export default {
@ -77,72 +79,115 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
offsets: {
type: Object,
required: false,
default: () => {}
},
timeSystem: { timeSystem: {
type: Object, type: Object,
required: true required: true
},
mode: {
type: String,
required: true
} }
}, },
data() { data() {
return { return {
/** /**
* previous bounds entries available for easy re-use * previous bounds entries available for easy re-use
* @history array of timespans * @realtimeHistory array of timespans
* @timespans {start, end} number representing timestamp * @timespans {start, end} number representing timestamp
*/ */
history: this.getHistoryFromLocalStorage(), realtimeHistory: {},
/**
* previous bounds entries available for easy re-use
* @fixedHistory array of timespans
* @timespans {start, end} number representing timestamp
*/
fixedHistory: {},
presets: [] presets: []
}; };
}, },
computed: { computed: {
currentHistory() {
return this.mode + 'History';
},
isFixed() {
return this.openmct.time.clock() === undefined;
},
hasHistoryPresets() { hasHistoryPresets() {
return this.timeSystem.isUTCBased && this.presets.length; return this.timeSystem.isUTCBased && this.presets.length;
}, },
historyForCurrentTimeSystem() { historyForCurrentTimeSystem() {
const history = this.history[this.timeSystem.key]; const history = this[this.currentHistory][this.timeSystem.key];
return history; return history;
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (this.mode !== 'fixed') {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
return key;
} }
}, },
watch: { watch: {
bounds: { bounds: {
handler() {
// only for fixed time since we track offsets for realtime
if (this.isFixed) {
this.addTimespan();
}
},
deep: true
},
offsets: {
handler() { handler() {
this.addTimespan(); this.addTimespan();
}, },
deep: true deep: true
}, },
timeSystem: { timeSystem: {
handler() { handler(ts) {
this.loadConfiguration(); this.loadConfiguration();
this.addTimespan(); this.addTimespan();
}, },
deep: true deep: true
},
mode: function () {
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
this.loadConfiguration();
} }
}, },
mounted() { mounted() {
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory(); this.initializeHistoryIfNoHistory();
}, },
methods: { methods: {
getHistoryFromLocalStorage() { getHistoryFromLocalStorage() {
const localStorageHistory = localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY); const localStorageHistory = localStorage.getItem(this.storageKey);
const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined; const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;
this[this.currentHistory] = history;
return history;
}, },
initializeHistoryIfNoHistory() { initializeHistoryIfNoHistory() {
if (!this.history) { if (!this[this.currentHistory]) {
this.history = {}; this[this.currentHistory] = {};
this.persistHistoryToLocalStorage(); this.persistHistoryToLocalStorage();
} }
}, },
persistHistoryToLocalStorage() { persistHistoryToLocalStorage() {
localStorage.setItem(LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(this.history)); localStorage.setItem(this.storageKey, JSON.stringify(this[this.currentHistory]));
}, },
addTimespan() { addTimespan() {
const key = this.timeSystem.key; const key = this.timeSystem.key;
let [...currentHistory] = this.history[key] || []; let [...currentHistory] = this[this.currentHistory][key] || [];
const timespan = { const timespan = {
start: this.bounds.start, start: this.isFixed ? this.bounds.start : this.offsets.start,
end: this.bounds.end end: this.isFixed ? this.bounds.end : this.offsets.end
}; };
let self = this; let self = this;
@ -160,20 +205,24 @@ export default {
} }
currentHistory.unshift(timespan); currentHistory.unshift(timespan);
this.history[key] = currentHistory; this.$set(this[this.currentHistory], key, currentHistory);
this.persistHistoryToLocalStorage(); this.persistHistoryToLocalStorage();
}, },
selectTimespan(timespan) { selectTimespan(timespan) {
if (this.isFixed) {
this.openmct.time.bounds(timespan); this.openmct.time.bounds(timespan);
} else {
this.openmct.time.clockOffsets(timespan);
}
}, },
selectPresetBounds(bounds) { selectPresetBounds(bounds) {
const start = typeof bounds.start === 'function' ? bounds.start() : bounds.start; const start = typeof bounds.start === 'function' ? bounds.start() : bounds.start;
const end = typeof bounds.end === 'function' ? bounds.end() : bounds.end; const end = typeof bounds.end === 'function' ? bounds.end() : bounds.end;
this.selectTimespan({ this.selectTimespan({
start: start, start,
end: end end
}); });
}, },
loadConfiguration() { loadConfiguration() {
@ -184,7 +233,9 @@ export default {
this.records = this.loadRecords(configurations); this.records = this.loadRecords(configurations);
}, },
loadPresets(configurations) { loadPresets(configurations) {
const configuration = configurations.find(option => option.presets); const configuration = configurations.find(option => {
return option.presets && option.name.toLowerCase() === this.mode;
});
const presets = configuration ? configuration.presets : []; const presets = configuration ? configuration.presets : [];
return presets; return presets;
@ -196,11 +247,24 @@ export default {
return records; return records;
}, },
formatTime(time) { formatTime(time) {
let format = this.timeSystem.timeFormat;
let isNegativeOffset = false;
if (!this.isFixed) {
if (time < 0) {
isNegativeOffset = true;
}
time = Math.abs(time);
format = this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER;
}
const formatter = this.openmct.telemetry.getValueFormatter({ const formatter = this.openmct.telemetry.getValueFormatter({
format: this.timeSystem.timeFormat format: format
}).formatter; }).formatter;
return formatter.format(time); return (isNegativeOffset ? '-' : '') + formatter.format(time);
} }
} }
}; };

View File

@ -0,0 +1,69 @@
/*****************************************************************************
* 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 MetadataListView from './components/MetadataList.vue';
import Vue from 'vue';
export default class ViewDatumAction {
constructor(openmct) {
this.name = 'View Full Datum';
this.key = 'viewDatumAction';
this.description = 'View full value of datum received';
this.cssClass = 'icon-object';
this._openmct = openmct;
}
invoke(objectPath, view) {
let viewContext = view.getViewContext && view.getViewContext();
let attributes = viewContext.getDatum && viewContext.getDatum();
let component = new Vue ({
provide: {
name: this.name,
attributes
},
components: {
MetadataListView
},
template: '<MetadataListView />'
});
this._openmct.overlays.overlay({
element: component.$mount().$el,
size: 'large',
dismissable: true,
onDestroy: () => {
component.$destroy();
}
});
}
appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext() || {};
let datum = viewContext.getDatum;
let enabled = viewContext.viewDatumAction;
if (enabled && datum) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,24 @@
<template>
<div class="c-attributes-view">
<div class="c-overlay__top-bar">
<div class="c-overlay__dialog-title">{{ name }}</div>
</div>
<div class="c-overlay__contents-main l-preview-window__object-view">
<ul class="c-attributes-view__content">
<li
v-for="attribute in Object.keys(attributes)"
:key="attribute"
>
<span class="c-attributes-view__grid-item__label">{{ attribute }}</span>
<span class="c-attributes-view__grid-item__value">{{ attributes[attribute] }}</span>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
inject: ['name', 'attributes']
};
</script>

View File

@ -0,0 +1,30 @@
.c-attributes-view {
display: flex;
flex: 1 1 auto;
flex-direction: column;
> * {
flex: 0 0 auto;
}
&__content {
$p: 3px;
display: grid;
grid-template-columns: max-content 1fr;
grid-row-gap: $p;
li { display: contents; }
[class*="__grid-item"] {
border-bottom: 1px solid rgba(#999, 0.2);
padding: 0 5px $p 0;
}
[class*="__label"] {
opacity: 0.8;
}
}
}

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