Compare commits

...

49 Commits

Author SHA1 Message Date
Deep Tailor
c58a387a70 fix status class 2020-11-12 16:54:20 -08:00
Deep Tailor
77d5cef709 Adding a new Status API to Open MCT (#3479)
* wip

* simplifying class applications

* add status to tabs view

* add tests for status API

* fix lint errors

* fix style application in telemetry vue

* 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;

* fix lint issues

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
2020-11-12 16:06:06 -08:00
Deep Tailor
0126542411 merge latest master 2020-11-12 15:04:09 -08:00
Shefali Joshi
b0203f2272 Preparing master for the next sprint v1.4.1-SNAPSHOT (#3508) 2020-11-09 13:10:50 -08:00
Charles Hacskaylo
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
Deep Tailor
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
Deep Tailor
02be8a9875 merge with master 2020-11-03 13:48:28 -08:00
Deep Tailor
3bd57c8fff add tests for actionsAPI 2020-11-03 13:43:45 -08:00
Jamie V
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
David Tsay
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
Shefali Joshi
2401473012 [#3465] Intercept drag start event for imagery controls (#3485) 2020-11-02 11:26:33 -08:00
Deep Tailor
94091b25ec remove focused describe 2020-11-02 11:09:59 -08:00
Deep Tailor
c191ffb37d add tests for MenuAPI 2020-11-02 11:06:59 -08:00
Charles Hacskaylo
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
Charles Hacskaylo
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
Nikhil
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
Deep Tailor
eb709a60cb Merge branch 'master' of https://github.com/nasa/openmct into three-dot-menu-proto 2020-10-29 15:24:51 -07:00
Jamie V
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
Jamie V
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
Nikhil
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
Shefali Joshi
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
David Tsay
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
David Tsay
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
David Tsay
463c44679d [Display Layout] User should be able to toggle grid lines (#3331)
* 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

* extract display layout grid to own vue component

* split toolbar structure into two structures

* allow toggling grid when editing display layout

* toggle grid icon show/hide state on click

* grid be shown on starting edit mode

* remove dimensions code for other display layout feature

* toggle icon after method completes

* change icon names

* update spec to include new action and separator

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-10-23 09:32:35 -07:00
Sanchit Singhal
c1f3ea4e61 fixed windows scss load time issues (#3361)
Co-authored-by: MUDKIP-9560\sanch <sanchit.singhal@mandsconsulting.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2020-10-22 16:17:41 -07:00
Deep Tailor
bf3fd66942 fix broken export marked rows 2020-10-20 16:54:53 -07:00
Deep Tailor
8414ded1ec Merge branch 'master' into three-dot-menu-proto 2020-10-19 18:01:44 -07:00
Nikhil
142b767470 [Notebook] new notebook entry causes console error #3440 (#3443)
* [Notebook] new notebook entry causes console error #3440

* using 'makeKeyString' to compare notebook identifiers

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-10-19 17:57:57 -07:00
Deep Tailor
184b716b53 [Telemetry Table] Row counts (#3428)
* add marked rows and total rows in tables

* Styling for table row counts addition

- Main styles for new `.c-table-indicator` and elements;
- Refined main layout spacing;
- Layout for table footer elements;
- Hover behavior for footer when table in Display Layout;

* Styling for table row counts addition

- Refined `.c-filter-indication` styles;
- Refined `.c-table-indicator` styles;
- Added dynamic tooltips for total and marked rows count elements;

* fix lint issues

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-10-19 11:48:10 -07:00
Deep Tailor
646c871c76 Merge branch 'master' of https://github.com/nasa/openmct into three-dot-menu-proto 2020-10-19 10:29:38 -07:00
Shefali Joshi
e53399495b Legacy and new object providers work together (#3461)
* Strip mct namespace from ids when getting models from cache

* Revert PersistenceCapability to use legacy code
Enforce empty namespace for LegacyPersistenceAdapter for new object providers

* Reverts change to caching provider

* CouchObject provider is registered with the mct space.
When saving objects via the persistence capability use the mct space to find the couchdb object provider
2020-10-19 10:17:18 -07:00
David Tsay
d27f73579b [Plots] Toggle grid lines (#3313)
* add toggle button

* enable toggle grid lines in plots

* fix merge issue

* change to new glyphs

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-10-19 10:07:51 -07:00
Deep Tailor
ba401b3341 fix lint errors 2020-10-18 15:55:39 -07:00
Deep Tailor
5ef02ec4a2 git enable legacy toolbar for legacy tables 2020-10-15 11:55:21 -07:00
Deep Tailor
d788031019 Merge branch 'master' of https://github.com/nasa/openmct into three-dot-menu-proto 2020-10-15 09:49:32 -07:00
Shefali Joshi
1ae8199e89 Changing master version for new sprint (#3456) 2020-10-14 15:52:27 -07:00
Shefali Joshi
2deb4e8474 Duplicate tree ancestors fix (#3454)
* block nav when "syncing" tree only, where most of the problems popped up
* not populating ancestors on navigation until current directory children loaded

Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
2020-10-14 10:52:45 -07:00
Jamie V
7f10681424 block nav when "syncing" tree only, where most of the problems popped up (#3451) 2020-10-13 10:01:16 -07:00
Shefali Joshi
c756adad6f Move tests to their own describe block (#3447) 2020-10-09 14:29:52 -07:00
Andrew Henry
f3d593bc1e Cache gets (#3437)
* Cache gets

* Added test
2020-10-08 20:30:23 -07:00
Nikhil
b637307de6 [Notebook] Clicking new entry does not work as expected #3423 (#3434)
* [Notebook] Clicking new entry does not work as expected #3423

Co-authored-by: Joshi <simplyrender@gmail.com>
2020-10-08 16:56:37 -07:00
Shefali Joshi
b6e0208e71 Reverting when cancelling out of edits works for both legacy and new object providers (#3435)
* Update persistence capability to use object api get
* Getting objects using the legacy object service provider will use the defaultSpace if necessary
2020-10-08 16:48:26 -07:00
Shefali Joshi
631876cab3 Add missing APIs to legacy persistence adapter (#3433)
* Sends new style object to the object API for save when calling it from legacy persistence adapter

* Adds createObject and deleteObject methods to LegacyPersistenceAdapter
2020-10-08 10:45:06 -07:00
Shefali Joshi
a192d46c2b Sends new style object to the object API for save when calling it from legacy persistence adapter (#3431)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-10-07 16:44:20 -07:00
Jamie V
6923f17645 [Navigation Tree] Race condition on checking document readystate (#3430)
* checking if state is already ready, as this is a subcomponent, that could be the case

* optimizing readystate checks
2020-10-07 16:38:54 -07:00
Nikhil
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
Deep Tailor
711a7a2eb5 fix console error on changing types and canceling edit 2020-10-07 12:30:07 -07:00
Charles Hacskaylo
87a45de05b Fix scroll issues in tree overflow state (#3385)
* Fixes #3383 - Tree scrolling area should not display horizontal scroll.
* Includes various additional improvements to the object tree.
Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
2020-10-07 11:29:42 -07:00
Jamie V
ab76451360 Imagery Age to be displayed for realtime mode in Imagery View (#3308)
* fix linting errors

* removing testing units

* WIP: stubbe in age in template, adding getAge function

* WIP: stubbed in age in template, dummy function to start

* added image age for realtime mode, ready for styling

* reverting unnecesarry telemetryview file changes, not needed for this issue

* checking for age tracking conditions on mount

* Image age styling and changes

- Cleaned up code in ImageryPlugin to use const instead of var, changed
image delay time into a const

* Image age styling and changes

- WIP!
- Layout changes for Imagery control-bar;
- New animation effect, WIP;

* Image age styling and changes

- Markup and CSS updates for Imagery view;
- Final layout for age indicator;

* parsing image timestamp in case it is a string

* using moment for human readable durations above 8 hours

* UTC based timesystem check

* reset "new" css class on image age when "time" updates

* WIP: debuggin weird imagery plugin issue for first selection of image in thumbnails

* fixing pause overwriting clicked images selection

* making isImageNew a computed value

* WIP: pr updates

* WIP: tabling PR edits to focus on lower hanging PR edits for testathon

* WIP

* overhaul of imagery plugin logic for optimization PLUS imagery age

* adding next/prev functionality to refactored plugin

* added arrow left and right keys to navigate next and previous

* added arrow key scrolling and scrolling thumbnail into view and hold down scrolling

* adding in missing class

* component based key listening, PR updates

* refactor to use just imageIndex to track focused image, utilized more caching, PR comment edits

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-10-06 16:01:47 -07:00
124 changed files with 4176 additions and 1217 deletions

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ define([
) {
function ImageryPlugin() {
var IMAGE_SAMPLES = [
const IMAGE_SAMPLES = [
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg",
@@ -47,13 +47,14 @@ define([
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
];
const IMAGE_DELAY = 20000;
function pointForTimestamp(timestamp, name) {
return {
name: name,
utc: Math.floor(timestamp / 5000) * 5000,
local: Math.floor(timestamp / 5000) * 5000,
url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length]
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]
};
}
@@ -64,7 +65,7 @@ define([
subscribe: function (domainObject, callback) {
var interval = setInterval(function () {
callback(pointForTimestamp(Date.now(), domainObject.name));
}, 5000);
}, IMAGE_DELAY);
return function () {
clearInterval(interval);
@@ -81,9 +82,9 @@ define([
var start = options.start;
var end = Math.min(options.end, Date.now());
var data = [];
while (start <= end && data.length < 5000) {
while (start <= end && data.length < IMAGE_DELAY) {
data.push(pointForTimestamp(start, domainObject.name));
start += 5000;
start += IMAGE_DELAY;
}
return Promise.resolve(data);

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-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">
<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>
<body>
</body>
<script>
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'
@@ -73,21 +111,21 @@
{
label: 'Last Day',
bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 24,
start: () => Date.now() - ONE_DAY,
end: () => Date.now()
}
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 2,
start: () => Date.now() - TWO_HOURS,
end: () => Date.now()
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - 1000 * 60 * 60,
start: () => Date.now() - ONE_HOUR,
end: () => Date.now()
}
}
@@ -96,7 +134,7 @@
records: 10,
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
limit: 1000 * 60 * 60 * 24
limit: ONE_DAY
},
{
name: "Realtime",
@@ -105,7 +143,44 @@
clockOffsets: {
start: - THIRTY_MINUTES,
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",
"version": "1.3.0-SNAPSHOT",
"version": "1.4.1-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {

View File

@@ -20,12 +20,12 @@
at runtime from the About dialog for additional information.
-->
<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()}}"
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 class='c-object-label__name'>{{model.name}}</div>
</div>

View File

@@ -114,7 +114,12 @@ define(["objectUtils"],
var self = this,
domainObject = this.domainObject;
let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), domainObject.getId());
const identifier = {
namespace: this.getSpace(),
key: this.getKey()
};
let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), identifier);
return this.openmct.objects
.save(newStyleObject)
@@ -146,6 +151,7 @@ define(["objectUtils"],
return domainObject.useCapability("mutation", function () {
return model;
}, modified);
}
}

View File

@@ -99,8 +99,8 @@ define(
mockNewStyleDomainObject = Object.assign({}, model);
mockNewStyleDomainObject.identifier = {
namespace: "",
key: id
namespace: SPACE,
key: key
};
// Simulate mutation capability

View File

@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<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">
{{clock.zone()}}
</div>

View File

@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<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">
<button ng-click="timer.clickStopButton()"
ng-hide="timer.timerState == 'stopped'"

View File

@@ -246,6 +246,8 @@ define([
this.actions = new api.ActionsAPI(this);
this.status = new api.StatusAPI(this);
this.router = new ApplicationRouter();
this.branding = BrandingAPI.default;

View File

@@ -128,7 +128,7 @@ define([
};
ObjectServiceProvider.prototype.get = function (key) {
const keyString = utils.makeKeyString(key);
let keyString = utils.makeKeyString(key);
return this.objectService.getObjects([keyString])
.then(function (results) {

View File

@@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import utils from 'objectUtils';
export default class LegacyPersistenceAdapter {
constructor(openmct) {
this.openmct = openmct;
@@ -33,8 +35,31 @@ export default class LegacyPersistenceAdapter {
return Promise.resolve(Object.keys(this.openmct.objects.providers));
}
updateObject(legacyDomainObject) {
return this.openmct.objects.save(legacyDomainObject.useCapability('adapter'));
createObject(space, key, legacyDomainObject) {
let object = utils.toNewFormat(legacyDomainObject, {
namespace: space,
key: key
});
return this.openmct.objects.save(object);
}
deleteObject(space, key) {
const identifier = {
namespace: space,
key: key
};
return this.openmct.objects.delete(identifier);
}
updateObject(space, key, legacyDomainObject) {
let object = utils.toNewFormat(legacyDomainObject, {
namespace: space,
key: key
});
return this.openmct.objects.save(object);
}
readObject(space, key) {

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

@@ -30,8 +30,8 @@ define([
'./notifications/NotificationAPI',
'./Editor',
'./menu/MenuAPI',
'./actions/ActionsAPI'
'./actions/ActionsAPI',
'./status/StatusAPI'
], function (
TimeAPI,
ObjectAPI,
@@ -42,7 +42,8 @@ define([
NotificationAPI,
EditorAPI,
MenuAPI,
ActionsAPI
ActionsAPI,
StatusAPI
) {
return {
TimeAPI: TimeAPI,
@@ -54,6 +55,7 @@ define([
NotificationAPI: NotificationAPI.default,
EditorAPI: EditorAPI,
MenuAPI: MenuAPI.default,
ActionsAPI: ActionsAPI.default
ActionsAPI: ActionsAPI.default,
StatusAPI: StatusAPI.default
};
});

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

@@ -47,6 +47,7 @@ define([
this.providers = {};
this.rootRegistry = new RootRegistry();
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
this.cache = {};
}
/**
@@ -154,6 +155,11 @@ define([
* has been saved, or be rejected if it cannot be saved
*/
ObjectAPI.prototype.get = function (identifier) {
let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = utils.parseKeyString(identifier);
const provider = this.getProvider(identifier);
@@ -165,7 +171,15 @@ define([
throw new Error('Provider does not support get!');
}
return provider.get(identifier);
let objectPromise = provider.get(identifier);
this.cache[keystring] = objectPromise;
return objectPromise.then(result => {
delete this.cache[keystring];
return result;
});
};
ObjectAPI.prototype.delete = function () {

View File

@@ -59,4 +59,25 @@ describe("The Object API", () => {
});
});
});
describe("The get function", () => {
describe("when a provider is available", () => {
let mockProvider;
beforeEach(() => {
mockProvider = jasmine.createSpyObj("mock provider", [
"get"
]);
mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
});
it("Caches multiple requests for the same object", () => {
expect(mockProvider.get.calls.count()).toBe(0);
objectAPI.get(mockDomainObject.identifier);
expect(mockProvider.get.calls.count()).toBe(1);
objectAPI.get(mockDomainObject.identifier);
expect(mockProvider.get.calls.count()).toBe(1);
});
});
});
});

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

@@ -21,7 +21,7 @@
*****************************************************************************/
<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">
<thead>
<tr>

View File

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

View File

@@ -21,21 +21,22 @@
*****************************************************************************/
<template>
<div class="c-style">
<span :class="[
{ 'is-style-invisible': styleItem.style.isStyleInvisible },
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
class="c-style-thumb"
>
<span class="c-style-thumb__text"
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
<div class="c-style has-local-controls c-toolbar">
<div class="c-style__controls">
<div :class="[
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
class="c-style-thumb"
>
ABC
</span>
</span>
<span class="c-toolbar">
<span class="c-style-thumb__text"
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
>
ABC
</span>
</div>
<toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
class="c-style__toolbar-button--border-color u-menu-to--center"
:options="borderColorOption"
@@ -61,7 +62,14 @@
:options="isStyleInvisibleOption"
@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>
</template>
@@ -80,12 +88,11 @@ export default {
ToolbarColorPicker,
ToolbarToggleButton
},
inject: [
'openmct'
],
inject: ['openmct'],
props: {
isEditing: {
type: Boolean
type: Boolean,
required: true
},
mixedStyles: {
type: Array,
@@ -93,6 +100,10 @@ export default {
return [];
}
},
nonSpecificFontProperties: {
type: Array,
required: true
},
styleItem: {
type: Object,
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: {
@@ -216,6 +236,9 @@ export default {
}
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">
Object Style
</div>
<FontStyleEditor
v-if="canStyleFont"
:font-style="consolidatedFontStyle"
@set-font-property="setFontProperty"
/>
<div class="c-inspect-styles__content">
<div v-if="staticStyle"
class="c-inspect-styles__style"
@@ -39,7 +44,9 @@
:style-item="staticStyle"
:is-editing="allowEditing"
:mixed-styles="mixedStyles"
:non-specific-font-properties="nonSpecificFontProperties"
@persist="updateStaticStyle"
@save-style="saveStyle"
/>
</div>
<button
@@ -58,10 +65,11 @@
</div>
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject"
class="c-object-label icon-conditional"
class="c-object-label"
:href="navigateToPath"
@click="navigateOrPreview"
>
<span class="c-object-label__type-icon icon-conditional"></span>
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
</a>
<template v-if="allowEditing">
@@ -80,6 +88,12 @@
</template>
</div>
<FontStyleEditor
v-if="canStyleFont"
:font-style="consolidatedFontStyle"
@set-font-property="setFontProperty"
/>
<div v-if="conditionsLoaded"
class="c-inspect-styles__conditions"
>
@@ -97,8 +111,10 @@
/>
<style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:non-specific-font-properties="nonSpecificFontProperties"
:is-editing="allowEditing"
@persist="updateConditionalStyle"
@save-style="saveStyle"
/>
</div>
</div>
@@ -108,6 +124,7 @@
<script>
import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue';
import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js";
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 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 {
name: 'StylesView',
components: {
FontStyleEditor,
StyleEditor,
ConditionError,
ConditionDescription
},
inject: [
'openmct',
'selection'
'selection',
'stylesManager'
],
data() {
return {
@@ -139,19 +170,80 @@ export default {
conditionsLoaded: false,
navigateToPath: '',
selectedConditionId: '',
locked: false
items: [],
domainObject: undefined,
consolidatedFontStyle: {}
};
},
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() {
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() {
this.removeListeners();
this.openmct.editor.off('isEditing', this.setEditState);
this.stylesManager.off('styleSelected', this.applyStyleToSelection);
},
mounted() {
this.items = [];
this.previewAction = new PreviewAction(this.openmct);
this.isMultipleSelection = this.selection.length > 1;
this.getObjectsAndItemsFromSelection();
@@ -166,7 +258,10 @@ export default {
this.initializeStaticStyle();
}
this.setConsolidatedFontStyle();
this.openmct.editor.on('isEditing', this.setEditState);
this.stylesManager.on('styleSelected', this.applyStyleToSelection);
},
methods: {
getObjectStyles() {
@@ -178,10 +273,10 @@ export default {
}
} else if (this.items.length) {
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];
}
} 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;
}
@@ -219,6 +314,18 @@ export default {
isItemType(type, item) {
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) {
const id = layoutItem ? layoutItem.id : undefined;
@@ -235,13 +342,8 @@ export default {
this.selection.forEach((selectionItem) => {
const item = selectionItem[0].context.item;
const layoutItem = selectionItem[0].context.layoutItem;
const layoutDomainObject = selectionItem[0].context.item;
const isChildItem = selectionItem.length > 1;
if (layoutDomainObject && layoutDomainObject.locked) {
this.locked = true;
}
if (!isChildItem) {
domainObject = item;
itemStyle = getApplicableStylesForItem(item);
@@ -251,7 +353,7 @@ export default {
} else {
this.canHide = true;
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);
itemStyle = getApplicableStylesForItem(item);
if (this.hasConditionalStyle(item)) {
@@ -275,7 +377,7 @@ export default {
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
this.initialStyles = styles;
this.mixedStyles = mixedStyles;
// main layout
this.domainObject = domainObject;
this.removeListeners();
if (this.domainObject) {
@@ -298,6 +400,7 @@ export default {
isKeyItemId(key) {
return (key !== 'styles')
&& (key !== 'staticStyle')
&& (key !== 'fontStyle')
&& (key !== 'defaultConditionId')
&& (key !== 'selectedConditionId')
&& (key !== 'conditionSetIdentifier');
@@ -637,6 +740,124 @@ export default {
},
persist(domainObject, 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 {
align-items: baseline;
border-bottom: 1px solid $colorInteriorBorder;
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: $interiorMargin;
.c-object-label {
flex: 1 1 auto;
@@ -53,7 +55,10 @@
}
}
&__style,
&__style {
padding-bottom: $interiorMargin;
}
&__condition {
padding: $interiorMargin;
}

View File

@@ -146,6 +146,8 @@ describe('the plugin', function () {
let displayLayoutItem;
let lineLayoutItem;
let boxLayoutItem;
let notCreatableObjectItem;
let notCreatableObject;
let selection;
let component;
let styleViewComponentObject;
@@ -264,6 +266,19 @@ describe('the plugin', function () {
"stroke": "#717171",
"type": "line-view",
"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": [
@@ -297,6 +312,52 @@ describe('the plugin', function () {
"type": "box-view",
"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 = [
[{
context: {
@@ -316,6 +377,19 @@ describe('the plugin', function () {
"index": 0
}
},
{
context: {
item: displayLayoutItem,
"supportsMultiSelect": true
}
}],
[{
context: {
"item": notCreatableObject,
"layoutItem": notCreatableObjectItem,
"index": 2
}
},
{
context: {
item: displayLayoutItem,
@@ -344,7 +418,7 @@ describe('the plugin', function () {
});
it('initializes the items in the view', () => {
expect(styleViewComponentObject.items.length).toBe(2);
expect(styleViewComponentObject.items.length).toBe(3);
});
it('initializes conditional styles', () => {
@@ -363,7 +437,7 @@ describe('the plugin', function () {
return Vue.nextTick().then(() => {
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
[boxLayoutItem, lineLayoutItem].forEach((item) => {
[boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles;
expect(itemStyles.length).toBe(2);
const foundStyle = itemStyles.find((style) => {
@@ -385,7 +459,7 @@ describe('the plugin', function () {
return Vue.nextTick().then(() => {
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
[boxLayoutItem, lineLayoutItem].forEach((item) => {
[boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle;
expect(itemStyle).toBeDefined();
const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item);

View File

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

View File

@@ -64,9 +64,16 @@ define([
components: {
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 () {
component.$destroy();
component = undefined;

View File

@@ -73,7 +73,6 @@ define(['lodash'], function (_) {
]
}
};
const VIEW_TYPES = {
'telemetry-view': {
value: 'telemetry-view',
@@ -96,7 +95,6 @@ define(['lodash'], function (_) {
class: 'icon-tabular-realtime'
}
};
const APPLICABLE_VIEWS = {
'telemetry-view': [
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) {
return {
control: "button",
@@ -423,7 +398,7 @@ define(['lodash'], function (_) {
property: function (selectionPath) {
return getPath(selectionPath);
},
icon: "icon-font",
icon: "icon-pencil",
title: "Edit text properties",
dialog: DIALOG_FORM.text
};
@@ -623,6 +598,33 @@ define(['lodash'], function (_) {
}
}
function getToggleGridButton(selection, selectionPath) {
const ICON_GRID_SHOW = 'icon-grid-on';
const ICON_GRID_HIDE = 'icon-grid-off';
let displayLayoutContext;
if (selection.length === 1 && selectionPath === undefined) {
displayLayoutContext = selection[0][0].context;
} else {
displayLayoutContext = selectionPath[1].context;
}
return {
control: "button",
domainObject: displayLayoutContext.item,
icon: ICON_GRID_SHOW,
method: function () {
displayLayoutContext.toggleGrid();
this.icon = this.icon === ICON_GRID_SHOW
? ICON_GRID_HIDE
: ICON_GRID_SHOW;
},
secondary: true
};
}
function getSeparator() {
return {
control: "separator"
@@ -637,7 +639,9 @@ define(['lodash'], function (_) {
}
if (isMainLayoutSelected(selectedObjects[0])) {
return [getAddButton(selectedObjects)];
return [
getToggleGridButton(selectedObjects),
getAddButton(selectedObjects)];
}
let toolbar = {
@@ -649,11 +653,11 @@ define(['lodash'], function (_) {
'display-mode': [],
'telemetry-value': [],
'style': [],
'text-style': [],
'position': [],
'duplicate': [],
'unit-toggle': [],
'remove': []
'remove': [],
'toggle-grid': []
};
selectedObjects.forEach(selectionPath => {
@@ -699,12 +703,6 @@ define(['lodash'], function (_) {
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
}
if (toolbar['text-style'].length === 0) {
toolbar['text-style'] = [
getTextSizeMenu(selectedParent, selectedObjects)
];
}
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
@@ -730,12 +728,6 @@ define(['lodash'], function (_) {
}
}
} else if (layoutItem.type === 'text-view') {
if (toolbar['text-style'].length === 0) {
toolbar['text-style'] = [
getTextSizeMenu(selectedParent, selectedObjects)
];
}
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
@@ -800,6 +792,10 @@ define(['lodash'], function (_) {
if (toolbar.duplicate.length === 0) {
toolbar.duplicate = [getDuplicateButton(selectedParent, selectionPath, selectedObjects)];
}
if (toolbar['toggle-grid'].length === 0) {
toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)];
}
});
let toolbarArray = Object.values(toolbar);

View File

@@ -56,6 +56,28 @@ define(function () {
1
],
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')"
>
<div
class="c-box-view"
class="c-box-view u-style-receiver js-style-receiver"
:class="[styleClass]"
:style="style"
></div>

View File

@@ -22,7 +22,7 @@
<template>
<div
class="l-layout"
class="l-layout u-style-receiver js-style-receiver"
:class="{
'is-multi-selected': selectedLayoutItems.length > 1,
'allow-editing': isEditing
@@ -31,21 +31,19 @@
@click.capture="bypassSelection"
@drop="handleDrop"
>
<!-- Background grid -->
<div
<display-layout-grid
v-if="isEditing"
class="l-layout__grid-holder c-grid"
:grid-size="gridSize"
:show-grid="showGrid"
/>
<div
v-if="shouldDisplayLayoutDimensions"
class="l-layout__dimensions"
:style="layoutDimensionsStyle"
>
<div
v-if="gridSize[0] >= 3"
class="c-grid__x l-grid l-grid-x"
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]"
></div>
<div
v-if="gridSize[1] >= 3"
class="c-grid__y l-grid l-grid-y"
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"
></div>
<div class="l-layout__dimensions-vals">
{{ layoutDimensions[0] }},{{ layoutDimensions[1] }}
</div>
</div>
<component
:is="item.type"
@@ -81,6 +79,7 @@ import TextView from './TextView.vue';
import LineView from './LineView.vue';
import ImageView from './ImageView.vue';
import EditMarquee from './EditMarquee.vue';
import DisplayLayoutGrid from './DisplayLayoutGrid.vue';
import _ from 'lodash';
const TELEMETRY_IDENTIFIER_FUNCTIONS = {
@@ -127,6 +126,7 @@ const DUPLICATE_OFFSET = 3;
let components = ITEM_TYPE_VIEW_MAP;
components['edit-marquee'] = EditMarquee;
components['display-layout-grid'] = DisplayLayoutGrid;
function getItemDefinition(itemType, ...options) {
let itemView = ITEM_TYPE_VIEW_MAP[itemType];
@@ -140,6 +140,7 @@ function getItemDefinition(itemType, ...options) {
export default {
components: components,
inject: ['openmct', 'options', 'objectPath'],
props: {
domainObject: {
type: Object,
@@ -156,7 +157,8 @@ export default {
return {
internalDomainObject: domainObject,
initSelectIndex: undefined,
selection: []
selection: [],
showGrid: true
};
},
computed: {
@@ -171,6 +173,23 @@ export default {
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() {
let selectionPath = this.selection[0];
let singleSelectedLine = this.selection.length === 1
@@ -179,7 +198,13 @@ export default {
return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine;
}
},
inject: ['openmct', 'options', 'objectPath'],
watch: {
isEditing(value) {
if (value) {
this.showGrid = value;
}
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) {
this.internalDomainObject = JSON.parse(JSON.stringify(obj));
@@ -798,6 +823,9 @@ export default {
this.removeItem(selection);
this.initSelectIndex = this.layoutItems.length - 1; //restore selection
},
toggleGrid() {
this.showGrid = !this.showGrid;
}
}
};

View File

@@ -0,0 +1,34 @@
<template>
<div
class="l-layout__grid-holder"
:class="{ 'c-grid': showGrid }"
>
<div
v-if="gridSize[0] >= 3"
class="c-grid__x l-grid l-grid-x"
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]"
></div>
<div
v-if="gridSize[1] >= 3"
class="c-grid__y l-grid l-grid-y"
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"
></div>
</div>
</template>
<script>
export default {
props: {
gridSize: {
type: Array,
required: true,
validator: (arr) => arr && arr.length === 2
&& arr.every(el => typeof el === 'number')
},
showGrid: {
type: Boolean,
required: true
}
}
};
</script>

View File

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

View File

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

View File

@@ -31,15 +31,14 @@
<div
v-if="domainObject"
class="c-telemetry-view"
:class="{
styleClass,
'is-missing': domainObject.status === 'missing'
}"
:class="[statusClass]"
:style="styleObject"
@contextmenu.prevent.stop="showContextMenu"
:data-font-size="item.fontSize"
:data-font="item.font"
@contextmenu.prevent="showContextMenu"
>
<div class="is-missing__indicator"
title="This item is missing"
<div class="is-status__indicator"
:title="`This item is ${this.status}`"
></div>
<div
v-if="showLabel"
@@ -74,10 +73,11 @@
import LayoutFrame from './LayoutFrame.vue';
import printj from 'printj';
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1];
const CONTEXT_MENU_ACTIONS = ['viewHistoricalData'];
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
export default {
makeDefinition(openmct, gridSize, domainObject, position) {
@@ -95,7 +95,8 @@ export default {
stroke: "",
fill: "",
color: "",
size: "13px"
fontSize: 'default',
font: 'default'
};
},
inject: ['openmct', 'objectPath'],
@@ -126,13 +127,18 @@ export default {
},
data() {
return {
currentObjectPath: undefined,
datum: undefined,
formats: undefined,
domainObject: undefined,
currentObjectPath: undefined
formats: undefined,
viewKey: `alphanumeric-format-${Math.random()}`,
status: ''
};
},
computed: {
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
},
showLabel() {
let displayMode = this.item.displayMode;
@@ -150,10 +156,15 @@ export default {
return unit;
},
styleObject() {
return Object.assign({}, {
fontSize: this.item.size
}, this.itemStyle);
let size;
//for legacy size support
if (!this.item.fontSize) {
size = this.item.size;
}
return Object.assign({}, {
size
}, this.itemStyle);
},
fieldName() {
return this.valueMetadata && this.valueMetadata.name;
@@ -205,9 +216,13 @@ export default {
this.openmct.objects.get(this.item.identifier)
.then(this.setObject);
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() {
this.removeSubscription();
this.removeStatusListener();
if (this.removeSelectable) {
this.removeSelectable();
@@ -216,6 +231,18 @@ export default {
this.openmct.time.off("bounds", this.refreshData);
},
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() {
let bounds = this.openmct.time.bounds();
let options = {
@@ -296,8 +323,30 @@ export default {
updateTelemetryFormat(format) {
this.$emit('formatChanged', this.item, format);
},
showContextMenu(event) {
this.openmct.menus.showMenu(event.x, event.y, this.applicableActions);
async getContextMenuActions() {
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')"
>
<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]"
:style="style"
>
@@ -47,13 +49,14 @@ export default {
return {
fill: '',
stroke: '',
size: '13px',
color: '',
x: 1,
y: 1,
width: 10,
height: 5,
text: element.text
text: element.text,
fontSize: 'default',
font: 'default'
};
},
inject: ['openmct'],
@@ -84,8 +87,14 @@ export default {
},
computed: {
style() {
let size;
//legacy size support
if (!this.item.fontSize) {
size = this.item.size;
}
return Object.assign({
fontSize: this.item.size
size
}, this.itemStyle);
}
},

View File

@@ -17,10 +17,29 @@
flex-direction: column;
overflow: auto;
&__grid-holder {
&__grid-holder,
&__dimensions {
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 {
position: absolute;
}
@@ -34,6 +53,10 @@
> .l-layout {
background: $editUIGridColorBg;
> [class*="__dimensions"] {
display: block;
}
> [class*="__grid-holder"] {
display: block;
}
@@ -42,12 +65,16 @@
}
.l-layout__frame {
&[s-selected],
&[s-selected]:not([multi-select="true"]),
&[s-selected-parent] {
// 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;
> [class*="__dimensions"] {
display: block;
}
> [class*='grid-holder'] {
display: block;
}

View File

@@ -27,14 +27,20 @@
border: 1px solid transparent;
}
@include isMissing($absPos: true);
.is-missing__indicator {
.is-status__indicator {
top: 0;
left: 0;
}
&.is-missing {
&.is-status--missing {
@include isMissing($absPos: true);
border: $borderMissing;
}
&.is-status--suspect {
@include isSuspect($absPos: true);
border: $borderMissing;
}
}

View File

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

View File

@@ -26,9 +26,12 @@ import objectUtils from 'objectUtils';
import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
import CopyToClipboardAction from './actions/CopyToClipboardAction';
export default function DisplayLayoutPlugin(options) {
return function (openmct) {
openmct.actions.register(new CopyToClipboardAction(openmct));
openmct.objectViews.addProvider({
key: 'layout.view',
canView: function (domainObject) {
@@ -72,7 +75,8 @@ export default function DisplayLayoutPlugin(options) {
duplicateItem: component && component.$refs.displayLayout.duplicateItem,
switchViewType: component && component.$refs.displayLayout.switchViewType,
mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews,
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots,
toggleGrid: component && component.$refs.displayLayout.toggleGrid
};
},
onEditModeChange: function (isEditing) {

View File

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

View File

@@ -3,20 +3,26 @@
@include userSelectNone();
background: $colorFilterBg;
color: $colorFilterFg;
display: flex;
align-items: center;
font-size: 0.9em;
margin-top: $interiorMarginSm;
padding: 2px;
text-transform: uppercase;
&:before {
font-family: symbolsfont-12px;
content: $glyph-icon-filter;
display: block;
font-size: 12px;
margin-right: $interiorMarginSm;
}
&--mixed {
.c-filter-indication__mixed {
font-style: italic;
}
}
&__label {
+ .c-filter-indication__label {
&:before {
content: ', ';
}
}
}
}
.c-filter-tree-item {

View File

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

View File

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

View File

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

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

@@ -1,119 +1,224 @@
<template>
<div class="c-imagery">
<div
tabindex="0"
class="c-imagery"
@keyup="arrowUpHandler"
@keydown="arrowDownHandler"
@mouseover="focusElement"
>
<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">
<span class="holder flex-elem grows c-imagery__lc__sliders">
<input v-model="filters.brightness"
class="icon-brightness"
type="range"
min="0"
max="500"
>
<input v-model="filters.contrast"
class="icon-contrast"
type="range"
min="0"
max="500"
>
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
<span class="c-image-controls__sliders"
draggable="true"
@dragstart="startDrag"
>
<div class="c-image-controls__slider-wrapper icon-brightness">
<input v-model="filters.brightness"
type="range"
min="0"
max="500"
>
</div>
<div class="c-image-controls__slider-wrapper icon-contrast">
<input v-model="filters.contrast"
type="range"
min="0"
max="500"
>
</div>
</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"
@click="filters={brightness: 100, contrast: 100}"
></a>
</span>
</div>
<div class="main-image s-image-main c-imagery__main-image has-local-controls js-imageryView-image"
:class="{'paused unnsynced': paused(),'stale':false }"
:style="{'background-image': getImageUrl() ? `url(${getImageUrl()})` : 'none',
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
:data-openmct-image-timestamp="getTime()"
:data-openmct-object-keystring="keystring"
<div class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }"
>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev"
title="Previous image"
:disabled="isPrevDisabled()"
@click="prevImage()"
></button>
<button class="c-nav c-nav--next"
title="Next image"
:disabled="isNextDisabled()"
@click="nextImage()"
></button>
</div>
<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-object-keystring="keyString"
></div>
</div>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev"
title="Previous image"
:disabled="isPrevDisabled"
@click="prevImage()"
></button>
<button class="c-nav c-nav--next"
title="Next image"
:disabled="isNextDisabled"
@click="nextImage()"
></button>
</div>
<div class="c-imagery__control-bar">
<div class="c-imagery__timestamp">{{ getTime() }}</div>
<div class="h-local-controls flex-elem">
<div class="c-imagery__time">
<div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div>
<div
v-if="canTrackDuration"
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
class="c-imagery__age icon-timer"
>{{ formattedDuration }}</div>
</div>
<div class="h-local-controls">
<button
class="c-button icon-pause pause-play"
:class="{'is-paused': paused()}"
@click="paused(!paused(), true)"
:class="{'is-paused': isPaused}"
@click="paused(!isPaused, 'button')"
></button>
</div>
</div>
</div>
<div ref="thumbsWrapper"
class="c-imagery__thumbs-wrapper"
:class="{'is-paused': paused()}"
:class="{'is-paused': isPaused}"
@scroll="handleScroll"
>
<div v-for="(imageData, index) in imageHistory"
:key="index"
<div v-for="(datum, index) in imageHistory"
:key="datum.url"
class="c-imagery__thumb c-thumb"
:class="{selected: imageData.selected}"
@click="setSelectedImage(imageData)"
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)"
>
<img class="c-thumb__image"
:src="getImageUrl(imageData)"
:src="formatImageUrl(datum)"
>
<div class="c-thumb__timestamp">{{ getTime(imageData) }}</div>
<div class="c-thumb__timestamp">{{ formatTime(datum) }}</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
const DEFAULT_DURATION_FORMATTER = 'duration';
const REFRESH_CSS_MS = 500;
const DURATION_TRACK_MS = 1000;
const ARROW_DOWN_DELAY_CHECK_MS = 400;
const ARROW_SCROLL_RATE_MS = 100;
const THUMBNAIL_CLICKED = true;
const ONE_MINUTE = 60 * 1000;
const FIVE_MINUTES = 5 * ONE_MINUTE;
const ONE_HOUR = ONE_MINUTE * 60;
const EIGHT_HOURS = 8 * ONE_HOUR;
const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
const ARROW_RIGHT = 39;
const ARROW_LEFT = 37;
export default {
inject: ['openmct', 'domainObject'],
data() {
let timeSystem = this.openmct.time.timeSystem();
return {
autoScroll: true,
durationFormatter: undefined,
filters: {
brightness: 100,
contrast: 100
},
image: {
selected: ''
},
imageFormat: '',
imageHistory: [],
imageUrl: '',
thumbnailClick: THUMBNAIL_CLICKED,
isPaused: false,
metadata: {},
requestCount: 0,
timeFormat: '',
keystring: ''
timeSystem: timeSystem,
timeFormatter: undefined,
refreshCSS: false,
keyString: undefined,
focusedImageIndex: undefined,
numericDuration: undefined
};
},
computed: {
bounds() {
return this.openmct.time.bounds();
time() {
return this.formatTime(this.focusedImage);
},
imageUrl() {
return this.formatImageUrl(this.focusedImage);
},
isImageNew() {
let cutoff = FIVE_MINUTES;
let age = this.numericDuration;
return age < cutoff && !this.refreshCSS;
},
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
},
isNextDisabled() {
let disabled = false;
if (this.focusedImageIndex === -1 || this.focusedImageIndex === this.imageHistory.length - 1) {
disabled = true;
}
return disabled;
},
isPrevDisabled() {
let disabled = false;
if (this.focusedImageIndex === 0 || this.imageHistory.length < 2) {
disabled = true;
}
return disabled;
},
focusedImage() {
return this.imageHistory[this.focusedImageIndex];
},
parsedSelectedTime() {
return this.parseTime(this.focusedImage);
},
formattedDuration() {
let result = 'N/A';
let negativeAge = -1;
if (this.numericDuration > TWENTYFOUR_HOURS) {
negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS);
result = moment.duration(negativeAge, 'days').humanize(true);
} else if (this.numericDuration > EIGHT_HOURS) {
negativeAge *= (this.numericDuration / ONE_HOUR);
result = moment.duration(negativeAge, 'hours').humanize(true);
} else if (this.durationFormatter) {
result = this.durationFormatter.format(this.numericDuration);
}
return result;
}
},
watch: {
focusedImageIndex() {
this.trackDuration();
this.resetAgeCSS();
}
},
mounted() {
// set
this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageFormat = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
// initialize
this.timeKey = this.openmct.time.timeSystem().key;
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
this.openmct.time.on('clock', this.clockChange);
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.requestHistory();
@@ -127,41 +232,55 @@ export default {
delete this.unsubscribe;
}
this.stopDurationTracking();
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
this.openmct.time.off('clock', this.clockChange);
},
methods: {
focusElement() {
this.$el.focus();
},
datumIsNotValid(datum) {
if (this.imageHistory.length === 0) {
return false;
}
const datumTime = this.timeFormat.format(datum);
const datumURL = this.imageFormat.format(datum);
const lastHistoryTime = this.timeFormat.format(this.imageHistory.slice(-1)[0]);
const lastHistoryURL = this.imageFormat.format(this.imageHistory.slice(-1)[0]);
const datumURL = this.formatImageUrl(datum);
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history
const datumTimeCheck = this.timeFormat.parse(datum);
const historyTimeCheck = this.timeFormat.parse(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTime === lastHistoryTime) && (datumURL === lastHistoryURL);
const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale;
},
getImageUrl(datum) {
return datum
? this.imageFormat.format(datum)
: this.imageUrl;
formatImageUrl(datum) {
if (!datum) {
return;
}
return this.imageFormatter.format(datum);
},
getTime(datum) {
let dateTimeStr = datum
? this.timeFormat.format(datum)
: this.time;
formatTime(datum) {
if (!datum) {
return;
}
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr ? dateTimeStr.replace("T", " ") : "";
return dateTimeStr.replace("T", " ");
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
@@ -174,26 +293,35 @@ export default {
|| (scrollHeight - scrollTop) > 2 * clientHeight;
this.autoScroll = !disableScroll;
},
paused(state, button = false) {
if (arguments.length > 0 && state !== this.isPaused) {
this.unselectAllImages();
this.isPaused = state;
if (state === true && button) {
// If we are pausing, select the latest image in imageHistory
this.setSelectedImage(this.imageHistory[this.imageHistory.length - 1]);
}
paused(state, type) {
if (this.nextDatum) {
this.updateValues(this.nextDatum);
delete this.nextDatum;
} else {
this.updateValues(this.imageHistory[this.imageHistory.length - 1]);
}
this.isPaused = state;
this.autoScroll = true;
if (type === 'button') {
this.setFocusedImage(this.imageHistory.length - 1);
}
return this.isPaused;
if (this.nextImageIndex) {
this.setFocusedImage(this.nextImageIndex);
delete this.nextImageIndex;
}
this.autoScroll = true;
},
scrollToFocused() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper) {
return;
}
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
if (domThumb) {
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
},
scrollToRight() {
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
@@ -207,22 +335,17 @@ export default {
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
},
setSelectedImage(image) {
// If we are paused and the current image IS selected, unpause
// Otherwise, set current image and pause
if (!image) {
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
this.nextImageIndex = index;
return;
}
if (this.isPaused && image.selected) {
this.paused(false);
this.unselectAllImages();
} else {
this.imageUrl = this.getImageUrl(image);
this.time = this.getTime(image);
this.focusedImageIndex = index;
if (thumbnailClick && !this.isPaused) {
this.paused(true);
this.unselectAllImages();
image.selected = true;
}
},
boundsChange(bounds, isTick) {
@@ -230,98 +353,162 @@ export default {
this.requestHistory();
}
},
requestHistory() {
const requestId = ++this.requestCount;
async requestHistory() {
let bounds = this.openmct.time.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
this.openmct.telemetry
.request(this.domainObject, this.bounds)
.then((values = []) => {
if (this.requestCount === requestId) {
// add each image to the history
// update values for the very last image (set current image time and url)
values.forEach((datum, index) => this.updateHistory(datum, index === values.length - 1));
}
let data = await this.openmct.telemetry
.request(this.domainObject, bounds) || [];
if (this.requestCount === requestId) {
data.forEach((datum, index) => {
this.updateHistory(datum, index === data.length - 1);
});
}
},
timeSystemChange(system) {
// reset timesystem dependent variables
this.timeKey = system.key;
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
this.timeSystem = this.openmct.time.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.trackDuration();
},
clockChange(clock) {
this.trackDuration();
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.timeFormat.parse(datum);
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
if (parsedTimestamp >= this.bounds.start && parsedTimestamp <= this.bounds.end) {
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
this.updateHistory(datum);
}
});
},
unselectAllImages() {
this.imageHistory.forEach(image => image.selected = false);
},
updateHistory(datum, updateValues = true) {
updateHistory(datum, setFocused = true) {
if (this.datumIsNotValid(datum)) {
return;
}
this.imageHistory.push(datum);
if (updateValues) {
this.updateValues(datum);
if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1);
}
},
updateValues(datum) {
if (this.isPaused) {
this.nextDatum = datum;
getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
trackDuration() {
if (this.canTrackDuration) {
this.stopDurationTracking();
this.updateDuration();
this.durationTracker = window.setInterval(
this.updateDuration, DURATION_TRACK_MS
);
} else {
this.stopDurationTracking();
}
},
stopDurationTracking() {
window.clearInterval(this.durationTracker);
},
updateDuration() {
let currentTime = this.openmct.time.clock().currentValue();
this.numericDuration = currentTime - this.parsedSelectedTime;
},
resetAgeCSS() {
this.refreshCSS = true;
// unable to make this work with nextTick
setTimeout(() => {
this.refreshCSS = false;
}, REFRESH_CSS_MS);
},
nextImage() {
if (this.isNextDisabled) {
return;
}
this.time = this.timeFormat.format(datum);
this.imageUrl = this.imageFormat.format(datum);
},
selectedImageIndex() {
return this.imageHistory.findIndex(image => image.selected);
},
setSelectedByIndex(index) {
this.setSelectedImage(this.imageHistory[index]);
},
nextImage() {
let index = this.selectedImageIndex();
this.setSelectedByIndex(++index);
let index = this.focusedImageIndex;
this.setFocusedImage(++index, THUMBNAIL_CLICKED);
if (index === this.imageHistory.length - 1) {
this.paused(false);
}
},
prevImage() {
let index = this.selectedImageIndex();
if (index === -1) {
this.setSelectedByIndex(this.imageHistory.length - 2);
if (this.isPrevDisabled) {
return;
}
let index = this.focusedImageIndex;
if (index === this.imageHistory.length - 1) {
this.setFocusedImage(this.imageHistory.length - 2, THUMBNAIL_CLICKED);
} else {
this.setSelectedByIndex(--index);
this.setFocusedImage(--index, THUMBNAIL_CLICKED);
}
},
isNextDisabled() {
let disabled = false;
let index = this.selectedImageIndex();
if (index === -1 || index === this.imageHistory.length - 1) {
disabled = true;
}
return disabled;
startDrag(e) {
e.preventDefault();
e.stopPropagation();
},
isPrevDisabled() {
let disabled = false;
let index = this.selectedImageIndex();
arrowDownHandler(event) {
let key = event.keyCode;
if (index === 0 || this.imageHistory.length < 2) {
disabled = true;
if (this.isLeftOrRightArrowKey(key)) {
this.arrowDown = true;
window.clearTimeout(this.arrowDownDelayTimeout);
this.arrowDownDelayTimeout = window.setTimeout(() => {
this.arrowKeyScroll(this.directionByKey(key));
}, ARROW_DOWN_DELAY_CHECK_MS);
}
},
arrowUpHandler(event) {
let key = event.keyCode;
window.clearTimeout(this.arrowDownDelayTimeout);
if (this.isLeftOrRightArrowKey(key)) {
this.arrowDown = false;
let direction = this.directionByKey(key);
this[direction + 'Image']();
}
},
arrowKeyScroll(direction) {
if (this.arrowDown) {
this.arrowKeyScrolling = true;
this[direction + 'Image']();
setTimeout(() => {
this.arrowKeyScroll(direction);
}, ARROW_SCROLL_RATE_MS);
} else {
window.clearTimeout(this.arrowDownDelayTimeout);
this.arrowKeyScrolling = false;
this.scrollToFocused();
}
},
directionByKey(keyCode) {
let direction;
if (keyCode === ARROW_LEFT) {
direction = 'prev';
}
return disabled;
if (keyCode === ARROW_RIGHT) {
direction = 'next';
}
return direction;
},
isLeftOrRightArrowKey(keyCode) {
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
}
}
};

View File

@@ -1,8 +1,12 @@
.c-imagery {
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: hidden;
height: 100%;
&:focus {
outline: none;
}
> * + * {
margin-top: $interiorMargin;
@@ -15,24 +19,75 @@
}
&__main-image {
background-position: center;
background-repeat: no-repeat;
background-size: contain;
height: 100%;
&__bg {
background-color: $colorPlotBg;
border: 1px solid transparent;
flex: 1 1 auto;
&.unnsynced{
@include sUnsynced();
&.unnsynced{
@include sUnsynced();
}
}
&__image {
@include abs(); // Safari fix
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
}
&__control-bar {
padding: 5px 0 0 0;
&__control-bar,
&__time {
display: flex;
align-items: center;
align-items: baseline;
> * + * {
margin-left: $interiorMarginSm;
}
}
&__control-bar {
margin-top: 2px;
padding: $interiorMarginSm 0;
justify-content: space-between;
}
&__time {
flex: 0 1 auto;
overflow: hidden;
}
&__timestamp,
&__age {
@include ellipsize();
flex: 0 1 auto;
}
&__timestamp {
flex: 1 1 auto;
flex-shrink: 10;
}
&__age {
border-radius: $controlCr;
display: flex;
flex-shrink: 0;
align-items: baseline;
padding: 1px $interiorMarginSm;
&:before {
opacity: 0.5;
margin-right: $interiorMarginSm;
}
}
&--new {
// New imagery
$bgColor: $colorOk;
background: rgba($bgColor, 0.5);
@include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
}
&__thumbs-wrapper {
@@ -91,11 +146,6 @@
}
}
.s-image-main {
background-color: $colorPlotBg;
border: 1px solid transparent;
}
/*************************************** IMAGERY LOCAL CONTROLS*/
.c-imagery {
.h-local-controls--overlay-content {
@@ -105,7 +155,7 @@
background: $colorLocalControlOvrBg;
border-radius: $basicCr;
max-width: 200px;
min-width: 100px;
min-width: 70px;
width: 35%;
align-items: center;
padding: $interiorMargin $interiorMarginLg;
@@ -126,6 +176,7 @@
&__lc {
&__reset-btn {
$bc: $scrollbarTrackColorBg;
&:before,
&:after {
border-right: 1px solid $bc;
@@ -148,9 +199,51 @@
}
}
.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 */
.c-button.pause-play {
// Pause icon set by default in markup
justify-self: end;
&.is-paused {
background: $colorPausedBg !important;
color: $colorPausedFg;
@@ -162,14 +255,13 @@
}
.c-imagery__prev-next-buttons {
//background: rgba(deeppink, 0.2);
display: flex;
width: 100%;
justify-content: space-between;
pointer-events: none;
position: absolute;
top: 50%;
transform: translateY(-50%);
transform: translateY(-75%);
.c-nav {
pointer-events: all;

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

@@ -112,9 +112,9 @@ import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
import { throttle } from 'lodash';
import objectUtils from 'objectUtils';
const DEFAULT_CLASS = 'is-notebook-default';
import { throttle } from 'lodash';
export default {
inject: ['openmct', 'domainObject', 'snapshotContainer'],
@@ -197,15 +197,6 @@ export default {
});
},
methods: {
addDefaultClass() {
const classList = this.internalDomainObject.classList || [];
if (classList.includes(DEFAULT_CLASS)) {
return;
}
classList.push(DEFAULT_CLASS);
this.mutateObject('classList', classList);
},
changeSelectedSection({ sectionId, pageId }) {
const sections = this.sections.map(s => {
s.isSelected = false;
@@ -425,14 +416,7 @@ export default {
return;
}
const classList = domainObject.classList || [];
const index = classList.indexOf(DEFAULT_CLASS);
if (!classList.length || index < 0) {
return;
}
classList.splice(index, 1);
this.openmct.objects.mutate(domainObject, 'classList', classList);
this.openmct.status.delete(domainObject.identifier);
},
searchItem(input) {
this.search = input;
@@ -442,11 +426,20 @@ export default {
},
async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.openmct, notebookStorage);
this.addDefaultClass();
this.defaultSectionId = notebookStorage.section.id;
this.defaultPageId = notebookStorage.page.id;
if (!defaultNotebookObject) {
setDefaultNotebook(this.openmct, notebookStorage);
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.openmct, notebookStorage);
}
if (this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
this.defaultSectionId = notebookStorage.section.id;
}
if (this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
this.defaultPageId = notebookStorage.page.id;
}
},
updateDefaultNotebookPage(pages, id) {
if (!id) {

View File

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

View File

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

View File

@@ -50,6 +50,7 @@ export default {
},
mounted() {
this.notebookSnapshot = new Snapshot(this.openmct);
this.setDefaultNotebookStatus();
},
methods: {
showMenu(event) {
@@ -92,7 +93,7 @@ export default {
const bounds = this.openmct.time.bounds();
const link = !this.ignoreLink
? window.location.href
? window.location.hash
: null;
const objectPath = this.objectPath || this.openmct.router.path;
@@ -105,6 +106,15 @@ export default {
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

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

View File

@@ -49,7 +49,7 @@ export default class Snapshot {
.then(domainObject => {
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}`;
this._showNotification(msg);
});

View File

@@ -1,5 +1,6 @@
import objectLink from '../../../ui/mixins/object-link';
export const DEFAULT_CLASS = 'notebook-default';
const TIME_BOUNDS = {
START_BOUND: 'tc.startBound',
END_BOUND: 'tc.endBound',
@@ -102,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) {
return;
}
@@ -124,10 +125,11 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
defaultEntries.push({
id,
createdOn: date,
text: '',
text: entryText,
embeds
});
addDefaultClass(domainObject, openmct);
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
return id;
@@ -193,5 +195,10 @@ export function deleteNotebookEntries(openmct, domainObject, selectedSection, se
}
delete entries[selectedSection.id][selectedPage.id];
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
}
function addDefaultClass(domainObject, openmct) {
openmct.status.set(domainObject.identifier, DEFAULT_CLASS);
}

View File

@@ -60,7 +60,6 @@ export function setDefaultNotebookSection(section) {
notebookStorage.section = section;
saveDefaultNotebook(notebookStorage);
}
export function setDefaultNotebookPage(page) {

View File

@@ -86,7 +86,10 @@ export default class CouchObjectProvider {
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
}
this.objectQueue[key].updateRevision(response[REV]);
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
if (!this.objectQueue[key].pending) {
this.objectQueue[key].updateRevision(response[REV]);
}
return object;
} else {

View File

@@ -22,9 +22,10 @@
import CouchObjectProvider from './CouchObjectProvider';
const NAMESPACE = '';
const PERSISTENCE_SPACE = 'mct';
export default function CouchPlugin(url) {
return function install(openmct) {
openmct.objects.addProvider(NAMESPACE, new CouchObjectProvider(openmct, url, NAMESPACE));
openmct.objects.addProvider(PERSISTENCE_SPACE, new CouchObjectProvider(openmct, url, NAMESPACE));
};
}

View File

@@ -31,19 +31,18 @@ describe('the plugin', () => {
let element;
let child;
let provider;
let testSpace = 'testSpace';
let testPath = '/test/db';
let mockDomainObject;
beforeEach((done) => {
mockDomainObject = {
identifier: {
namespace: '',
namespace: 'mct',
key: 'some-value'
}
};
openmct = createOpenMct(false);
openmct.install(new CouchPlugin(testSpace, testPath));
openmct.install(new CouchPlugin(testPath));
element = document.createElement('div');
child = document.createElement('div');

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="plot-legend-item"
ng-class="{
'is-missing': series.domainObject.status === 'missing'
'is-status--missing': series.domainObject.status === 'missing'
}"
ng-repeat="series in series track by $index"
>
@@ -48,7 +48,7 @@
<span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }">
</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>
</div>
<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"
class="plot-legend-item"
ng-class="{
'is-missing': series.domainObject.status === 'missing'
'is-status--missing': series.domainObject.status === 'missing'
}"
>
<td class="plot-series-swatch-and-name">
<span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }">
</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>
</td>
@@ -188,15 +188,19 @@
ng-style="{
right: (100 * (max - tick.value) / interval) + '%',
height: '100%'
}">
</div>
}"
ng-show="plot.gridLines"
>
</div>
</mct-ticks>
<mct-ticks axis="yAxis">
<div class="gl-plot-hash hash-h"
<div class="gl-plot-hash hash-h"
ng-repeat="tick in ticks track by tick.value"
ng-style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }">
</div>
ng-style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
ng-show="plot.gridLines"
>
</div>
</mct-ticks>
<mct-chart config="config"

View File

@@ -22,16 +22,16 @@
<div ng-controller="PlotController as controller"
class="c-plot holder holder-plot has-control-bar">
<div class="c-control-bar" ng-show="!controller.hideExportButtons">
<span class="c-button-set c-button-set--strip-h">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
ng-click="controller.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
ng-click="controller.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
ng-click="controller.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
ng-click="controller.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
@@ -39,9 +39,14 @@
ng-click="controller.toggleCursorGuide($event)"
title="Toggle cursor guides">
</button>
<button class="c-button"
ng-class="{ 'icon-grid-on': controller.gridLines, 'icon-grid-off': !controller.gridLines }"
ng-click="controller.toggleGridLines($event)"
title="Toggle grid lines">
</button>
</div>
<div class="l-view-section">
<div class="l-view-section u-style-receiver js-style-receiver">
<div class="c-loading--overlay loading"
ng-show="!!pending"></div>
<mct-plot config="controller.config"

View File

@@ -22,25 +22,30 @@
<div ng-controller="StackedPlotController as stackedPlot"
class="c-plot c-plot--stacked holder holder-plot has-control-bar">
<div class="c-control-bar" ng-show="!stackedPlot.hideExportButtons">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
ng-click="stackedPlot.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
ng-click="stackedPlot.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
</button>
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
ng-click="stackedPlot.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
ng-click="stackedPlot.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
ng-class="{ 'is-active': stackedPlot.cursorGuide }"
ng-click="stackedPlot.toggleCursorGuide($event)"
title="Toggle cursor guides">
</button>
<button class="c-button"
ng-class="{ 'icon-grid-on': stackedPlot.gridLines, 'icon-grid-off': !stackedPlot.gridLines }"
ng-click="stackedPlot.toggleGridLines($event)"
title="Toggle grid lines">
</button>
</div>
<div class="l-view-section">
<div class="l-view-section u-style-receiver js-style-receiver">
<div class="c-loading--overlay loading"
ng-show="!!currentRequest.pending"></div>
<div class="gl-plot child-frame u-inspectable"

View File

@@ -96,7 +96,10 @@ define([
this.cursorGuideHorizontal = this.$element[0].querySelector('.js-cursor-guide--h');
this.cursorGuide = false;
this.gridLines = true;
this.listenTo(this.$scope, 'cursorguide', this.toggleCursorGuide, this);
this.listenTo(this.$scope, 'toggleGridLines', this.toggleGridLines, this);
this.listenTo(this.$scope, '$destroy', this.destroy, this);
this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this);
@@ -554,6 +557,10 @@ define([
this.cursorGuide = !this.cursorGuide;
};
MCTPlotController.prototype.toggleGridLines = function ($event) {
this.gridLines = !this.gridLines;
};
MCTPlotController.prototype.getXKeyOption = function (key) {
return this.$scope.xKeyOptions.find(option => option.key === key);
};

View File

@@ -60,6 +60,7 @@ define([
this.objectService = objectService;
this.exportImageService = exportImageService;
this.cursorGuide = false;
this.gridLines = true;
$scope.pending = 0;
@@ -331,6 +332,11 @@ define([
this.$scope.$broadcast('cursorguide', $event);
};
PlotController.prototype.toggleGridLines = function ($event) {
this.gridLines = !this.gridLines;
this.$scope.$broadcast('toggleGridLines', $event);
};
return PlotController;
});

View File

@@ -160,5 +160,10 @@ define([], function () {
this.$scope.$broadcast('cursorguide', $event);
};
StackedPlotController.prototype.toggleGridLines = function ($event) {
this.gridLines = !this.gridLines;
this.$scope.$broadcast('toggleGridLines', $event);
};
return StackedPlotController;
});

View File

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

View File

@@ -25,6 +25,7 @@ define([
'lodash',
'./collections/BoundedTableRowCollection',
'./collections/FilteredTableRowCollection',
'./TelemetryTableNameColumn',
'./TelemetryTableRow',
'./TelemetryTableColumn',
'./TelemetryTableUnitColumn',
@@ -34,6 +35,7 @@ define([
_,
BoundedTableRowCollection,
FilteredTableRowCollection,
TelemetryTableNameColumn,
TelemetryTableRow,
TelemetryTableColumn,
TelemetryTableUnitColumn,
@@ -71,6 +73,24 @@ define([
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() {
if (this.domainObject.type === 'table') {
this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters);
@@ -211,7 +231,13 @@ define([
addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
this.addNameColumn(telemetryObject, metadataValues);
metadataValues.forEach(metadatum => {
if (metadatum.key === 'name') {
return;
}
let column = this.createColumn(metadatum);
this.configuration.addSingleColumnForObject(telemetryObject, column);
// 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

@@ -70,16 +70,16 @@ define([
data() {
return {
isEditing: editMode,
markingProp
markingProp,
view
};
},
provide: {
openmct,
table,
objectPath,
view
objectPath
},
template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp"/>'
template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp" :view="view"/>'
});
},
onEditModeChange(editMode) {

View File

@@ -36,7 +36,7 @@ let exportMarkedRows = {
description: "Export marked rows as CSV",
cssClass: 'icon-download labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().exportMarkedDataAsCSV();
viewProvider.getViewContext().exportMarkedRows();
},
group: 'view'
};

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

@@ -0,0 +1,29 @@
.c-table-indicator {
display: flex;
align-items: center;
font-size: 0.9em;
overflow: hidden;
&__elem {
@include ellipsize();
flex: 0 1 auto;
padding: 2px;
text-transform: uppercase;
> * {
//display: contents;
}
}
&__counts {
//background: rgba(deeppink, 0.1);
display: flex;
flex: 1 1 auto;
justify-content: flex-end;
overflow: hidden;
> * {
margin-left: $interiorMargin;
}
}
}

View File

@@ -1,18 +1,41 @@
<template>
<div
v-if="filterNames.length > 0"
:title="title"
class="c-filter-indication"
:class="{ 'c-filter-indication--mixed': hasMixedFilters }"
class="c-table-indicator"
:class="{ 'is-filtering': filterNames.length > 0 }"
>
<span class="c-filter-indication__mixed">{{ label }}</span>
<span
v-for="(name, index) in filterNames"
:key="index"
class="c-filter-indication__label"
<div
v-if="filterNames.length > 0"
class="c-table-indicator__filter c-table-indicator__elem c-filter-indication"
:class="{ 'c-filter-indication--mixed': hasMixedFilters }"
:title="title"
>
{{ name }}
</span>
<span class="c-filter-indication__mixed">{{ label }}</span>
<span
v-for="(name, index) in filterNames"
:key="index"
class="c-filter-indication__label"
>
{{ name }}
</span>
</div>
<div class="c-table-indicator__counts">
<span
:title="totalRows + ' rows visible after any filtering'"
class="c-table-indicator__elem c-table-indicator__row-count"
>
{{ totalRows }} Rows
</span>
<span
v-if="markedRows"
class="c-table-indicator__elem c-table-indicator__marked-count"
:title="markedRows + ' rows selected'"
>
{{ markedRows }} Marked
</span>
</div>
</div>
</template>
@@ -27,6 +50,16 @@ const USE_GLOBAL = 'useGlobal';
export default {
inject: ['openmct', 'table'],
props: {
markedRows: {
type: Number,
default: 0
},
totalRows: {
type: Number,
default: 0
}
},
data() {
return {
filterNames: [],

View File

@@ -9,6 +9,9 @@
.c-telemetry-table {
// Table that displays telemetry in a scrolling body area
@include fontAndSize();
display: flex;
flex-flow: column nowrap;
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
align-items: stretch;
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 & {
pointer-events: none;
@@ -150,6 +153,41 @@
white-space: nowrap;
}
}
&__sizing-tr {
// A row element used to determine sizing of rows based on font size
visibility: hidden;
pointer-events: none;
}
&__footer {
$pt: 2px;
border-top: 1px solid $colorInteriorBorder;
margin-top: $interiorMargin;
padding: $pt 0;
overflow: hidden;
transition: all 250ms;
&:not(.is-filtering) {
.c-frame & {
height: 0;
padding: 0;
visibility: hidden;
}
}
}
.c-frame & {
// target .c-frame .c-telemetry-table {}
$pt: 2px;
&:hover {
.c-telemetry-table__footer:not(.is-filtering) {
height: $pt + 16px;
padding: initial;
visibility: visible;
}
}
}
}
/******************************* SPECIFIC CASE WRAPPERS */

View File

@@ -23,6 +23,77 @@
<div class="c-table-wrapper"
:class="{ 'is-paused': paused }"
>
<div v-if="enableLegacyToolbar"
class="c-table-control-bar c-control-bar"
>
<button
v-if="allowExport"
v-show="!markedRows.length"
class="c-button icon-download labeled"
title="Export this view's data"
@click="exportAllDataAsCSV()"
>
<span class="c-button__label">Export Table Data</span>
</button>
<button
v-if="allowExport"
v-show="markedRows.length"
class="c-button icon-download labeled"
title="Export marked rows as CSV"
@click="exportMarkedDataAsCSV()"
>
<span class="c-button__label">Export Marked Rows</span>
</button>
<button
v-show="markedRows.length"
class="c-button icon-x labeled"
title="Unmark all rows"
@click="unmarkAllRows()"
>
<span class="c-button__label">Unmark All Rows</span>
</button>
<div
v-if="marking.enable"
class="c-separator"
></div>
<button
v-if="marking.enable"
class="c-button icon-pause pause-play labeled"
:class=" paused ? 'icon-play is-paused' : 'icon-pause'"
:title="paused ? 'Continue real-time data flow' : 'Pause real-time data flow'"
@click="togglePauseByButton()"
>
<span class="c-button__label">
{{ paused ? 'Play' : 'Pause' }}
</span>
</button>
<template v-if="!isEditing">
<div
class="c-separator"
>
</div>
<button
v-if="isAutosizeEnabled"
class="c-button icon-arrows-right-left labeled"
title="Increase column widths to fit currently available data."
@click="recalculateColumnWidths"
>
<span class="c-button__label">Expand Columns</span>
</button>
<button
v-else
class="c-button icon-expand labeled"
title="Automatically size columns to fit the table into the available space."
@click="autosizeColumns"
>
<span class="c-button__label">Autosize Columns</span>
</button>
</template>
<slot name="buttons"></slot>
</div>
<!-- alternate controlbar start -->
<div v-if="marking.useAlternateControlBar"
class="c-table-control-bar c-control-bar"
@@ -52,7 +123,7 @@
<!-- alternate controlbar end -->
<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="{
'loading': loading,
'is-paused' : paused
@@ -161,6 +232,10 @@
class="c-telemetry-table__sizing js-telemetry-table__sizing"
:style="sizingTableWidth"
>
<sizing-row
:is-editing="isEditing"
@change-height="setRowHeight"
/>
<tr>
<template v-for="(title, key) in headers">
<th
@@ -180,7 +255,11 @@
:object-path="objectPath"
/>
</table>
<telemetry-filter-indicator />
<table-footer-indicator
class="c-telemetry-table__footer"
:marked-rows="markedRows.length"
:total-rows="totalNumberOfRows"
/>
</div>
</div><!-- closes c-table-wrapper -->
</template>
@@ -189,10 +268,11 @@
import TelemetryTableRow from './table-row.vue';
import search from '../../../ui/components/search.vue';
import TableColumnHeader from './table-column-header.vue';
import TelemetryFilterIndicator from './TelemetryFilterIndicator.vue';
import TableFooterIndicator from './table-footer-indicator.vue';
import CSVExporter from '../../../exporters/CSVExporter.js';
import _ from 'lodash';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import SizingRow from './sizing-row.vue';
const VISIBLE_ROW_COUNT = 100;
const ROW_HEIGHT = 17;
@@ -204,10 +284,11 @@ export default {
TelemetryTableRow,
TableColumnHeader,
search,
TelemetryFilterIndicator,
ToggleSwitch
TableFooterIndicator,
ToggleSwitch,
SizingRow
},
inject: ['table', 'openmct', 'objectPath', 'view'],
inject: ['table', 'openmct', 'objectPath'],
props: {
isEditing: {
type: Boolean,
@@ -218,12 +299,12 @@ export default {
default: true
},
allowFiltering: {
'type': Boolean,
'default': true
type: Boolean,
default: true
},
allowSorting: {
'type': Boolean,
'default': true
type: Boolean,
default: true
},
marking: {
type: Object,
@@ -236,6 +317,17 @@ export default {
rowNamePlural: ""
};
}
},
enableLegacyToolbar: {
type: Boolean,
default: false
},
view: {
type: Object,
required: false,
default() {
return {};
}
}
},
data() {
@@ -269,7 +361,8 @@ export default {
paused: false,
markedRows: [],
isShowingMarkedRowsOnly: false,
hideHeaders: configuration.hideHeaders
hideHeaders: configuration.hideHeaders,
totalNumberOfRows: 0
};
},
computed: {
@@ -311,32 +404,38 @@ export default {
handler(newVal, oldVal) {
this.$emit('marked-rows-updated', newVal, oldVal);
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']);
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 (newVal) {
this.viewActionsCollection.hide(['pause-data']);
this.viewActionsCollection.show(['play-data']);
} else {
this.viewActionsCollection.hide(['play-data']);
this.viewActionsCollection.show(['pause-data']);
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 (newVal) {
this.viewActionsCollection.show(['expand-columns']);
this.viewActionsCollection.hide(['autosize-columns']);
} else {
this.viewActionsCollection.show(['autosize-columns']);
this.viewActionsCollection.hide(['expand-columns']);
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']);
}
}
}
}
@@ -350,8 +449,10 @@ export default {
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
this.scroll = _.throttle(this.scroll, 100);
this.viewActionsCollection = this.openmct.actions.get(this.objectPath, this.view);
this.initializeViewActions();
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-removed', this.removeObject);
@@ -409,6 +510,8 @@ export default {
let filteredRows = this.table.filteredRows.getRows();
let filteredRowsLength = filteredRows.length;
this.totalNumberOfRows = filteredRowsLength;
if (filteredRowsLength < VISIBLE_ROW_COUNT) {
end = filteredRowsLength;
} else {
@@ -457,7 +560,7 @@ export default {
let columnWidths = {};
let totalWidth = 0;
let headerKeys = Object.keys(this.headers);
let sizingTableRow = this.sizingTable.children[0];
let sizingTableRow = this.sizingTable.children[1];
let sizingCells = sizingTableRow.children;
headerKeys.forEach((headerKey, headerIndex, array) => {
@@ -886,6 +989,12 @@ export default {
this.viewActionsCollection.show(['autosize-columns']);
this.viewActionsCollection.hide(['expand-columns']);
}
},
setRowHeight(height) {
this.rowHeight = height;
this.setHeight();
this.calculateTableSize();
this.clearRowsAndRerender();
}
}
};

View File

@@ -1,37 +0,0 @@
.c-filter-indication {
@include userSelectNone();
background: $colorFilterBg;
color: $colorFilterFg;
display: flex;
align-items: center;
font-size: 0.9em;
margin-top: $interiorMarginSm;
padding: 2px;
text-transform: uppercase;
&:before {
font-family: symbolsfont-12px;
content: $glyph-icon-filter;
display: block;
font-size: 12px;
margin-right: $interiorMarginSm;
}
&__mixed {
margin-right: $interiorMarginSm;
}
&--mixed {
.c-filter-indication__mixed {
font-style: italic;
}
}
&__label {
+ .c-filter-indication__label {
&:before {
content: ',';
}
}
}
}

View File

@@ -185,10 +185,11 @@ describe("the plugin", () => {
it("Renders a column for every item in telemetry metadata", () => {
let headers = element.querySelectorAll('span.c-telemetry-table__headers__label');
expect(headers.length).toBe(3);
expect(headers[0].innerText).toBe('Time');
expect(headers[1].innerText).toBe('Some attribute');
expect(headers[2].innerText).toBe('Another attribute');
expect(headers.length).toBe(4);
expect(headers[0].innerText).toBe('Name');
expect(headers[1].innerText).toBe('Time');
expect(headers[2].innerText).toBe('Some attribute');
expect(headers[3].innerText).toBe('Another attribute');
});
it("Supports column reordering via drag and drop", () => {

View File

@@ -1,21 +1,21 @@
@import "~styles/vendor/normalize-min";
@import "~styles/constants";
@import "~styles/constants-mobile.scss";
@import "../../styles/vendor/normalize-min";
@import "../../styles/constants";
@import "../../styles/constants-mobile.scss";
@import "~styles/constants-espresso";
@import "../../styles/constants-espresso";
@import "~styles/mixins";
@import "~styles/animations";
@import "~styles/about";
@import "~styles/glyphs";
@import "~styles/global";
@import "~styles/status";
@import "~styles/controls";
@import "~styles/forms";
@import "~styles/table";
@import "~styles/legacy";
@import "~styles/legacy-plots";
@import "~styles/plotly";
@import "~styles/legacy-messages";
@import "../../styles/mixins";
@import "../../styles/animations";
@import "../../styles/about";
@import "../../styles/glyphs";
@import "../../styles/global";
@import "../../styles/status";
@import "../../styles/controls";
@import "../../styles/forms";
@import "../../styles/table";
@import "../../styles/legacy";
@import "../../styles/legacy-plots";
@import "../../styles/plotly";
@import "../../styles/legacy-messages";
@import "~styles/vue-styles.scss";
@import "../../styles/vue-styles.scss";

View File

@@ -1,21 +1,21 @@
@import "~styles/vendor/normalize-min";
@import "~styles/constants";
@import "~styles/constants-mobile.scss";
@import "../../styles/vendor/normalize-min";
@import "../../styles/constants";
@import "../../styles/constants-mobile.scss";
@import "~styles/constants-maelstrom";
@import "../../styles/constants-maelstrom";
@import "~styles/mixins";
@import "~styles/animations";
@import "~styles/about";
@import "~styles/glyphs";
@import "~styles/global";
@import "~styles/status";
@import "~styles/controls";
@import "~styles/forms";
@import "~styles/table";
@import "~styles/legacy";
@import "~styles/legacy-plots";
@import "~styles/plotly";
@import "~styles/legacy-messages";
@import "../../styles/mixins";
@import "../../styles/animations";
@import "../../styles/about";
@import "../../styles/glyphs";
@import "../../styles/global";
@import "../../styles/status";
@import "../../styles/controls";
@import "../../styles/forms";
@import "../../styles/table";
@import "../../styles/legacy";
@import "../../styles/legacy-plots";
@import "../../styles/plotly";
@import "../../styles/legacy-messages";
@import "~styles/vue-styles.scss";
@import "../../styles/vue-styles.scss";

View File

@@ -1,21 +1,21 @@
@import "~styles/vendor/normalize-min";
@import "~styles/constants";
@import "~styles/constants-mobile.scss";
@import "../../styles/vendor/normalize-min";
@import "../../styles/constants";
@import "../../styles/constants-mobile.scss";
@import "~styles/constants-snow";
@import "../../styles/constants-snow";
@import "~styles/mixins";
@import "~styles/animations";
@import "~styles/about";
@import "~styles/glyphs";
@import "~styles/global";
@import "~styles/status";
@import "~styles/controls";
@import "~styles/forms";
@import "~styles/table";
@import "~styles/legacy";
@import "~styles/legacy-plots";
@import "~styles/plotly";
@import "~styles/legacy-messages";
@import "../../styles/mixins";
@import "../../styles/animations";
@import "../../styles/about";
@import "../../styles/glyphs";
@import "../../styles/global";
@import "../../styles/status";
@import "../../styles/controls";
@import "../../styles/forms";
@import "../../styles/table";
@import "../../styles/legacy";
@import "../../styles/legacy-plots";
@import "../../styles/plotly";
@import "../../styles/legacy-messages";
@import "~styles/vue-styles.scss";
@import "../../styles/vue-styles.scss";

View File

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

View File

@@ -66,7 +66,9 @@
<script>
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;
export default {
@@ -77,72 +79,115 @@ export default {
type: Object,
required: true
},
offsets: {
type: Object,
required: false,
default: () => {}
},
timeSystem: {
type: Object,
required: true
},
mode: {
type: String,
required: true
}
},
data() {
return {
/**
* previous bounds entries available for easy re-use
* @history array of timespans
* @realtimeHistory array of timespans
* @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: []
};
},
computed: {
currentHistory() {
return this.mode + 'History';
},
isFixed() {
return this.openmct.time.clock() === undefined;
},
hasHistoryPresets() {
return this.timeSystem.isUTCBased && this.presets.length;
},
historyForCurrentTimeSystem() {
const history = this.history[this.timeSystem.key];
const history = this[this.currentHistory][this.timeSystem.key];
return history;
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (this.mode !== 'fixed') {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
return key;
}
},
watch: {
bounds: {
handler() {
// only for fixed time since we track offsets for realtime
if (this.isFixed) {
this.addTimespan();
}
},
deep: true
},
offsets: {
handler() {
this.addTimespan();
},
deep: true
},
timeSystem: {
handler() {
handler(ts) {
this.loadConfiguration();
this.addTimespan();
},
deep: true
},
mode: function () {
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
this.loadConfiguration();
}
},
mounted() {
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
},
methods: {
getHistoryFromLocalStorage() {
const localStorageHistory = localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY);
const localStorageHistory = localStorage.getItem(this.storageKey);
const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;
return history;
this[this.currentHistory] = history;
},
initializeHistoryIfNoHistory() {
if (!this.history) {
this.history = {};
if (!this[this.currentHistory]) {
this[this.currentHistory] = {};
this.persistHistoryToLocalStorage();
}
},
persistHistoryToLocalStorage() {
localStorage.setItem(LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(this.history));
localStorage.setItem(this.storageKey, JSON.stringify(this[this.currentHistory]));
},
addTimespan() {
const key = this.timeSystem.key;
let [...currentHistory] = this.history[key] || [];
let [...currentHistory] = this[this.currentHistory][key] || [];
const timespan = {
start: this.bounds.start,
end: this.bounds.end
start: this.isFixed ? this.bounds.start : this.offsets.start,
end: this.isFixed ? this.bounds.end : this.offsets.end
};
let self = this;
@@ -160,20 +205,24 @@ export default {
}
currentHistory.unshift(timespan);
this.history[key] = currentHistory;
this.$set(this[this.currentHistory], key, currentHistory);
this.persistHistoryToLocalStorage();
},
selectTimespan(timespan) {
this.openmct.time.bounds(timespan);
if (this.isFixed) {
this.openmct.time.bounds(timespan);
} else {
this.openmct.time.clockOffsets(timespan);
}
},
selectPresetBounds(bounds) {
const start = typeof bounds.start === 'function' ? bounds.start() : bounds.start;
const end = typeof bounds.end === 'function' ? bounds.end() : bounds.end;
this.selectTimespan({
start: start,
end: end
start,
end
});
},
loadConfiguration() {
@@ -184,7 +233,9 @@ export default {
this.records = this.loadRecords(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 : [];
return presets;
@@ -196,11 +247,24 @@ export default {
return records;
},
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({
format: this.timeSystem.timeFormat
format: format
}).formatter;
return formatter.format(time);
return (isNegativeOffset ? '-' : '') + formatter.format(time);
}
}
};

View File

@@ -173,6 +173,7 @@ $editUIAreaShdw: $editUIAreaBaseColor 0 0 0 2px; // Edit area s-selected-parent
$editUIAreaShdwSelected: $editUIAreaBaseColor 0 0 0 3px; // Edit area s-selected
$editUIGridColorBg: rgba($editUIBaseColor, 0.2); // Background of layout editing area
$editUIGridColorFg: rgba(#000, 0.1); // Grid lines in layout editing area
$editDimensionsColor: #6a5ea6;
$editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-selected frames in a layout; move-bar on frame hover
$editFrameBorder: 1px dotted $editFrameColor;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects

View File

@@ -177,6 +177,7 @@ $editUIAreaShdw: $editUIAreaBaseColor 0 0 0 2px; // Edit area s-selected-parent
$editUIAreaShdwSelected: $editUIAreaBaseColor 0 0 0 3px; // Edit area s-selected
$editUIGridColorBg: rgba($editUIBaseColor, 0.2); // Background of layout editing area
$editUIGridColorFg: rgba(#000, 0.1); // Grid lines in layout editing area
$editDimensionsColor: #6a5ea6;
$editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-selected frames in a layout; move-bar on frame hover
$editFrameBorder: 1px dotted $editFrameColor;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects

View File

@@ -173,6 +173,7 @@ $editUIAreaShdw: $editUIAreaBaseColor 0 0 0 2px; // Edit area s-selected-parent
$editUIAreaShdwSelected: $editUIAreaBaseColor 0 0 0 3px; // Edit area s-selected
$editUIGridColorBg: rgba($editUIBaseColor, 0.2); // Background of layout editing area
$editUIGridColorFg: rgba($editUIBaseColor, 0.3); // Grid lines in layout editing area
$editDimensionsColor: #d7aeff;
$editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-selected frames in a layout; move-bar on frame hover
$editFrameBorder: 1px dotted $editFrameColor;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects

View File

@@ -103,6 +103,8 @@ $colorProgressBarHolder: rgba(black, 0.1);
$colorProgressBar: #0085ad;
$progressAnimW: 500px;
$progressBarMinH: 6px;
/************************** FONT STYLING */
$listFontSizes: 8,9,10,11,12,13,14,16,18,20,24,28,32,36,42,48,72,96,128;
/************************** GLYPH CHAR UNICODES */
$glyph-icon-alert-rect: '\e900';

View File

@@ -113,6 +113,10 @@ button {
}
}
.c-icon-button--disabled {
@include cClickIconButtonLayout();
}
.c-icon-link {
&:before {
// Icon
@@ -121,8 +125,8 @@ button {
}
.c-icon-button {
&__label {
margin-left: $interiorMargin;
[class*='label'] {
opacity: 0.6;
}
&--mixed {
@@ -300,19 +304,7 @@ input[type=number]::-webkit-outer-spin-button {
&-inline,
&--inline {
// A text input or contenteditable element that indicates edit affordance on hover and looks like an input on focus
@include reactive-input($bg: transparent);
box-shadow: none;
display: block !important;
min-width: 0;
padding-left: 0;
padding-right: 0;
overflow: hidden;
transition: all 250ms ease;
white-space: nowrap;
&:not(:focus) {
text-overflow: ellipsis;
}
@include inlineInput;
&:hover,
&:focus {
@@ -651,6 +643,7 @@ select {
}
&__item-none {
@include userSelectNone();
flex: 0 0 auto;
display: flex;
align-items: center;
@@ -739,8 +732,17 @@ select {
}
.c-toolbar {
> * + * {
margin-left: 2px;
display: flex;
align-items: center;
justify-content: space-between;
> * {
// First level items
display: flex;
> * + * {
margin-left: 2px;
}
}
&__separator {
@@ -763,6 +765,12 @@ select {
color: $editUIBaseColorFg !important;
}
&--menu {
$p: 4px;
padding-top: $p;
padding-bottom: $p;
}
&--swatched {
padding-bottom: floor($pTB / 2);
width: 2em; // Standardize the width
@@ -844,8 +852,41 @@ select {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: space-between;
> * + * { margin-left: $interiorMargin; }
&__controls {
// Holds thumb, icon buttons
display: flex;
flex: 1 0 auto;
> * + * { margin-left: $interiorMargin; }
}
&__button-save,
&__button-delete {
// Holds save and delete buttons accordingly
flex: 0 0 auto;
}
&--saved {
border-radius: $controlCr;
padding: $interiorMargin !important;
cursor: pointer;
@include hover {
background: rgba($editUIBaseColorHov, 0.3);
}
.c-style__controls {
[class*='button'] {
pointer-events: none;
&:before {
opacity: $controlDisabledOpacity;
}
}
}
}
}
.c-style-thumb {
@@ -854,7 +895,9 @@ select {
border-radius: $basicCr;
box-shadow: rgba($colorBodyFg, 0.4) 0 0 3px;
flex: 0 0 auto;
padding: $interiorMargin $interiorMarginLg;
padding: $interiorMargin;
text-align: center;
width: 50px;
&--mixed {
@include mixedBg();
@@ -869,20 +912,33 @@ select {
/******************************************************** SLIDERS AND RANGE */
@mixin sliderKnobRound() {
$h: 12px;
@mixin sliderKnobRound($h: 12px) {
@include themedButton();
cursor: pointer;
width: $h;
height: $h;
border-radius: 50% !important;
transform: translateY(-42%);
}
@mixin sliderTrack($bg: $scrollbarTrackColorBg, $knobH: 12px, $trackH: 3px) {
border-radius: 2px;
$breakPointPx: floor(($knobH - $trackH) / 2);
$bp1: $breakPointPx;
$bp2: $breakPointPx + $trackH;
box-sizing: border-box;
// For cross-browser compatibility, the track needs to be the same height as the knob.
height: $knobH;
// Gradient visually adds a horizontal line smaller than the knob
background: linear-gradient(0deg, rgba($bg,0) $bp1, $bg $bp1, $bg $bp2, rgba($bg,0) $bp2);
}
input[type="range"] {
// HTML5 range inputs
$knobH: 11px;
$trackH: 3px;
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
background: transparent; /* Otherwise white in Chrome */
&:focus {
outline: none; /* Removes the blue border. */
}
@@ -890,28 +946,26 @@ input[type="range"] {
// Thumb
&::-webkit-slider-thumb {
-webkit-appearance: none;
@include sliderKnobRound();
@include sliderKnobRound($knobH);
}
&::-moz-range-thumb {
border: none;
@include sliderKnobRound();
@include sliderKnobRound($knobH);
}
&::-ms-thumb {
border: none;
@include sliderKnobRound();
@include sliderKnobRound($knobH);
}
// Track
&::-webkit-slider-runnable-track {
width: 100%;
height: 3px;
@include sliderTrack();
@include sliderTrack($knobH: $knobH, $trackH: $trackH);
}
&::-moz-range-track {
width: 100%;
height: 3px;
@include sliderTrack();
@include sliderTrack($knobH: $knobH, $trackH: $trackH);
}
}

View File

@@ -96,6 +96,37 @@ body.desktop {
}
}
/******************************************************** FONTS */
@mixin fontAndSize() {
@each $size in $listFontSizes {
&[data-font-size="#{$size}"] {
font-size: #{$size}px;
// Set row heights in telemetry tables
tr {
min-height: #{$size + ($tabularTdPadTB * 2)};
}
}
}
&[data-font*="bold"] {
font-weight: bold;
}
&[data-font*="narrow"] {
font-family: 'Arial Narrow', sans-serif;
}
&[data-font*="monospace"] {
font-family: 'Andale Mono', sans-serif;
}
}
.u-style-receiver {
@include fontAndSize();
}
/******************************************************** HTML ENTITIES */
a {
color: $colorA;
@@ -227,7 +258,7 @@ body.desktop .has-local-controls {
}
/******************************************************** STATES */
@mixin spinner($b: 5px, $c: $colorKey) {
@mixin spinner($b: 5, $c: $colorKey) {
animation-name: rotation-centered;
animation-duration: 0.5s;
animation-iteration-count: infinite;
@@ -277,7 +308,7 @@ body.desktop .has-local-controls {
}
&.c-tree__item {
$d: $waitSpinnerTreeD;
$spinnerL: 19px + $d/2;
$spinnerL: 19 + $d/2;
display: flex;
align-items: center;

View File

@@ -59,12 +59,12 @@ mct-plot {
}
/*********************** MISSING ITEM INDICATORS */
.is-missing__indicator {
.is-status__indicator {
display: none;
}
.is-missing {
.is-status--missing {
@include isMissing();
.is-missing__indicator {
.is-status__indicator {
font-size: 0.8em;
}
}

View File

@@ -52,6 +52,7 @@
$ctrlW: 22px;
&__controls {
font-size: 1rem !important;
margin-right: 0;
min-width: 0;
overflow: hidden;
@@ -62,7 +63,7 @@
}
&__direction {
font-size: 0.9em;
font-size: 0.9rem !important;
margin-right: $interiorMargin;
}

View File

@@ -50,6 +50,18 @@
}
/************************** EFFECTS */
@mixin flash($animName: flash, $dur: 500ms, $dir: alternate, $iter: 20, $prop: background, $valStart: rgba($colorOk, 1), $valEnd: rgba($colorOk, 0)) {
@keyframes #{$animName} {
0% { #{$prop}: $valStart; }
100% { #{$prop}: $valEnd; }
}
animation-name: $animName;
animation-duration: $dur;
animation-direction: $dir;
animation-iteration-count: $iter;
animation-timing-function: ease-out;
}
@mixin mixedBg() {
$c1: nth($mixedSettingBg, 1);
$c2: nth($mixedSettingBg, 2);
@@ -117,31 +129,45 @@
}
}
@mixin isMissing($absPos: false) {
@mixin isStatus($absPos: false) {
// Supports CSS classing as follows:
// is-status--missing, is-status--suspect, etc.
// Common styles to be applied to tree items, object labels, grid and list item views
//opacity: 0.7;
//pointer-events: none; // Don't think we can do this, as disables title hover on icon element
.is-missing__indicator {
.is-status__indicator {
display: none ;
text-shadow: $colorBodyBg 0 0 2px;
color: $colorAlert;
font-family: symbolsfont;
&:before {
content: $glyph-icon-alert-triangle;
&[class^='is-status'] .is-status__indicator,
[class^='is-status'] .is-status__indicator {
display: block !important;
}
}
@if $absPos {
.is-missing__indicator {
@if $absPos {
display: block;
position: absolute;
z-index: 3;
}
}
}
&.is-missing .is-missing__indicator,
.is-missing .is-missing__indicator { display: block !important; }
@mixin isMissing($absPos: false) {
@include isStatus($absPos);
.is-status__indicator:before {
color: $colorAlert;
content: $glyph-icon-alert-triangle;
}
}
@mixin isSuspect($absPos: false) {
@include isStatus($absPos);
.is-status__indicator:before {
color: $colorWarningLo;
content: $glyph-icon-alert-rect;
}
}
@mixin bgDiagonalStripes($c: yellow, $a: 0.1, $d: 40px) {
@@ -368,11 +394,21 @@
&:focus {
box-shadow: $shdwInputFoc;
}
}
@include hover() {
&:not(:focus) {
box-shadow: $shdwInputHov;
}
@mixin inlineInput() {
@include reactive-input($bg: transparent);
box-shadow: none;
display: block !important;
min-width: 0;
padding-left: 0;
padding-right: 0;
overflow: hidden;
transition: all 250ms ease;
white-space: nowrap;
&:not(:focus) {
text-overflow: ellipsis;
}
}
@@ -514,7 +550,7 @@
}
&[class*="--major"] {
color: $colorKey;
color: $colorBtnMajorBg !important;
}
}

View File

@@ -2,7 +2,7 @@
"metadata": {
"name": "Open MCT Symbols 16px",
"lastOpened": 0,
"created": 1597943624771
"created": 1602779919972
},
"iconSets": [
{
@@ -752,7 +752,7 @@
"tempChar": ""
},
{
"order": 114,
"order": 194,
"id": 4,
"name": "icon-font-size",
"prevSize": 24,
@@ -2718,16 +2718,25 @@
{
"id": 4,
"paths": [
"M842.841 380.048h-120.956l-52.382 139.676 52.918 141.12 59.942-159.84 62.361 166.314h-119.884l34.019 90.717h119.884l39.695 105.836h105.836l-181.434-483.823z",
"M263.903 160.129l-263.903 703.742h153.944l57.729-153.944h280.397l57.729 153.944h153.944l-263.903-703.742zM261.154 577.976l90.717-241.911 90.717 241.911z"
"M1226.4 320h-176l-76.22 203.24 77 205.34 87.22-232.58 90.74 242h-174.44l49.5 132h174.44l57.76 154h154l-264-704z",
"M384 0l-384 1024h224l84-224h408l84 224h224l-384-1024zM380 608l132-352 132 352z"
],
"attrs": [
{},
{}
],
"attrs": [],
"grid": 16,
"tags": [
"icon-font-size-alt1"
],
"width": 1504,
"isMulticolor": false,
"isMulticolor2": false,
"colorPermutations": {
"12552552551": []
"12552552551": [
{},
{}
]
}
},
{

View File

@@ -100,7 +100,7 @@
<glyph unicode="&#xea2c;" glyph-name="icon-frame-hide" d="M128 642h420l104 128h-652v-802.4l128 157.4zM896 2h-420l-104-128h652v802.4l-128-157.4zM832 834l-832-1024h192l832 1024zM392 450l104 128h-304v-128z" />
<glyph unicode="&#xea2d;" glyph-name="icon-import" d="M832 639.6v-639.4c0-0.2-0.2-0.2-0.4-0.4h-319.6v-192h320c105.6 0 192 86.4 192 192v640.2c0 105.6-86.4 192-192 192h-320v-192h319.6c0.2 0 0.4-0.2 0.4-0.4zM192 128v-192l384 384-384 384v-192h-192v-384z" />
<glyph unicode="&#xea2e;" glyph-name="icon-export" d="M192 0.34v639.32l0.34 0.34h319.66v192h-320c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h320v192h-319.66zM1024 320l-384 384v-192h-192v-384h192v-192l384 384z" />
<glyph unicode="&#xea2f;" glyph-name="icon-font-size" d="M842.841 451.952h-120.956l-52.382-139.676 52.918-141.12 59.942 159.84 62.361-166.314h-119.884l34.019-90.717h119.884l39.695-105.836h105.836l-181.434 483.823zM263.903 671.871l-263.903-703.742h153.944l57.729 153.944h280.397l57.729-153.944h153.944l-263.903 703.742zM261.154 254.024l90.717 241.911 90.717-241.911z" />
<glyph unicode="&#xea2f;" glyph-name="icon-font-size" horiz-adv-x="1504" d="M1226.4 512h-176l-76.22-203.24 77-205.34 87.22 232.58 90.74-242h-174.44l49.5-132h174.44l57.76-154h154l-264 704zM384 832l-384-1024h224l84 224h408l84-224h224l-384 1024zM380 224l132 352 132-352z" />
<glyph unicode="&#xea30;" glyph-name="icon-clear-data" d="M632 520l-120-120-120 120-80-80 120-120-120-120 80-80 120 120 120-120 80 80-120 120 120 120-80 80zM512 832c-282.76 0-512-86-512-192v-640c0-106 229.24-192 512-192s512 86 512 192v640c0 106-229.24 192-512 192zM512 0c-176.731 0-320 143.269-320 320s143.269 320 320 320c176.731 0 320-143.269 320-320v0c0-176.731-143.269-320-320-320v0z" />
<glyph unicode="&#xea31;" glyph-name="icon-history" d="M576 768c-247.4 0-448-200.6-448-448h-128l192-192 192 192h-128c0 85.4 33.2 165.8 93.8 226.2 60.4 60.6 140.8 93.8 226.2 93.8s165.8-33.2 226.2-93.8c60.6-60.4 93.8-140.8 93.8-226.2s-33.2-165.8-93.8-226.2c-60.4-60.6-140.8-93.8-226.2-93.8s-165.8 33.2-226.2 93.8l-90.6-90.6c81-81 193-131.2 316.8-131.2 247.4 0 448 200.6 448 448s-200.6 448-448 448zM576 560c-26.6 0-48-21.4-48-48v-211.8l142-142c9.4-9.4 21.6-14 34-14s24.6 4.6 34 14c18.8 18.8 18.8 49.2 0 67.8l-114 114v172c0 26.6-21.4 48-48 48z" />
<glyph unicode="&#xea32;" glyph-name="icon-arrow-up-to-parent" horiz-adv-x="1056" d="M643.427 6.739c-81.955 0.697-148.179 67.065-148.642 149.010v395.872l296.871-247.393v197.914l-395.828 329.857-395.828-328.62v-197.502l296.871 246.156v-396.241c0-190.905 155.239-346.556 346.144-346.968l412.321-0.825 0.412 197.914z" />

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -25,6 +25,7 @@
$headerFontSize: 1.3em;
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: hidden;
height: 100%;
@@ -275,21 +276,22 @@
&__text {
min-height: 22px; // Needed in Firefox when field is blank
white-space: pre-wrap;
&.is-blank-notebook-entry {
&:not(:focus):before {
content: 'Blank entry';
font-style: italic;
opacity: 0.5;
}
}
}
&__embeds {
//flex-wrap: wrap;
&__input {
// Appended to __text element when Notebook is not in readOnly mode
@include inlineInput;
padding-left: $inputTextPLeftRight;
padding-right: $inputTextPLeftRight;
> [class*="__embed"] {
//margin: 0 $interiorMarginSm $interiorMarginSm 0;
@include hover {
&:not(:focus) {
background: rgba($colorBodyFg, 0.1);
}
}
&:focus {
background: $colorInputBg;
}
}

View File

@@ -18,7 +18,7 @@
@import "../plugins/folderView/components/list-view.scss";
@import "../plugins/imagery/components/imagery-view-layout.scss";
@import "../plugins/telemetryTable/components/table-row.scss";
@import "../plugins/telemetryTable/components/telemetry-filter-indicator.scss";
@import "../plugins/telemetryTable/components/table-footer-indicator.scss";
@import "../plugins/tabs/components/tabs.scss";
@import "../plugins/telemetryTable/components/table.scss";
@import "../plugins/timeConductor/conductor.scss";
@@ -48,3 +48,7 @@
@import "../ui/toolbar/components/toolbar-checkbox.scss";
@import "./notebook.scss";
@import "../plugins/notebook/components/sidebar.scss";
#splash-screen {
display: none;
}

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