Compare commits

..

64 Commits

Author SHA1 Message Date
be41c95ab3 use texture map to create dotted plot lines 2020-07-07 11:19:37 -07:00
a6d16409b9 use physical coordinates for distance comparisons 2020-07-06 16:30:22 -07:00
d55466f2c9 Merge branch 'plots/customization' into plots/customization-line 2020-07-06 13:18:30 -07:00
0ef977ebee Merge branch 'master' into plots/customization 2020-07-06 12:46:10 -07:00
41141aa11c access correct file 2020-07-06 12:22:46 -07:00
9ceb3c5b1e Merge pull request #3130 from nasa/display-layout-fix-3128
[Display Layouts] Prevent duplicate from being added when composition 'add' is fired
2020-07-06 11:36:25 -07:00
5ca38c0f43 draw dotted lines using webGL 2020-07-02 16:14:32 -07:00
bee3a9eedf Merge branch 'master' into display-layout-fix-3128 2020-07-02 10:48:53 -07:00
e515d19acd Merge pull request #3144 from nasa/switching-type-error
Fix for non working switch from alpha to tables and plots
2020-07-02 10:38:01 -07:00
dd13efe065 Merge branch 'master' into switching-type-error 2020-07-02 10:26:13 -07:00
b5dfbe268c Merge pull request #3146 from nasa/new-folder-action-fix-07012020
Added unnamed folder and made it required
2020-07-01 16:48:10 -07:00
a9b9107cc3 change icon and action name 2020-07-01 16:30:03 -07:00
cfda4e4214 added unnamed folder and required 2020-07-01 16:20:33 -07:00
0a657de4b2 Fix for non working switch from alpha to tables 2020-07-01 16:09:05 -07:00
8153edb9cb Update any/all criterion when telemetry is removed (#3138)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-07-01 12:22:47 -07:00
22ca339fb9 [LADTable] Lad bounds listener FIX (#3114)
* added bounds listener, moved history request to function, checking for race conditions

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-07-01 09:50:18 -07:00
246aee0029 add inspector display text helper functions 2020-06-30 22:22:59 -07:00
52c918455a Merge branch 'plots/customization' into plots/customization-line 2020-06-30 21:48:53 -07:00
f0cde1055d helper function for marker options display in inspector
refactor for clarity
2020-06-30 21:38:28 -07:00
7f8764560b Add new glyphs 062320 (#3140)
* Adding new glyphs for multiple branches

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-06-30 16:19:45 -07:00
4411bb0a2d UI fixes for NIRVSS #170 (#3141)
* UI fixes for NIRVSS client #170
2020-06-30 16:12:28 -07:00
07b2aeaae1 add missing semi-colon 2020-06-30 14:10:45 -07:00
c4736f3cac add canvas 2d draw line styles 2020-06-30 13:06:08 -07:00
4ecd264d93 [Time Conductor] add history and select range features (#2932)
* basic brush prototype visible

* require alt pressed for grab handle. display only

* pan and zoom now co-exist

* revert selection to times

* make LocalTimeSystem UTCBased (Earth based)

* add LocalTimeSystem

* make isTimeFixed check reusable

* linting

* zoom axis sets start and end times

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

* disable cursor for local time and enable for fixed time

* linting

* resize brush on window resize

* just use d3-brush instead of entire d3 package

* WIP prototyping conductor history

* set global bounds before emitting change event

* WIP conductor history

* WIP save history to and pull history from local storage

* WIP persistence works

* reset axis height after prototyping

* conductor history functionality complete

* clean up refactoring

* add presets

code cleanup

* axis visual tuning

* remove unused function calls

* change tick to timespan to avoid confusion

* fix bounds to use for timespans on pan axis

* linting

* linting

* more linting

* linting

* change realtime end bound to 30 secondes

* add max duration validation

* Tweaks to Time Conductor History menu

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

* refactor to use browser mouse events instead of d3brush

* Styling Time Conductor axis area

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

* resolve merge conflicts

* Styling Time Conductor axis and inputs

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

* fix merge conflict

* move zoom/pan styling up to conductor

* WIP almost there

* fix zoom

* move altPressed up to parent

* handle no drag on pan

* rename inMode vars for clarity

* Styling for Time Conductor zoom and pan

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

* add configurable bounds limit to time conductor

* add presets and records

* fixes for history

* remove lodash

* add default configurables for examples

* do not install local time system

* cleanup

* fix indentation

remove logging

* remove comments

* section-hint without section-separator styling

* provide reasonable defaults for conductor configuration

* specify input to check validation on

* improve validation

* first check both inputs for valid formats

* clear each valid input on new entry

* tear down listeners

* add user instructions

* allow preset bounds to be declared as callback function

* set this.left on resize

code refactoring

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-30 12:10:35 -07:00
0d91b88eb1 add select and display line style to inspector 2020-06-30 12:04:04 -07:00
16677c99c9 Add staleness evaluation to conditions. (#3110)
* Add staleness evaluation to conditions.
Add supporting tests
Resolves #3109

* Fix broken test

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-29 14:13:44 -07:00
ac1ff36c20 refactor shape to shape code 2020-06-29 13:29:53 -07:00
e01e263b37 Merge branch 'master' into plots/customization 2020-06-29 13:18:29 -07:00
6ab468086a Lock views and prevent editing (#3094)
* working lock and unlock

* prevent flexible layout drop hints from showing

* fix lint issue

* wip

* disable mousedown when not editing in DisplayLayout

* continued wip

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

* More new glyphs, updated art

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

* Edit toggle refinements WIP

- Markup, CSS in BrowseBar.vue;

* More new glyphs, updated art

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

* Edit toggle refinements

- Replaced toggle switch with button;

* prevent styling changes when locked

* fix lint issues

* fix tests

* make reviewer suggested changes

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-06-29 13:14:42 -07:00
b80373aee9 refactor for clarity 2020-06-29 12:18:12 -07:00
376ecf93a9 allow point shapes for webGL plots
add circle shape
2020-06-27 15:35:50 -07:00
9d2991ee10 [Snapshots] Add download as PNG and JPG buttons (#3123)
* working export

* fix lint errors
2020-06-26 17:34:36 -07:00
774726af77 add canvas2d circle marker shape 2020-06-26 16:11:57 -07:00
101f3c022c change line style to line method in anticipation of adding true line style attribute 2020-06-26 15:37:52 -07:00
b4d4945057 allow marker shape selection in plot inspector
add shapes for canvas2d - point, cross, star
2020-06-26 15:32:39 -07:00
dadb6120c2 fix lint error 2020-06-26 14:00:39 -07:00
d9a94db59d prevent composition from adding a dupe into layout 2020-06-26 13:51:03 -07:00
6dd8d448df Merge pull request #3116 from nasa/new-folder-action
New folder action
2020-06-25 13:27:28 -07:00
ef2db1edaf Merge branch 'master' into new-folder-action 2020-06-25 13:13:06 -07:00
3748927e87 Display layout fixes 062320 (#3111)
* fix for persisting new domainObject

* convert stacked plot to alpha
2020-06-25 11:03:31 -07:00
7e4aac028b Merge branch 'master' into new-folder-action 2020-06-25 10:08:26 -07:00
c0780a62ae lineWidth is not supported in modern browsers 2020-06-24 13:29:08 -07:00
8e54b8a819 LAD Table (Set) Composition Policy (#2962)
* added LAD Table composition policy, with a check for lad table sets, child can only be lad table
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-24 13:26:22 -07:00
9e5eddec9b [Plots] y-axis width fix (#3112)
* remove lodash

native implementation of lodash max

* remove unused lodash imports

* add 'missing' semi-colon

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

* added some checks for no image

* some code style updates and removing a nested if statement

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2020-06-24 09:47:04 -07:00
6f674930d9 remove fdescribe 2020-06-23 18:34:19 -07:00
8675fc3fa6 add a few more tests 2020-06-23 18:33:51 -07:00
25434342f3 remove unused imports 2020-06-23 16:20:44 -07:00
8044dfe726 fix tests 2020-06-23 16:20:28 -07:00
cd6c7fdc5e fix broken tests 2020-06-23 15:54:10 -07:00
7ff85dc396 remove report 2020-06-23 15:30:37 -07:00
fb4877924a remove fdescribe 2020-06-23 15:23:00 -07:00
4b13cbdb33 add test 2020-06-23 15:22:43 -07:00
51c9328dfd working new folder action 2020-06-23 14:39:19 -07:00
31ac67b393 Do not respond to bounds tick changes (#3106)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-06-23 13:14:17 -07:00
0399766ccd Merge pull request #3074 from nasa/testing-guidelines
Added guidelines to testing documentation
2020-06-23 10:53:39 -07:00
18ab034147 Merge branch 'master' into testing-guidelines 2020-06-23 10:40:58 -07:00
8a4bc2a463 bumped angular to >=1.8.0 (#3100) 2020-06-22 14:56:06 -07:00
e9cf337aac Merge branch 'master' into testing-guidelines 2020-06-17 15:38:56 -07:00
04a18248c7 Added reference to Angular memory leak best practices 2020-06-17 15:38:17 -07:00
d462db60de Add note on convenience function for test cleanup 2020-06-17 15:31:35 -07:00
8962b0c88b Merge branch 'master' into testing-guidelines 2020-05-28 15:48:12 -07:00
3876151a4b Added guidelines to testing documentation
Migrating our testing guidelines into the open source repository in the interests of transparency, and to assist contributors to the project.
2020-05-27 13:57:46 -07:00
93 changed files with 3120 additions and 1100 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@ -1,6 +1,6 @@
# Open MCT License # Open MCT License
Open MCT, Copyright (c) 2014-2019, United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved. Open MCT, Copyright (c) 2014-2020, United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Open MCT is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

View File

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

View File

@ -34,9 +34,8 @@
<body> <body>
</body> </body>
<script> <script>
const ONE_MINUTE = 60 * 1000; const THIRTY_SECONDS = 30 * 1000;
const FIVE_MINUTES = 5 * 60 * 1000; const THIRTY_MINUTES = THIRTY_SECONDS * 60;
const THIRTY_MINUTES = 30 * 60 * 1000;
[ [
'example/eventGenerator' 'example/eventGenerator'
@ -64,15 +63,47 @@
bounds: { bounds: {
start: Date.now() - THIRTY_MINUTES, start: Date.now() - THIRTY_MINUTES,
end: Date.now() end: Date.now()
} },
// commonly used bounds can be stored in history
// bounds (start and end) can accept either a milliseconds number
// or a callback function returning a milliseconds number
// a function is useful for invoking Date.now() at exact moment of preset selection
presets: [
{
label: 'Last Day',
bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 24,
end: () => Date.now()
}
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 2,
end: () => Date.now()
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - 1000 * 60 * 60,
end: () => Date.now()
}
}
],
// maximum recent bounds to retain in conductor history
records: 10,
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
limit: 1000 * 60 * 60 * 24
}, },
{ {
name: "Realtime", name: "Realtime",
timeSystem: 'utc', timeSystem: 'utc',
clock: 'local', clock: 'local',
clockOffsets: { clockOffsets: {
start: - FIVE_MINUTES, start: - THIRTY_MINUTES,
end: ONE_MINUTE end: THIRTY_SECONDS
} }
} }
] ]

View File

@ -2,8 +2,9 @@
"name": "openmct", "name": "openmct",
"version": "1.0.0-snapshot", "version": "1.0.0-snapshot",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": { "devDependencies": {
"angular": "1.7.9", "angular": ">=1.8.0",
"angular-route": "1.4.14", "angular-route": "1.4.14",
"babel-eslint": "8.2.6", "babel-eslint": "8.2.6",
"comma-separated-values": "^3.6.4", "comma-separated-values": "^3.6.4",
@ -38,13 +39,13 @@
"istanbul-instrumenter-loader": "^3.0.1", "istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.1.0", "jasmine-core": "^3.1.0",
"jsdoc": "^3.3.2", "jsdoc": "^3.3.2",
"karma": "5.0.9", "karma": "^2.0.3",
"karma-chrome-launcher": "3.1.0", "karma-chrome-launcher": "^2.2.0",
"karma-cli": "^1.0.1", "karma-cli": "^1.0.1",
"karma-coverage": "^1.1.2", "karma-coverage": "^1.1.2",
"karma-coverage-istanbul-reporter": "^2.1.1", "karma-coverage-istanbul-reporter": "^2.1.1",
"karma-html-reporter": "^0.2.7", "karma-html-reporter": "^0.2.7",
"karma-jasmine": "^2.0.0", "karma-jasmine": "^1.1.2",
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^3.0.0", "karma-webpack": "^3.0.0",
"location-bar": "^3.0.1", "location-bar": "^3.0.1",
@ -59,7 +60,6 @@
"node-bourbon": "^4.2.3", "node-bourbon": "^4.2.3",
"node-sass": "^4.9.2", "node-sass": "^4.9.2",
"painterro": "^0.2.65", "painterro": "^0.2.65",
"plotly.js-dist": "^1.54.1",
"printj": "^1.2.1", "printj": "^1.2.1",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"request": "^2.69.0", "request": "^2.69.0",

View File

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

View File

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

View File

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

View File

@ -252,7 +252,6 @@ define([
// Plugins that are installed by default // Plugins that are installed by default
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
this.install(this.plugins.PlotlyPlot());
this.install(this.plugins.TelemetryTable()); this.install(this.plugins.TelemetryTable());
this.install(PreviewPlugin.default()); this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin()); this.install(LegacyIndicatorsPlugin());
@ -269,6 +268,7 @@ define([
this.install(this.plugins.ConditionWidget()); this.install(this.plugins.ConditionWidget());
this.install(this.plugins.URLTimeSettingsSynchronizer()); this.install(this.plugins.URLTimeSettingsSynchronizer());
this.install(this.plugins.NotificationIndicator()); this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction());
} }
MCT.prototype = Object.create(EventEmitter.prototype); MCT.prototype = Object.create(EventEmitter.prototype);

View File

@ -20,20 +20,13 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
export default class LADTableCompositionPolicy { export default function ladTableCompositionPolicy(openmct) {
return function (parent, child) {
constructor(openmct) {
this.openmct = openmct;
return this.allow.bind(this);
}
allow(parent, child) {
if(parent.type === 'LadTable') { if(parent.type === 'LadTable') {
return this.openmct.telemetry.isTelemetryObject(child); return openmct.telemetry.isTelemetryObject(child);
} else if(parent.type === 'LadTableSet') { } else if(parent.type === 'LadTableSet') {
return child.type === 'LadTable'; return child.type === 'LadTable';
} }
return true; return true;
} }
} }

View File

@ -64,7 +64,7 @@ export default {
}, },
computed: { computed: {
formattedTimestamp() { formattedTimestamp() {
return this.timestamp !== undefined ? this.formats[this.timestampKey].format(this.timestamp) : '---'; return this.timestamp !== undefined ? this.getFormattedTimestamp(this.timestamp) : '---';
} }
}, },
mounted() { mounted() {
@ -110,11 +110,11 @@ export default {
}, },
methods: { methods: {
updateValues(datum) { updateValues(datum) {
let newTimestamp = this.formats[this.timestampKey].parse(datum), let newTimestamp = this.getParsedTimestamp(datum),
limit; limit;
if(this.shouldUpdate(newTimestamp)) { if(this.shouldUpdate(newTimestamp)) {
this.timestamp = this.formats[this.timestampKey].parse(datum); this.timestamp = newTimestamp;
this.value = this.formats[this.valueKey].format(datum); this.value = this.formats[this.valueKey].format(datum);
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata); limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
if (limit) { if (limit) {
@ -125,9 +125,12 @@ export default {
} }
}, },
shouldUpdate(newTimestamp) { shouldUpdate(newTimestamp) {
return (this.timestamp === undefined) || let newTimestampInBounds = this.inBounds(newTimestamp),
(this.inBounds(newTimestamp) && noExistingTimestamp = this.timestamp === undefined,
newTimestamp > this.timestamp); newTimestampIsLatest = newTimestamp > this.timestamp;
return newTimestampInBounds &&
(noExistingTimestamp || newTimestampIsLatest);
}, },
requestHistory() { requestHistory() {
this.openmct this.openmct
@ -146,6 +149,7 @@ export default {
updateBounds(bounds, isTick) { updateBounds(bounds, isTick) {
this.bounds = bounds; this.bounds = bounds;
if(!isTick) { if(!isTick) {
this.resetValues();
this.requestHistory(); this.requestHistory();
} }
}, },
@ -153,13 +157,34 @@ export default {
return timestamp >= this.bounds.start && timestamp <= this.bounds.end; return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
}, },
updateTimeSystem(timeSystem) { updateTimeSystem(timeSystem) {
this.value = '---'; this.resetValues();
this.timestamp = '---';
this.valueClass = '';
this.timestampKey = timeSystem.key; this.timestampKey = timeSystem.key;
}, },
showContextMenu(event) { showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
},
resetValues() {
this.value = '---';
this.timestamp = undefined;
this.valueClass = '';
},
getParsedTimestamp(timestamp) {
if(this.timeSystemFormat()) {
return this.formats[this.timestampKey].parse(timestamp);
}
},
getFormattedTimestamp(timestamp) {
if(this.timeSystemFormat()) {
return this.formats[this.timestampKey].format(timestamp);
}
},
timeSystemFormat() {
if(this.formats[this.timestampKey]) {
return true;
} else {
console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`);
return false;
}
} }
} }
} }

View File

@ -21,7 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import LADTableViewProvider from './LADTableViewProvider'; import LADTableViewProvider from './LADTableViewProvider';
import LADTableSetViewProvider from './LADTableSetViewProvider'; import LADTableSetViewProvider from './LADTableSetViewProvider';
import LADTableCompositionPolicy from './LADTableCompositionPolicy'; import ladTableCompositionPolicy from './LADTableCompositionPolicy';
export default function plugin() { export default function plugin() {
return function install(openmct) { return function install(openmct) {
@ -49,6 +49,6 @@ export default function plugin() {
} }
}); });
openmct.composition.addPolicy(new LADTableCompositionPolicy(openmct)); openmct.composition.addPolicy(ladTableCompositionPolicy(openmct));
}; };
} }

View File

@ -24,7 +24,7 @@ import {
setAllSearchParams setAllSearchParams
} from 'utils/openmctLocation'; } from 'utils/openmctLocation';
const TIME_EVENTS = ['bounds', 'timeSystem', 'clock', 'clockOffsets']; const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets'];
const SEARCH_MODE = 'tc.mode'; const SEARCH_MODE = 'tc.mode';
const SEARCH_TIME_SYSTEM = 'tc.timeSystem'; const SEARCH_TIME_SYSTEM = 'tc.timeSystem';
const SEARCH_START_BOUND = 'tc.startBound'; const SEARCH_START_BOUND = 'tc.startBound';
@ -42,6 +42,7 @@ export default class URLTimeSettingsSynchronizer {
this.destroy = this.destroy.bind(this); this.destroy = this.destroy.bind(this);
this.updateTimeSettings = this.updateTimeSettings.bind(this); this.updateTimeSettings = this.updateTimeSettings.bind(this);
this.setUrlFromTimeApi = this.setUrlFromTimeApi.bind(this); this.setUrlFromTimeApi = this.setUrlFromTimeApi.bind(this);
this.updateBounds = this.updateBounds.bind(this);
openmct.on('start', this.initialize); openmct.on('start', this.initialize);
openmct.on('destroy', this.destroy); openmct.on('destroy', this.destroy);
@ -54,7 +55,7 @@ export default class URLTimeSettingsSynchronizer {
TIME_EVENTS.forEach(event => { TIME_EVENTS.forEach(event => {
this.openmct.time.on(event, this.setUrlFromTimeApi); this.openmct.time.on(event, this.setUrlFromTimeApi);
}); });
this.openmct.time.on('bounds', this.updateBounds);
} }
destroy() { destroy() {
@ -65,6 +66,7 @@ export default class URLTimeSettingsSynchronizer {
TIME_EVENTS.forEach(event => { TIME_EVENTS.forEach(event => {
this.openmct.time.off(event, this.setUrlFromTimeApi); this.openmct.time.off(event, this.setUrlFromTimeApi);
}); });
this.openmct.time.on('bounds', this.updateBounds);
} }
updateTimeSettings() { updateTimeSettings() {
@ -72,7 +74,6 @@ export default class URLTimeSettingsSynchronizer {
if (!this.isUrlUpdateInProgress) { if (!this.isUrlUpdateInProgress) {
let timeParameters = this.parseParametersFromUrl(); let timeParameters = this.parseParametersFromUrl();
if (this.areTimeParametersValid(timeParameters)) { if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters); this.setTimeApiFromUrl(timeParameters);
} else { } else {
@ -138,6 +139,12 @@ export default class URLTimeSettingsSynchronizer {
} }
} }
updateBounds(bounds, isTick) {
if (!isTick) {
this.setUrlFromTimeApi();
}
}
setUrlFromTimeApi() { setUrlFromTimeApi() {
let searchParams = getAllSearchParams(); let searchParams = getAllSearchParams();
let clock = this.openmct.time.clock(); let clock = this.openmct.time.clock();

View File

@ -150,6 +150,7 @@ export default class Condition extends EventEmitter {
criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct); criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);
} }
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
if (!this.criteria) { if (!this.criteria) {
this.criteria = []; this.criteria = [];
} }
@ -178,10 +179,12 @@ export default class Condition extends EventEmitter {
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration); const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct); let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
newCriterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
let criterion = found.item; let criterion = found.item;
criterion.unsubscribe(); criterion.unsubscribe();
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
this.criteria.splice(found.index, 1, newCriterion); this.criteria.splice(found.index, 1, newCriterion);
this.updateDescription(); this.updateDescription();
} }
@ -194,6 +197,9 @@ export default class Condition extends EventEmitter {
criterion.off('criterionUpdated', (obj) => { criterion.off('criterionUpdated', (obj) => {
this.handleCriterionUpdated(obj); this.handleCriterionUpdated(obj);
}); });
criterion.off('telemetryIsStale', (obj) => {
this.handleStaleCriterion(obj);
});
criterion.destroy(); criterion.destroy();
this.criteria.splice(found.index, 1); this.criteria.splice(found.index, 1);
this.updateDescription(); this.updateDescription();
@ -211,6 +217,18 @@ export default class Condition extends EventEmitter {
} }
} }
handleStaleCriterion(updatedCriterion) {
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
let latestTimestamp = {};
latestTimestamp = getLatestTimestamp(
latestTimestamp,
updatedCriterion.data,
this.timeSystems,
this.openmct.time.timeSystem()
);
this.conditionManager.updateCurrentCondition(latestTimestamp);
}
updateDescription() { updateDescription() {
const triggerDescription = this.getTriggerDescription(); const triggerDescription = this.getTriggerDescription();
let description = ''; let description = '';

View File

@ -103,6 +103,8 @@ export default class ConditionManager extends EventEmitter {
criterion.operation = ''; criterion.operation = '';
conditionChanged = true; conditionChanged = true;
} }
} else {
conditionChanged = true;
} }
}); });
if (conditionChanged) { if (conditionChanged) {
@ -315,6 +317,10 @@ export default class ConditionManager extends EventEmitter {
condition.getResult(normalizedDatum); condition.getResult(normalizedDatum);
}); });
this.updateCurrentCondition(timestamp);
}
updateCurrentCondition(timestamp) {
const currentCondition = this.getCurrentCondition(); const currentCondition = this.getCurrentCondition();
this.emit('conditionSetResultUpdated', this.emit('conditionSetResultUpdated',

View File

@ -27,13 +27,13 @@
> >
{{ condition.configuration.name }} {{ condition.configuration.name }}
</span> </span>
<span class="c-style__condition-desc__text" <span v-if="!condition.isDefault"
v-if="!condition.isDefault" class="c-style__condition-desc__text"
> >
{{ description }} {{ description }}
</span> </span>
<span class="c-style__condition-desc__text" <span v-else
v-else class="c-style__condition-desc__text"
> >
Match if no other condition is matched Match if no other condition is matched
</span> </span>

View File

@ -55,6 +55,7 @@
> >
{{ option.name }} {{ option.name }}
</option> </option>
<option value="dataReceived">any data received</option>
</select> </select>
</span> </span>
<span v-if="criterion.telemetry && criterion.metadata" <span v-if="criterion.telemetry && criterion.metadata"
@ -83,6 +84,7 @@
> >
<span v-if="inputIndex < inputCount-1">and</span> <span v-if="inputIndex < inputCount-1">and</span>
</span> </span>
<span v-if="criterion.metadata === 'dataReceived'">seconds</span>
</template> </template>
<span v-else> <span v-else>
<span v-if="inputCount && criterion.operation" <span v-if="inputCount && criterion.operation"
@ -148,7 +150,11 @@ export default {
return (this.index !== 0 ? operator : '') + ' when'; return (this.index !== 0 ? operator : '') + ' when';
}, },
filteredOps: function () { filteredOps: function () {
return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1); if (this.criterion.metadata === 'dataReceived') {
return this.operations.filter(op => op.name === 'isStale');
} else {
return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1);
}
}, },
setInputType: function () { setInputType: function () {
let type = ''; let type = '';
@ -214,6 +220,8 @@ export default {
} else { } else {
this.operationFormat = 'number'; this.operationFormat = 'number';
} }
} else if (this.criterion.metadata === 'dataReceived') {
this.operationFormat = 'number';
} }
this.updateInputVisibilityAndValues(); this.updateInputVisibilityAndValues();
}, },

View File

@ -37,12 +37,13 @@
> >
<style-editor class="c-inspect-styles__editor" <style-editor class="c-inspect-styles__editor"
:style-item="staticStyle" :style-item="staticStyle"
:is-editing="isEditing" :is-editing="allowEditing"
:mixed-styles="mixedStyles" :mixed-styles="mixedStyles"
@persist="updateStaticStyle" @persist="updateStaticStyle"
/> />
</div> </div>
<button <button
v-if="allowEditing"
id="addConditionSet" id="addConditionSet"
class="c-button c-button--major c-toggle-styling-button labeled" class="c-button c-button--major c-toggle-styling-button labeled"
@click="addConditionSet" @click="addConditionSet"
@ -63,7 +64,7 @@
> >
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span> <span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
</a> </a>
<template v-if="isEditing"> <template v-if="allowEditing">
<button <button
id="changeConditionSet" id="changeConditionSet"
class="c-button labeled" class="c-button labeled"
@ -96,7 +97,7 @@
/> />
<style-editor class="c-inspect-styles__editor" <style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle" :style-item="conditionStyle"
:is-editing="isEditing" :is-editing="allowEditing"
@persist="updateConditionalStyle" @persist="updateConditionalStyle"
/> />
</div> </div>
@ -137,7 +138,13 @@ export default {
conditions: undefined, conditions: undefined,
conditionsLoaded: false, conditionsLoaded: false,
navigateToPath: '', navigateToPath: '',
selectedConditionId: '' selectedConditionId: '',
locked: false
}
},
computed: {
allowEditing() {
return this.isEditing && !this.locked;
} }
}, },
destroyed() { destroyed() {
@ -224,7 +231,13 @@ export default {
this.selection.forEach((selectionItem) => { this.selection.forEach((selectionItem) => {
const item = selectionItem[0].context.item; const item = selectionItem[0].context.item;
const layoutItem = selectionItem[0].context.layoutItem; const layoutItem = selectionItem[0].context.layoutItem;
const layoutDomainObject = selectionItem[0].context.item;
const isChildItem = selectionItem.length > 1; const isChildItem = selectionItem.length > 1;
if (layoutDomainObject && layoutDomainObject.locked) {
this.locked = true;
}
if (!isChildItem) { if (!isChildItem) {
domainObject = item; domainObject = item;
itemStyle = getApplicableStylesForItem(item); itemStyle = getApplicableStylesForItem(item);

View File

@ -22,7 +22,7 @@
import TelemetryCriterion from './TelemetryCriterion'; import TelemetryCriterion from './TelemetryCriterion';
import { evaluateResults } from "../utils/evaluator"; import { evaluateResults } from "../utils/evaluator";
import { getLatestTimestamp } from '../utils/time'; import {getLatestTimestamp, subscribeForStaleness} from '../utils/time';
import { getOperatorText } from "@/plugins/condition/utils/operations"; import { getOperatorText } from "@/plugins/condition/utils/operations";
export default class AllTelemetryCriterion extends TelemetryCriterion { export default class AllTelemetryCriterion extends TelemetryCriterion {
@ -41,6 +41,32 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
initialize() { initialize() {
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects }; this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
this.telemetryDataCache = {}; this.telemetryDataCache = {};
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(this.telemetryObjects || {});
}
}
subscribeForStaleData(telemetryObjects) {
if (!this.stalenessSubscription) {
this.stalenessSubscription = {};
}
Object.values(telemetryObjects).forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (!this.stalenessSubscription[id]) {
this.stalenessSubscription[id] = subscribeForStaleness((data) => {
this.handleStaleTelemetry(id, data);
}, this.input[0]*1000);
}
})
}
handleStaleTelemetry(id, data) {
if (this.telemetryDataCache) {
this.telemetryDataCache[id] = true;
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
this.emitEvent('telemetryIsStale', data);
} }
isValid() { isValid() {
@ -50,6 +76,9 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
updateTelemetryObjects(telemetryObjects) { updateTelemetryObjects(telemetryObjects) {
this.telemetryObjects = { ...telemetryObjects }; this.telemetryObjects = { ...telemetryObjects };
this.removeTelemetryDataCache(); this.removeTelemetryDataCache();
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(this.telemetryObjects || {});
}
} }
removeTelemetryDataCache() { removeTelemetryDataCache() {
@ -63,6 +92,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
}); });
telemetryCacheIds.forEach(id => { telemetryCacheIds.forEach(id => {
delete (this.telemetryDataCache[id]); delete (this.telemetryDataCache[id]);
delete (this.stalenessSubscription[id]);
}); });
} }
@ -96,7 +126,14 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
const validatedData = this.isValid() ? data : {}; const validatedData = this.isValid() ? data : {};
if (validatedData) { if (validatedData) {
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData); if (this.isStalenessCheck()) {
if (this.stalenessSubscription[validatedData.id]) {
this.stalenessSubscription[validatedData.id].update(validatedData);
}
this.telemetryDataCache[validatedData.id] = false;
} else {
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
}
} }
Object.values(telemetryObjects).forEach(telemetryObject => { Object.values(telemetryObjects).forEach(telemetryObject => {
@ -162,7 +199,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
getDescription() { getDescription() {
const telemetryDescription = this.telemetry === 'all' ? 'all telemetry' : 'any telemetry'; const telemetryDescription = this.telemetry === 'all' ? 'all telemetry' : 'any telemetry';
let metadataValue = this.metadata; let metadataValue = (this.metadata === 'dataReceived' ? '' : this.metadata);
let inputValue = this.input; let inputValue = this.input;
if (this.metadata) { if (this.metadata) {
const telemetryObjects = Object.values(this.telemetryObjects); const telemetryObjects = Object.values(this.telemetryObjects);
@ -182,5 +219,9 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
destroy() { destroy() {
delete this.telemetryObjects; delete this.telemetryObjects;
delete this.telemetryDataCache; delete this.telemetryDataCache;
if (this.stalenessSubscription) {
Object.values(this.stalenessSubscription).forEach((subscription) => subscription.clear);
delete this.stalenessSubscription;
}
} }
} }

View File

@ -22,6 +22,7 @@
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import { OPERATIONS, getOperatorText } from '../utils/operations'; import { OPERATIONS, getOperatorText } from '../utils/operations';
import { subscribeForStaleness } from "../utils/time";
export default class TelemetryCriterion extends EventEmitter { export default class TelemetryCriterion extends EventEmitter {
@ -43,6 +44,7 @@ export default class TelemetryCriterion extends EventEmitter {
this.input = telemetryDomainObjectDefinition.input; this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata; this.metadata = telemetryDomainObjectDefinition.metadata;
this.result = undefined; this.result = undefined;
this.stalenessSubscription = undefined;
this.initialize(); this.initialize();
this.emitEvent('criterionUpdated', this); this.emitEvent('criterionUpdated', this);
@ -51,14 +53,40 @@ export default class TelemetryCriterion extends EventEmitter {
initialize() { initialize() {
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry); this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects); this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData()
}
}
subscribeForStaleData() {
if (this.stalenessSubscription) {
this.stalenessSubscription.clear();
}
this.stalenessSubscription = subscribeForStaleness(this.handleStaleTelemetry.bind(this), this.input[0]*1000);
}
handleStaleTelemetry(data) {
this.result = true;
this.emitEvent('telemetryIsStale', data);
} }
isValid() { isValid() {
return this.telemetryObject && this.metadata && this.operation; return this.telemetryObject && this.metadata && this.operation;
} }
isStalenessCheck() {
return this.metadata && this.metadata === 'dataReceived';
}
isValidInput() {
return this.input instanceof Array && this.input.length;
}
updateTelemetryObjects(telemetryObjects) { updateTelemetryObjects(telemetryObjects) {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString]; this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData()
}
} }
createNormalizedDatum(telemetryDatum, endpoint) { createNormalizedDatum(telemetryDatum, endpoint) {
@ -91,7 +119,14 @@ export default class TelemetryCriterion extends EventEmitter {
getResult(data) { getResult(data) {
const validatedData = this.isValid() ? data : {}; const validatedData = this.isValid() ? data : {};
this.result = this.computeResult(validatedData); if (this.isStalenessCheck()) {
if (this.stalenessSubscription) {
this.stalenessSubscription.update(validatedData);
}
this.result = false;
} else {
this.result = this.computeResult(validatedData);
}
} }
requestLAD() { requestLAD() {
@ -136,7 +171,7 @@ export default class TelemetryCriterion extends EventEmitter {
let comparator = this.findOperation(this.operation); let comparator = this.findOperation(this.operation);
let params = []; let params = [];
params.push(data[this.metadata]); params.push(data[this.metadata]);
if (this.input instanceof Array && this.input.length) { if (this.isValidInput()) {
this.input.forEach(input => params.push(input)); this.input.forEach(input => params.push(input));
} }
if (typeof comparator === 'function') { if (typeof comparator === 'function') {
@ -191,7 +226,7 @@ export default class TelemetryCriterion extends EventEmitter {
description = `Unknown ${this.metadata} ${getOperatorText(this.operation, this.input)}`; description = `Unknown ${this.metadata} ${getOperatorText(this.operation, this.input)}`;
} else { } else {
const metadataObject = this.getMetaDataObject(this.telemetryObject, this.metadata); const metadataObject = this.getMetaDataObject(this.telemetryObject, this.metadata);
const metadataValue = this.getMetadataValueFromMetaData(metadataObject) || this.metadata; const metadataValue = this.getMetadataValueFromMetaData(metadataObject) || (this.metadata === 'dataReceived' ? '' : this.metadata);
const inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input; const inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input;
description = `${this.telemetryObject.name} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`; description = `${this.telemetryObject.name} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`;
} }
@ -202,5 +237,8 @@ export default class TelemetryCriterion extends EventEmitter {
destroy() { destroy() {
delete this.telemetryObject; delete this.telemetryObject;
delete this.telemetryObjectIdAsString; delete this.telemetryObjectIdAsString;
if (this.stalenessSubscription) {
delete this.stalenessSubscription;
}
} }
} }

View File

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

View File

@ -283,6 +283,18 @@ export const OPERATIONS = [
getDescription: function (values) { getDescription: function (values) {
return ' is not one of ' + values[0]; return ' is not one of ' + values[0];
} }
},
{
name: 'isStale',
operation: function () {
return false;
},
text: 'is older than',
appliesTo: ["number"],
inputCount: 1,
getDescription: function (values) {
return ` is older than ${values[0] || ''} seconds`;
}
} }
]; ];

View File

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

View File

@ -0,0 +1,64 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { subscribeForStaleness } from "./time";
describe('time related utils', () => {
let subscription;
let mockListener;
beforeEach(() => {
mockListener = jasmine.createSpy('listener');
subscription = subscribeForStaleness(mockListener, 100);
});
describe('subscribe for staleness', () => {
it('should call listeners when stale', (done) => {
setTimeout(() => {
expect(mockListener).toHaveBeenCalled();
done();
}, 200);
});
it('should update the subscription', (done) => {
function updated() {
setTimeout(() => {
expect(mockListener).not.toHaveBeenCalled();
done();
}, 50);
}
setTimeout(() => {
subscription.update();
updated();
}, 50);
});
it('should clear the subscription', (done) => {
subscription.clear();
setTimeout(() => {
expect(mockListener).not.toHaveBeenCalled();
done();
}, 200);
});
});
});

View File

@ -73,7 +73,7 @@ define(['lodash'], function (_) {
] ]
} }
}, },
viewTypes = { VIEW_TYPES = {
'telemetry-view': { 'telemetry-view': {
value: 'telemetry-view', value: 'telemetry-view',
name: 'Alphanumeric', name: 'Alphanumeric',
@ -95,28 +95,34 @@ define(['lodash'], function (_) {
class: 'icon-tabular-realtime' class: 'icon-tabular-realtime'
} }
}, },
applicableViews = { APPLICABLE_VIEWS = {
'telemetry-view': [ 'telemetry-view': [
viewTypes['telemetry.plot.overlay'], VIEW_TYPES['telemetry.plot.overlay'],
viewTypes.table VIEW_TYPES['telemetry.plot.stacked'],
VIEW_TYPES.table
], ],
'telemetry.plot.overlay': [ 'telemetry.plot.overlay': [
viewTypes['telemetry.plot.stacked'], VIEW_TYPES['telemetry.plot.stacked'],
viewTypes.table, VIEW_TYPES.table,
viewTypes['telemetry-view'] VIEW_TYPES['telemetry-view']
],
'telemetry.plot.stacked': [
VIEW_TYPES['telemetry.plot.overlay'],
VIEW_TYPES.table,
VIEW_TYPES['telemetry-view']
], ],
'table': [ 'table': [
viewTypes['telemetry.plot.overlay'], VIEW_TYPES['telemetry.plot.overlay'],
viewTypes['telemetry.plot.stacked'], VIEW_TYPES['telemetry.plot.stacked'],
viewTypes['telemetry-view'] VIEW_TYPES['telemetry-view']
], ],
'telemetry-view-multi': [ 'telemetry-view-multi': [
viewTypes['telemetry.plot.overlay'], VIEW_TYPES['telemetry.plot.overlay'],
viewTypes['telemetry.plot.stacked'], VIEW_TYPES['telemetry.plot.stacked'],
viewTypes.table VIEW_TYPES.table
], ],
'telemetry.plot.overlay-multi': [ 'telemetry.plot.overlay-multi': [
viewTypes['telemetry.plot.stacked'] VIEW_TYPES['telemetry.plot.stacked']
] ]
}; };
@ -510,7 +516,7 @@ define(['lodash'], function (_) {
selectedItemType = 'telemetry-view'; selectedItemType = 'telemetry-view';
} }
let viewOptions = applicableViews[selectedItemType]; let viewOptions = APPLICABLE_VIEWS[selectedItemType];
if (viewOptions) { if (viewOptions) {
return { return {
@ -533,7 +539,7 @@ define(['lodash'], function (_) {
domainObject: selectedParent, domainObject: selectedParent,
icon: "icon-object", icon: "icon-object",
title: "Merge into a telemetry table or plot", title: "Merge into a telemetry table or plot",
options: applicableViews['telemetry-view-multi'], options: APPLICABLE_VIEWS['telemetry-view-multi'],
method: function (option) { method: function (option) {
displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value); displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value);
} }
@ -546,7 +552,7 @@ define(['lodash'], function (_) {
domainObject: selectedParent, domainObject: selectedParent,
icon: "icon-object", icon: "icon-object",
title: "Merge into a stacked plot", title: "Merge into a stacked plot",
options: applicableViews['telemetry.plot.overlay-multi'], options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'],
method: function (option) { method: function (option) {
displayLayoutContext.mergeMultipleOverlayPlots(selection, option.value); displayLayoutContext.mergeMultipleOverlayPlots(selection, option.value);
} }
@ -590,7 +596,7 @@ define(['lodash'], function (_) {
let selectedParent = selectionPath[1].context.item; let selectedParent = selectionPath[1].context.item;
let layoutItem = selectionPath[0].context.layoutItem; let layoutItem = selectionPath[0].context.layoutItem;
if (!layoutItem) { if (!layoutItem || selectedParent.locked) {
return; return;
} }

View File

@ -24,6 +24,7 @@
<layout-frame <layout-frame
:item="item" :item="item"
:grid-size="gridSize" :grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)" @move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')" @endMove="() => $emit('endMove')"
> >
@ -70,7 +71,11 @@ export default {
type: Number, type: Number,
required: true required: true
}, },
initSelect: Boolean initSelect: Boolean,
isEditing: {
type: Boolean,
required: true
}
}, },
computed: { computed: {
style() { style() {

View File

@ -24,14 +24,18 @@
<div <div
class="l-layout" class="l-layout"
:class="{ :class="{
'is-multi-selected': selectedLayoutItems.length > 1 'is-multi-selected': selectedLayoutItems.length > 1,
'allow-editing': isEditing
}" }"
@dragover="handleDragOver" @dragover="handleDragOver"
@click.capture="bypassSelection" @click.capture="bypassSelection"
@drop="handleDrop" @drop="handleDrop"
> >
<!-- Background grid --> <!-- Background grid -->
<div class="l-layout__grid-holder c-grid"> <div
v-if="isEditing"
class="l-layout__grid-holder c-grid"
>
<div <div
v-if="gridSize[0] >= 3" v-if="gridSize[0] >= 3"
class="c-grid__x l-grid l-grid-x" class="c-grid__x l-grid l-grid-x"
@ -53,6 +57,7 @@
:init-select="initSelectIndex === index" :init-select="initSelectIndex === index"
:index="index" :index="index"
:multi-select="selectedLayoutItems.length > 1" :multi-select="selectedLayoutItems.length > 1"
:is-editing="isEditing"
@move="move" @move="move"
@endMove="endMove" @endMove="endMove"
@endLineResize="endLineResize" @endLineResize="endLineResize"
@ -78,6 +83,30 @@ import ImageView from './ImageView.vue'
import EditMarquee from './EditMarquee.vue' import EditMarquee from './EditMarquee.vue'
import _ from 'lodash' import _ from 'lodash'
const TELEMETRY_IDENTIFIER_FUNCTIONS = {
'table': (domainObject) => {
return Promise.resolve(domainObject.composition);
},
'telemetry.plot.overlay': (domainObject) => {
return Promise.resolve(domainObject.composition);
},
'telemetry.plot.stacked': (domainObject, openmct) => {
let composition = openmct.composition.get(domainObject);
return composition.load().then((objects) => {
let identifiers = [];
objects.forEach(object => {
if (object.type === 'telemetry.plot.overlay') {
identifiers.push(...object.composition);
} else {
identifiers.push(object.identifier);
}
});
return Promise.resolve(identifiers);
});
}
}
const ITEM_TYPE_VIEW_MAP = { const ITEM_TYPE_VIEW_MAP = {
'subobject-view': SubobjectView, 'subobject-view': SubobjectView,
'telemetry-view': TelemetryView, 'telemetry-view': TelemetryView,
@ -114,6 +143,10 @@ export default {
domainObject: { domainObject: {
type: Object, type: Object,
required: true required: true
},
isEditing: {
type: Boolean,
required: true
} }
}, },
data() { data() {
@ -140,7 +173,7 @@ export default {
let selectionPath = this.selection[0]; let selectionPath = this.selection[0];
let singleSelectedLine = this.selection.length === 1 && let singleSelectedLine = this.selection.length === 1 &&
selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type === 'line-view'; selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type === 'line-view';
return selectionPath && selectionPath.length > 1 && !singleSelectedLine; return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine;
} }
}, },
inject: ['openmct', 'options', 'objectPath'], inject: ['openmct', 'options', 'objectPath'],
@ -328,6 +361,9 @@ export default {
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier)); .some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
}, },
handleDragOver($event) { handleDragOver($event) {
if (this.internalDomainObject.locked) {
return;
}
// Get the ID of the dragged object // Get the ID of the dragged object
let draggedKeyString = $event.dataTransfer.types let draggedKeyString = $event.dataTransfer.types
.filter(type => type.startsWith(DRAG_OBJECT_TRANSFER_PREFIX)) .filter(type => type.startsWith(DRAG_OBJECT_TRANSFER_PREFIX))
@ -421,16 +457,44 @@ export default {
this.objectViewMap = {}; this.objectViewMap = {};
this.layoutItems.forEach(this.trackItem); this.layoutItems.forEach(this.trackItem);
}, },
addChild(child) { isItemAlreadyTracked(child) {
let keyString = this.openmct.objects.makeKeyString(child.identifier); let found = false,
if (this.isTelemetry(child)) { keyString = this.openmct.objects.makeKeyString(child.identifier);
if (!this.telemetryViewMap[keyString] && !this.objectViewMap[keyString]) {
this.addItem('telemetry-view', child); this.layoutItems.forEach(item => {
if (item.identifier) {
let itemKeyString = this.openmct.objects.makeKeyString(item.identifier);
if (itemKeyString === keyString) {
found = true;
return;
}
} }
} else if (!this.objectViewMap[keyString]) { });
this.addItem('subobject-view', child);
if (found) {
return true;
} else if (this.isTelemetry(child)) {
return this.telemetryViewMap[keyString] && this.objectViewMap[keyString];
} else {
return this.objectViewMap[keyString];
} }
}, },
addChild(child) {
if (this.isItemAlreadyTracked(child)) {
return;
}
let type;
if (this.isTelemetry(child)) {
type = 'telemetry-view';
} else {
type = 'subobject-view';
}
this.addItem(type, child);
},
removeChild(identifier) { removeChild(identifier) {
let keyString = this.openmct.objects.makeKeyString(identifier); let keyString = this.openmct.objects.makeKeyString(identifier);
@ -530,7 +594,7 @@ export default {
createNewDomainObject(domainObject, composition, viewType, nameExtension, model) { createNewDomainObject(domainObject, composition, viewType, nameExtension, model) {
let identifier = { let identifier = {
key: uuid(), key: uuid(),
namespace: domainObject.identifier.namespace namespace: this.internalDomainObject.identifier.namespace
}, },
type = this.openmct.types.get(viewType), type = this.openmct.types.get(viewType),
parentKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier), parentKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier),
@ -549,7 +613,7 @@ export default {
object.identifier = identifier; object.identifier = identifier;
object.location = parentKeyString; object.location = parentKeyString;
this.openmct.objects.mutate(object, 'persisted', Date.now()); this.openmct.objects.mutate(object, 'created', Date.now());
return object; return object;
}, },
@ -671,31 +735,42 @@ export default {
this.removeItem(selection); this.removeItem(selection);
this.initSelectIndex = this.layoutItems.length - 1; this.initSelectIndex = this.layoutItems.length - 1;
}, },
getTelemetryIdentifiers(domainObject) {
let method = TELEMETRY_IDENTIFIER_FUNCTIONS[domainObject.type];
if (method) {
return method(domainObject, this.openmct);
} else {
throw 'No method identified for domainObject type';
}
},
switchViewType(context, viewType, selection) { switchViewType(context, viewType, selection) {
let domainObject = context.item, let domainObject = context.item,
layoutItem = context.layoutItem, layoutItem = context.layoutItem,
position = [layoutItem.x, layoutItem.y], position = [layoutItem.x, layoutItem.y],
newDomainObject,
layoutType = 'subobject-view'; layoutType = 'subobject-view';
if (layoutItem.type === 'telemetry-view') { if (layoutItem.type === 'telemetry-view') {
newDomainObject = this.createNewDomainObject(domainObject, [domainObject.identifier], viewType); let newDomainObject = this.createNewDomainObject(domainObject, [domainObject.identifier], viewType);
} else {
if (viewType !== 'telemetry-view') {
newDomainObject = this.createNewDomainObject(domainObject, domainObject.composition, viewType);
} else {
domainObject.composition.forEach((identifier , index) => {
let positionX = position[0] + (index * DUPLICATE_OFFSET),
positionY = position[1] + (index * DUPLICATE_OFFSET);
this.convertToTelemetryView(identifier, [positionX, positionY]);
});
}
}
if (newDomainObject) {
this.composition.add(newDomainObject); this.composition.add(newDomainObject);
this.addItem(layoutType, newDomainObject, position); this.addItem(layoutType, newDomainObject, position);
} else {
this.getTelemetryIdentifiers(domainObject).then((identifiers) => {
if (viewType === 'telemetry-view') {
identifiers.forEach((identifier, index) => {
let positionX = position[0] + (index * DUPLICATE_OFFSET),
positionY = position[1] + (index * DUPLICATE_OFFSET);
this.convertToTelemetryView(identifier, [positionX, positionY]);
});
} else {
let newDomainObject = this.createNewDomainObject(domainObject, identifiers, viewType);
this.composition.add(newDomainObject);
this.addItem(layoutType, newDomainObject, position);
}
});
} }
this.removeItem(selection); this.removeItem(selection);

View File

@ -24,6 +24,7 @@
<layout-frame <layout-frame
:item="item" :item="item"
:grid-size="gridSize" :grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)" @move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')" @endMove="() => $emit('endMove')"
> >
@ -70,7 +71,11 @@ export default {
type: Number, type: Number,
required: true required: true
}, },
initSelect: Boolean initSelect: Boolean,
isEditing: {
type: Boolean,
required: true
}
}, },
computed: { computed: {
style() { style() {

View File

@ -33,7 +33,7 @@
<div <div
class="c-frame-edit__move" class="c-frame-edit__move"
@mousedown="startMove([1,1], [0,0], $event)" @mousedown="isEditing ? startMove([1,1], [0,0], $event) : null"
></div> ></div>
</div> </div>
</template> </template>
@ -54,6 +54,10 @@ export default {
required: true, required: true,
validator: (arr) => arr && arr.length === 2 validator: (arr) => arr && arr.length === 2
&& arr.every(el => typeof el === 'number') && arr.every(el => typeof el === 'number')
},
isEditing: {
type: Boolean,
required: true
} }
}, },
computed: { computed: {

View File

@ -24,6 +24,7 @@
:item="item" :item="item"
:grid-size="gridSize" :grid-size="gridSize"
:title="domainObject && domainObject.name" :title="domainObject && domainObject.name"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)" @move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')" @endMove="() => $emit('endMove')"
> >
@ -95,6 +96,10 @@ export default {
index: { index: {
type: Number, type: Number,
required: true required: true
},
isEditing: {
type: Boolean,
required: true
} }
}, },
data() { data() {

View File

@ -24,6 +24,7 @@
<layout-frame <layout-frame
:item="item" :item="item"
:grid-size="gridSize" :grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)" @move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')" @endMove="() => $emit('endMove')"
> >
@ -105,6 +106,10 @@ export default {
index: { index: {
type: Number, type: Number,
required: true required: true
},
isEditing: {
type: Boolean,
required: true
} }
}, },
data() { data() {

View File

@ -24,6 +24,7 @@
<layout-frame <layout-frame
:item="item" :item="item"
:grid-size="gridSize" :grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)" @move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')" @endMove="() => $emit('endMove')"
> >
@ -75,7 +76,11 @@ export default {
type: Number, type: Number,
required: true required: true
}, },
initSelect: Boolean initSelect: Boolean,
isEditing: {
type: Boolean,
required: true
}
}, },
computed: { computed: {
style() { style() {

View File

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

View File

@ -54,10 +54,11 @@ export default function DisplayLayoutPlugin(options) {
}, },
data() { data() {
return { return {
domainObject: domainObject domainObject: domainObject,
isEditing: openmct.editor.isEditing()
}; };
}, },
template: '<layout ref="displayLayout" :domain-object="domainObject"></layout>' template: '<layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></layout>'
}); });
}, },
getSelectionContext() { getSelectionContext() {
@ -73,6 +74,9 @@ export default function DisplayLayoutPlugin(options) {
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots
}; };
}, },
onEditModeChange: function (isEditing) {
component.isEditing = isEditing;
},
destroy() { destroy() {
component.$destroy(); component.$destroy();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,16 +24,17 @@
</div> </div>
<div class="main-image s-image-main c-imagery__main-image" <div class="main-image s-image-main c-imagery__main-image"
:class="{'paused unnsynced': paused(),'stale':false }" :class="{'paused unnsynced': paused(),'stale':false }"
:style="{'background-image': `url(${getImageUrl()})`, :style="{'background-image': getImageUrl() ? `url(${getImageUrl()})` : 'none',
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}" 'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
> >
</div> </div>
<div class="c-imagery__control-bar"> <div class="c-imagery__control-bar">
<div class="c-imagery__timestamp">{{ getTime() }}</div> <div class="c-imagery__timestamp">{{ getTime() }}</div>
<div class="h-local-controls flex-elem"> <div class="h-local-controls flex-elem">
<a class="c-button icon-pause pause-play" <a
:class="{'is-paused': paused()}" class="c-button icon-pause pause-play"
@click="paused(!paused())" :class="{'is-paused': paused()}"
@click="paused(!paused())"
></a> ></a>
</div> </div>
</div> </div>
@ -185,6 +186,10 @@ export default {
setSelectedImage(image) { setSelectedImage(image) {
// If we are paused and the current image IS selected, unpause // If we are paused and the current image IS selected, unpause
// Otherwise, set current image and pause // Otherwise, set current image and pause
if (!image) {
return;
}
if (this.isPaused && image.selected) { if (this.isPaused && image.selected) {
this.paused(false); this.paused(false);
this.unselectAllImages(); this.unselectAllImages();
@ -197,7 +202,7 @@ export default {
} }
}, },
boundsChange(bounds, isTick) { boundsChange(bounds, isTick) {
if(!isTick) { if (!isTick) {
this.requestHistory(); this.requestHistory();
} }
}, },

View File

@ -41,7 +41,7 @@ define([], function () {
this.timeFormat = 'local-format'; this.timeFormat = 'local-format';
this.durationFormat = 'duration'; this.durationFormat = 'duration';
this.isUTCBased = false; this.isUTCBased = true;
} }
return LocalTimeSystem; return LocalTimeSystem;

View File

@ -0,0 +1,82 @@
/*****************************************************************************
* 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 uuid from 'uuid';
export default class NewFolderAction {
constructor(openmct) {
this.name = 'Add New Folder';
this.key = 'newFolder';
this.description = 'Create a new folder';
this.cssClass = 'icon-folder-new';
this._openmct = openmct;
this._dialogForm = {
name: "Add New Folder",
sections: [
{
rows: [
{
key: "name",
control: "textfield",
name: "Folder Name",
pattern: "\\S+",
required: true,
cssClass: "l-input-lg"
}
]
}
]
};
}
invoke(objectPath) {
let domainObject = objectPath[0],
parentKeystring = this._openmct.objects.makeKeyString(domainObject.identifier),
composition = this._openmct.composition.get(domainObject),
dialogService = this._openmct.$injector.get('dialogService'),
folderType = this._openmct.types.get('folder');
dialogService.getUserInput(this._dialogForm, {name: 'Unnamed Folder'}).then((userInput) => {
let name = userInput.name,
identifier = {
key: uuid(),
namespace: domainObject.identifier.namespace
},
objectModel = {
identifier,
type: 'folder',
location: parentKeystring
};
folderType.definition.initialize(objectModel);
objectModel.name = name || 'New Folder';
this._openmct.objects.mutate(objectModel, 'created', Date.now());
composition.add(objectModel);
});
}
appliesTo(objectPath) {
let domainObject = objectPath[0];
return domainObject.type === 'folder';
}
}

View File

@ -0,0 +1,28 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import NewFolderAction from './newFolderAction';
export default function () {
return function (openmct) {
openmct.contextMenu.registerAction(new NewFolderAction(openmct));
};
}

View File

@ -0,0 +1,99 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe("the plugin", () => {
let openmct,
compositionAPI,
newFolderAction,
mockObjectPath,
mockDialogService,
mockComposition,
mockPromise,
newFolderName = 'New Folder';
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
newFolderAction = openmct.contextMenu._allActions.filter(action => {
return action.key === 'newFolder';
})[0];
});
afterEach(() => {
resetApplicationState(openmct);
});
it('installs the new folder action', () => {
expect(newFolderAction).toBeDefined();
});
describe('when invoked', () => {
beforeEach((done) => {
compositionAPI = openmct.composition;
mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
mockPromise = {
then: (callback) => {
callback({name: newFolderName});
done();
}
};
mockDialogService = jasmine.createSpyObj('dialogService', ['getUserInput']);
mockComposition = jasmine.createSpyObj('composition', ['add']);
mockDialogService.getUserInput.and.returnValue(mockPromise);
spyOn(openmct.$injector, 'get').and.returnValue(mockDialogService);
spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'mutate');
newFolderAction.invoke(mockObjectPath);
});
it('gets user input for folder name', () => {
expect(mockDialogService.getUserInput).toHaveBeenCalled();
});
it('creates a new folder object', () => {
expect(openmct.objects.mutate).toHaveBeenCalled();
});
it('adds new folder object to parent composition', () => {
expect(mockComposition.add).toHaveBeenCalled();
});
});
});

View File

@ -60,6 +60,7 @@ export default {
}, },
mounted() { mounted() {
this.addPopupMenuItems(); this.addPopupMenuItems();
this.exportImageService = this.openmct.$injector.get('exportImageService');
}, },
methods: { methods: {
addPopupMenuItems() { addPopupMenuItems() {
@ -205,7 +206,7 @@ export default {
}, },
openSnapshot() { openSnapshot() {
const self = this; const self = this;
const snapshot = new Vue({ this.snapshot = new Vue({
data: () => { data: () => {
return { return {
embed: self.embed embed: self.embed
@ -213,14 +214,15 @@ export default {
}, },
methods: { methods: {
formatTime: self.formatTime, formatTime: self.formatTime,
annotateSnapshot: self.annotateSnapshot annotateSnapshot: self.annotateSnapshot,
exportImage: self.exportImage
}, },
template: SnapshotTemplate template: SnapshotTemplate
}); });
const snapshotOverlay = this.openmct.overlays.overlay({ const snapshotOverlay = this.openmct.overlays.overlay({
element: snapshot.$mount().$el, element: this.snapshot.$mount().$el,
onDestroy: () => { snapshot.$destroy(true) }, onDestroy: () => { this.snapshot.$destroy(true) },
size: 'large', size: 'large',
dismissable: true, dismissable: true,
buttons: [ buttons: [
@ -234,6 +236,15 @@ export default {
] ]
}); });
}, },
exportImage(type) {
let element = this.snapshot.$refs['snapshot-image'];
if (type === 'png') {
this.exportImageService.exportPNG(element, this.embed.name);
} else {
this.exportImageService.exportJPG(element, this.embed.name);
}
},
previewEmbed() { previewEmbed() {
const self = this; const self = this;
const previewAction = new PreviewAction(self.openmct); const previewAction = new PreviewAction(self.openmct);

View File

@ -15,14 +15,32 @@
<div class="l-browse-bar__snapshot-datetime"> <div class="l-browse-bar__snapshot-datetime">
SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}} SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div> </div>
<span class="c-button-set c-button-set--strip-h">
<button
class="c-button icon-download"
title="Export This View's Data as PNG"
@click="exportImage('png')"
>
<span class="c-button__label">PNG</span>
</button>
<button
class="c-button"
title="Export This View's Data as JPG"
@click="exportImage('jpg')"
>
<span class="c-button__label">JPG</span>
</button>
</span>
<a class="l-browse-bar__annotate-button c-button icon-pencil" title="Annotate" @click="annotateSnapshot"> <a class="l-browse-bar__annotate-button c-button icon-pencil" title="Annotate" @click="annotateSnapshot">
<span class="title-label">Annotate</span> <span class="title-label">Annotate</span>
</a> </a>
</div> </div>
</div> </div>
<div class="c-notebook-snapshot__image" <div
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }" ref="snapshot-image"
class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
> >
</div> </div>
</div> </div>

View File

@ -44,19 +44,16 @@
</li> </li>
<li class="grid-row"> <li class="grid-row">
<div class="grid-cell label" <div class="grid-cell label"
title="The line rendering style for this series.">Line Style</div> title="The line style and rendering method to join lines for this series.">Line Method and Style</div>
<div class="grid-cell value">{{ { <div class="grid-cell value">
'none': 'None', {{ series.lineOptionsDisplayText() }}
'linear': 'Linear interpolation',
'stepAfter': 'Step After'
}[series.get('interpolate')] }}
</div> </div>
</li> </li>
<li class="grid-row"> <li class="grid-row">
<div class="grid-cell label" <div class="grid-cell label"
title="Whether markers are displayed, and their size.">Markers</div> title="Whether markers are displayed, and their size.">Markers</div>
<div class="grid-cell value"> <div class="grid-cell value">
{{series.get('markers') ? "Enabled: " + series.get('markerSize') + "px" : "Disabled"}} {{ series.markerOptionsDisplayText() }}
</div> </div>
</li> </li>
<li class="grid-row"> <li class="grid-row">

View File

@ -52,7 +52,7 @@
</li> </li>
<li class="grid-row"> <li class="grid-row">
<div class="grid-cell label" <div class="grid-cell label"
title="The line rendering style for this series.">Line Style</div> title="The rendering method to join lines for this series.">Line Method</div>
<div class="grid-cell value"> <div class="grid-cell value">
<select ng-model="form.interpolate"> <select ng-model="form.interpolate">
<option value="none">None</option> <option value="none">None</option>
@ -61,15 +61,45 @@
</select> </select>
</div> </div>
</li> </li>
<li class="grid-row" ng-show="form.interpolate !== 'none'">
<div class="grid-cell label"
title="The line style for this series.">Line Style</div>
<div class="grid-cell value">
<select ng-model="form.lineStyle">
<option
ng-repeat="option in lineStyleOptions"
value="{{ option.value }}"
ng-selected="option.value === form.lineStyle"
>
{{ option.name }}
</option>
</select>
</div>
</li>
<li class="grid-row"> <li class="grid-row">
<div class="grid-cell label" <div class="grid-cell label"
title="Whether markers are displayed.">Markers</div> title="Whether markers are displayed.">Markers</div>
<div class="grid-cell value"><input type="checkbox" ng-model="form.markers"/></div> <div class="grid-cell value">
<input type="checkbox" ng-model="form.markers"/>
<select
ng-show="form.markers"
ng-model="form.markerShape">
<option
ng-repeat="option in markerShapeOptions"
value="{{ option.value }}"
ng-selected="option.value == form.markerShape"
>
{{ option.name }}
</option>
</select>
</div>
</li> </li>
<li class="grid-row"> <li class="grid-row">
<div class="grid-cell label" <div class="grid-cell label"
title="Display markers visually denoting points in alarm.">Alarm Markers</div> title="Display markers visually denoting points in alarm.">Alarm Markers</div>
<div class="grid-cell value"><input type="checkbox" ng-model="form.alarmMarkers"/></div> <div class="grid-cell value">
<input type="checkbox" ng-model="form.alarmMarkers"/>
</div>
</li> </li>
<li class="grid-row" ng-show="form.markers || form.alarmMarkers"> <li class="grid-row" ng-show="form.markers || form.alarmMarkers">
<div class="grid-cell label" <div class="grid-cell label"

View File

@ -19,12 +19,12 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div ng-if="domainObject.getCapability('editor').inEditContext()"> <div ng-if="!domainObject.model.locked && domainObject.getCapability('editor').inEditContext()">
<mct-representation key="'plot-options-edit'" <mct-representation key="'plot-options-edit'"
mct-object="domainObject"> mct-object="domainObject">
</mct-representation> </mct-representation>
</div> </div>
<div ng-if="!domainObject.getCapability('editor').inEditContext()"> <div ng-if="domainObject.model.locked || !domainObject.getCapability('editor').inEditContext()">
<mct-representation key="'plot-options-browse'" <mct-representation key="'plot-options-browse'"
mct-object="domainObject"> mct-object="domainObject">
</mct-representation> </mct-representation>

View File

@ -30,8 +30,7 @@ define([
'./MCTChartPointSet', './MCTChartPointSet',
'./MCTChartAlarmPointSet', './MCTChartAlarmPointSet',
'../draw/DrawLoader', '../draw/DrawLoader',
'../lib/eventHelpers', '../lib/eventHelpers'
'lodash'
], ],
function ( function (
MCTChartLineLinear, MCTChartLineLinear,
@ -39,8 +38,7 @@ function (
MCTChartPointSet, MCTChartPointSet,
MCTChartAlarmPointSet, MCTChartAlarmPointSet,
DrawLoader, DrawLoader,
eventHelpers, eventHelpers
_
) { ) {
var MARKER_SIZE = 6.0, var MARKER_SIZE = 6.0,
@ -373,7 +371,8 @@ function (
chartElement.getBuffer(), chartElement.getBuffer(),
chartElement.color().asRGBAArray(), chartElement.color().asRGBAArray(),
chartElement.count, chartElement.count,
chartElement.series.get('markerSize') chartElement.series.get('markerSize'),
chartElement.series.get('markerShape')
); );
}; };
@ -381,7 +380,8 @@ function (
this.drawAPI.drawLine( this.drawAPI.drawLine(
chartElement.getBuffer(), chartElement.getBuffer(),
chartElement.color().asRGBAArray(), chartElement.color().asRGBAArray(),
chartElement.count chartElement.count,
chartElement.series.get('lineStyle')
); );
}; };
@ -397,9 +397,10 @@ function (
this.offset.yVal(highlight.point, highlight.series) this.offset.yVal(highlight.point, highlight.series)
]), ]),
color = highlight.series.get('color').asRGBAArray(), color = highlight.series.get('color').asRGBAArray(),
pointCount = 1; pointCount = 1,
shape = highlight.series.get('markerShape');
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE); this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
}; };
MCTChartController.prototype.drawRectangles = function () { MCTChartController.prototype.drawRectangles = function () {

View File

@ -22,13 +22,11 @@
/*global define*/ /*global define*/
define([ define([
'lodash',
'EventEmitter', 'EventEmitter',
'./Model', './Model',
'../lib/extend', '../lib/extend',
'../lib/eventHelpers' '../lib/eventHelpers'
], function ( ], function (
_,
EventEmitter, EventEmitter,
Model, Model,
extend, extend,

View File

@ -25,12 +25,16 @@ define([
'lodash', 'lodash',
'../configuration/Model', '../configuration/Model',
'../lib/extend', '../lib/extend',
'EventEmitter' 'EventEmitter',
'../draw/LineStyles',
'../draw/MarkerShapes'
], function ( ], function (
_, _,
Model, Model,
extend, extend,
EventEmitter EventEmitter,
LINE_STYLES,
MARKER_SHAPES
) { ) {
/** /**
@ -55,7 +59,9 @@ define([
* `interpolate`: interpolate method, either `undefined` (no interpolation), * `interpolate`: interpolate method, either `undefined` (no interpolation),
* `linear` (points are connected via straight lines), or * `linear` (points are connected via straight lines), or
* `stepAfter` (points are connected by steps). * `stepAfter` (points are connected by steps).
* `lineStyle`: string, style of line.
* `markers`: boolean, whether or not this series should render with markers. * `markers`: boolean, whether or not this series should render with markers.
* `markerShape`: string, shape of markers.
* `markerSize`: number, size in pixels of markers for this series. * `markerSize`: number, size in pixels of markers for this series.
* `alarmMarkers`: whether or not to display alarm markers for this series. * `alarmMarkers`: whether or not to display alarm markers for this series.
* `stats`: An object that tracks the min and max y values observed in this * `stats`: An object that tracks the min and max y values observed in this
@ -101,8 +107,10 @@ define([
xKey: options.collection.plot.xAxis.get('key'), xKey: options.collection.plot.xAxis.get('key'),
yKey: range.key, yKey: range.key,
markers: true, markers: true,
markerShape: 'point',
markerSize: 2.0, markerSize: 2.0,
alarmMarkers: true alarmMarkers: true,
lineStyle: 'solid'
}; };
}, },
@ -410,6 +418,36 @@ define([
} else { } else {
this.filters = deepCopiedFilters; this.filters = deepCopiedFilters;
} }
},
lineOptionsDisplayText: function () {
const lineMethods = {
'none': 'None',
'linear': 'Linear interpolation',
'stepAfter': 'Step After'
}
const lineMethodKey = this.get('interpolate');
const lineMethod = lineMethods[lineMethodKey];
if (lineMethod === 'None') {
return lineMethod;
}
const lineStyleKey = this.get('lineStyle');
const lineStyle = LINE_STYLES[lineStyleKey].label;
return `${lineMethod}: ${lineStyle}`;
},
markerOptionsDisplayText: function () {
const showMarkers = this.get('markers');
if (!showMarkers) {
return "Disabled";
}
const markerShapeKey = this.get('markerShape');
const markerShape = MARKER_SHAPES[markerShapeKey].label;
const markerSize = this.get('markerSize');
return `${markerShape}: ${markerSize}px`;
} }
}); });

View File

@ -22,13 +22,15 @@
define([ define([
'lodash',
'EventEmitter', 'EventEmitter',
'../lib/eventHelpers' '../lib/eventHelpers',
'./LineStyles',
'./MarkerShapes'
], function ( ], function (
_,
EventEmitter, EventEmitter,
eventHelpers eventHelpers,
LINE_STYLES,
MARKER_SHAPES
) { ) {
/** /**
@ -86,9 +88,8 @@ define([
this.origin = newOrigin; this.origin = newOrigin;
}; };
Draw2D.prototype.drawLine = function (buf, color, points) { Draw2D.prototype.drawLine = function (buf, color, points, style) {
var i; const pattern = LINE_STYLES[style].pattern;
this.setColor(color); this.setColor(color);
// Configure context to draw two-pixel-thick lines // Configure context to draw two-pixel-thick lines
@ -97,11 +98,12 @@ define([
// Start a new path... // Start a new path...
if (buf.length > 1) { if (buf.length > 1) {
this.c2d.beginPath(); this.c2d.beginPath();
this.c2d.setLineDash(pattern);
this.c2d.moveTo(this.x(buf[0]), this.y(buf[1])); this.c2d.moveTo(this.x(buf[0]), this.y(buf[1]));
} }
// ...and add points to it... // ...and add points to it...
for (i = 2; i < points * 2; i = i + 2) { for (let i = 2; i < points * 2; i = i + 2) {
this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1])); this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1]));
} }
@ -123,18 +125,17 @@ define([
buf, buf,
color, color,
points, points,
pointSize pointSize,
shape
) { ) {
var i = 0, const drawC2DShape = MARKER_SHAPES[shape].drawC2D.bind(this);
offset = pointSize / 2;
this.setColor(color); this.setColor(color);
for (; i < points; i++) { for (let i = 0; i < points; i++) {
this.c2d.fillRect( drawC2DShape(
this.x(buf[i * 2]) - offset, this.x(buf[i * 2]),
this.y(buf[i * 2 + 1]) - offset, this.y(buf[i * 2 + 1]),
pointSize,
pointSize pointSize
); );
} }

View File

@ -22,33 +22,59 @@
define([ define([
'lodash',
'EventEmitter', 'EventEmitter',
'../lib/eventHelpers' '../lib/eventHelpers',
'./LineStyles',
'./MarkerShapes'
], function ( ], function (
_,
EventEmitter, EventEmitter,
eventHelpers eventHelpers,
LINE_STYLES,
MARKER_SHAPES
) { ) {
// WebGL shader sources (for drawing plain colors) // WebGL shader sources (for drawing plain colors)
var FRAGMENT_SHADER = [ const FRAGMENT_SHADER = `
"precision mediump float;", precision mediump float;
"uniform vec4 uColor;", uniform vec4 uColor;
"void main(void) {", uniform int uLineStyle;
"gl_FragColor = uColor;", uniform int uMarkerShape;
"}" uniform sampler2D uPattern;
].join('\n'), uniform float uPointCount;
VERTEX_SHADER = [ varying float vLengthSoFar;
"attribute vec2 aVertexPosition;",
"uniform vec2 uDimensions;", void main(void) {
"uniform vec2 uOrigin;", if (uMarkerShape == 2) {
"uniform float uPointSize;", float distance = length(2.0 * gl_PointCoord - 1.0);
"void main(void) {", if (distance > 1.0) {
"gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);", discard;
"gl_PointSize = uPointSize;", }
"}" }
].join('\n'); gl_FragColor = uColor;
if (uLineStyle == 2) {
gl_FragColor = texture2D(
uPattern,
vec2(fract(vLengthSoFar * 10.0))
);
}
}
`;
const VERTEX_SHADER = `
attribute vec2 aVertexPosition;
attribute float aLengthSoFar;
uniform vec2 uDimensions;
uniform vec2 uOrigin;
uniform float uPointSize;
varying float vLengthSoFar;
void main(void) {
gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);
gl_PointSize = uPointSize;
vLengthSoFar = aLengthSoFar;
}
`;
/** /**
* Create a draw api utilizing WebGL. * Create a draw api utilizing WebGL.
@ -92,6 +118,7 @@ define([
this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER); this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
this.gl.shaderSource(this.vertexShader, VERTEX_SHADER); this.gl.shaderSource(this.vertexShader, VERTEX_SHADER);
this.gl.compileShader(this.vertexShader); this.gl.compileShader(this.vertexShader);
this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER); this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER); this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER);
this.gl.compileShader(this.fragmentShader); this.gl.compileShader(this.fragmentShader);
@ -106,19 +133,21 @@ define([
// Get locations for attribs/uniforms from the // Get locations for attribs/uniforms from the
// shader programs (to pass values into shaders at draw-time) // shader programs (to pass values into shaders at draw-time)
this.aVertexPosition = this.gl.getAttribLocation(this.program, "aVertexPosition"); this.aVertexPosition = this.gl.getAttribLocation(this.program, "aVertexPosition");
this.aLengthSoFar = this.gl.getAttribLocation(this.program, "aLengthSoFar");
this.uColor = this.gl.getUniformLocation(this.program, "uColor"); this.uColor = this.gl.getUniformLocation(this.program, "uColor");
this.uLineStyle = this.gl.getUniformLocation(this.program, "uLineStyle");
this.uMarkerShape = this.gl.getUniformLocation(this.program, "uMarkerShape");
this.uDimensions = this.gl.getUniformLocation(this.program, "uDimensions"); this.uDimensions = this.gl.getUniformLocation(this.program, "uDimensions");
this.uOrigin = this.gl.getUniformLocation(this.program, "uOrigin"); this.uOrigin = this.gl.getUniformLocation(this.program, "uOrigin");
this.uPointSize = this.gl.getUniformLocation(this.program, "uPointSize"); this.uPointSize = this.gl.getUniformLocation(this.program, "uPointSize");
this.uPointCount = this.gl.getUniformLocation(this.program, "uPointCount");
this.gl.enableVertexAttribArray(this.aVertexPosition); this.gl.enableVertexAttribArray(this.aVertexPosition);
this.gl.enableVertexAttribArray(this.aLengthSoFar);
// Create a buffer to holds points which will be drawn // Create a buffer to holds points which will be drawn
this.buffer = this.gl.createBuffer(); this.buffer = this.gl.createBuffer();
// Use a line width of 2.0 for legibility
this.gl.lineWidth(2.0);
// Enable blending, for smoothness // Enable blending, for smoothness
this.gl.enable(this.gl.BLEND); this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
@ -140,14 +169,62 @@ define([
((v - this.origin[1]) / this.dimensions[1]) * this.height; ((v - this.origin[1]) / this.dimensions[1]) * this.height;
}; };
DrawWebGL.prototype.doDraw = function (drawType, buf, color, points) { DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) {
if (this.isContextLost) { if (this.isContextLost) {
return; return;
} }
const lineStyle = LINE_STYLES[shape] ? LINE_STYLES[shape].drawWebGL : 0;
const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0;
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW); this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW);
this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0); this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
this.gl.uniform4fv(this.uColor, color); this.gl.uniform4fv(this.uColor, color);
this.gl.uniform1i(this.uLineStyle, lineStyle);
this.gl.uniform1i(this.uMarkerShape, shapeCode);
this.gl.uniform1f(this.uPointCount, points);
const lengthSoFar = [0];
for (let i = 1; i < points; i++) {
const lastX = (i - 1) * 2;
const currentX = i * 2;
const xDelta = this.x(buf[lastX]) - this.x(buf[currentX]);
const yDelta = this.y(buf[lastX + 1]) - this.y(buf[currentX + 1]);
const distance = Math.sqrt(
Math.pow(xDelta, 2)
+ Math.pow(yDelta, 2)
);
lengthSoFar.push(lengthSoFar[i - 1] + distance);
}
let lineBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, lineBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(lengthSoFar), this.gl.DYNAMIC_DRAW);
this.gl.vertexAttribPointer(this.aLengthSoFar, 1, this.gl.FLOAT, false, 0, 0);
const mappedColor = color.map(function (c, i) {
return Math.floor(c * 255);
});
const dots = [
...mappedColor,
0,0,0,0,
0,0,0,0,
0,0,0,0,
];
var texture = this.gl.createTexture();
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
this.gl.texImage2D(
this.gl.TEXTURE_2D, 0, this.gl.RGBA, dots.length / 4, 1, 0,
this.gl.RGBA, this.gl.UNSIGNED_BYTE, new Uint8Array(dots));
this.gl.texParameteri(
this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
this.gl.texParameteri(
this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
this.gl.drawArrays(drawType, 0, points); this.gl.drawArrays(drawType, 0, points);
}; };
@ -200,24 +277,26 @@ define([
* the line, as an RGBA color where each element * the line, as an RGBA color where each element
* is in the range of 0.0-1.0 * is in the range of 0.0-1.0
* @param {number} points the number of points to draw * @param {number} points the number of points to draw
* @param {string} style the line style
*/ */
DrawWebGL.prototype.drawLine = function (buf, color, points) { DrawWebGL.prototype.drawLine = function (buf, color, points, style) {
if (this.isContextLost) { if (this.isContextLost) {
return; return;
} }
this.doDraw(this.gl.LINE_STRIP, buf, color, points);
this.doDraw(this.gl.LINE_STRIP, buf, color, points, style);
}; };
/** /**
* Draw the buffer as points. * Draw the buffer as points.
* *
*/ */
DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize) { DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize, shape) {
if (this.isContextLost) { if (this.isContextLost) {
return; return;
} }
this.gl.uniform1f(this.uPointSize, pointSize); this.gl.uniform1f(this.uPointSize, pointSize);
this.doDraw(this.gl.POINTS, buf, color, points); this.doDraw(this.gl.POINTS, buf, color, points, shape);
}; };
/** /**

View File

@ -0,0 +1,53 @@
/*****************************************************************************
* 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.
*****************************************************************************/
define([], function () {
/**
* @label string (required) display name of shape
* @drawWebGL integer (unique, required) index provided to WebGL Fragment Shader
* @pattern array
*/
const LINE_STYLES = {
solid: {
label: 'Solid',
drawWebGL: 1,
drawC2D: []
},
dot: {
label: 'Dot',
drawWebGL: 2,
drawC2D: [2, 2]
}
// dash: {
// label: 'Dash',
// drawWebGL: 3,
// drawC2D: [5, 2]
// }
// dotDashDot: {
// label: 'Dot Dash Dot',
// drawWebGL: 4,
// drawC2D: [5, 2, 2]
// }
};
return LINE_STYLES;
});

View File

@ -0,0 +1,53 @@
/*****************************************************************************
* 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.
*****************************************************************************/
define([], function () {
/**
* @label string (required) display name of shape
* @drawWebGL integer (unique, required) index provided to WebGL Fragment Shader
* @drawC2D function (required) canvas2d draw function
*/
const MARKER_SHAPES = {
point: {
label: 'Point',
drawWebGL: 1,
drawC2D: function (x, y, size) {
const offset = size / 2;
this.c2d.fillRect(x - offset, y - offset, size, size);
}
},
circle: {
label: 'Circle',
drawWebGL: 2,
drawC2D: function (x, y, size) {
const radius = size / 2;
this.c2d.beginPath();
this.c2d.arc(x, y, radius, 0, 2 * Math.PI, false);
this.c2d.closePath();
this.c2d.fill();
}
}
};
return MARKER_SHAPES;
});

View File

@ -23,13 +23,11 @@
define([ define([
'../configuration/configStore', '../configuration/configStore',
'../lib/eventHelpers', '../lib/eventHelpers',
'objectUtils', 'objectUtils'
'lodash'
], function ( ], function (
configStore, configStore,
eventHelpers, eventHelpers,
objectUtils, objectUtils
_
) { ) {
function PlotOptionsController($scope, openmct, $timeout) { function PlotOptionsController($scope, openmct, $timeout) {

View File

@ -22,9 +22,13 @@
define([ define([
'./PlotModelFormController', './PlotModelFormController',
'../draw/LineStyles',
'../draw/MarkerShapes',
'lodash' 'lodash'
], function ( ], function (
PlotModelFormController, PlotModelFormController,
LINE_STYLES,
MARKER_SHAPES,
_ _
) { ) {
@ -93,6 +97,20 @@ define([
value: o.key value: o.key
}; };
}); });
this.$scope.lineStyleOptions = Object.entries(LINE_STYLES)
.map(([key, obj]) => {
return {
name: obj.label,
value: key
};
});
this.$scope.markerShapeOptions = Object.entries(MARKER_SHAPES)
.map(([key, obj]) => {
return {
name: obj.label,
value: key
};
});
}, },
fields: [ fields: [
@ -104,10 +122,18 @@ define([
modelProp: 'interpolate', modelProp: 'interpolate',
objectPath: dynamicPathForKey('interpolate') objectPath: dynamicPathForKey('interpolate')
}, },
{
modelProp: 'lineStyle',
objectPath: dynamicPathForKey('lineStyle')
},
{ {
modelProp: 'markers', modelProp: 'markers',
objectPath: dynamicPathForKey('markers') objectPath: dynamicPathForKey('markers')
}, },
{
modelProp: 'markerShape',
objectPath: dynamicPathForKey('markerShape')
},
{ {
modelProp: 'markerSize', modelProp: 'markerSize',
coerce: Number, coerce: Number,

View File

@ -21,11 +21,9 @@
*****************************************************************************/ *****************************************************************************/
define([ define([
'./PlotModelFormController', './PlotModelFormController'
'lodash'
], function ( ], function (
PlotModelFormController, PlotModelFormController
_
) { ) {
var PlotYAxisFormController = PlotModelFormController.extend({ var PlotYAxisFormController = PlotModelFormController.extend({

View File

@ -20,12 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([ define([], function () {
'lodash'
], function (
_
) {
function StackedPlotController($scope, openmct, objectService, $element, exportImageService) { function StackedPlotController($scope, openmct, objectService, $element, exportImageService) {
var tickWidth = 0, var tickWidth = 0,
composition, composition,
@ -125,12 +120,13 @@ define([
$scope.$watch('domainObject.getModel().composition', onCompositionChange); $scope.$watch('domainObject.getModel().composition', onCompositionChange);
$scope.$on('plot:tickWidth', function ($e, width) { $scope.$on('plot:tickWidth', function ($e, width) {
var plotId = $e.targetScope.domainObject.getId(); const plotId = $e.targetScope.domainObject.getId();
if (!tickWidthMap.hasOwnProperty(plotId)) { if (!tickWidthMap.hasOwnProperty(plotId)) {
return; return;
} }
tickWidthMap[plotId] = Math.max(width, tickWidthMap[plotId]); tickWidthMap[plotId] = Math.max(width, tickWidthMap[plotId]);
var newTickWidth = _.max(tickWidthMap); const newTickWidth = Math.max(...Object.values(tickWidthMap));
if (newTickWidth !== tickWidth || width !== tickWidth) { if (newTickWidth !== tickWidth || width !== tickWidth) {
tickWidth = newTickWidth; tickWidth = newTickWidth;
$scope.$broadcast('plot:tickWidth', tickWidth); $scope.$broadcast('plot:tickWidth', tickWidth);

View File

@ -1,72 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2019, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PlotlyViewLayout from './components/PlotlyViewLayout.vue';
import Vue from 'vue';
export default function PlotlyViewProvider(openmct) {
return {
key: 'plotlyPlot',
name: 'Plotly Plot',
cssClass: 'icon-plot-overlay',
canView: function (domainObject) {
return domainObject.type === 'plotlyPlot';
},
canEdit: function (domainObject) {
return domainObject.type === 'plotlyPlot';
},
view: function (domainObject) {
let component;
return {
show: function (element, isEditing) {
component = new Vue({
provide: {
openmct,
domainObject
},
el: element,
components: {
PlotlyViewLayout
},
data() {
return {
isEditing
}
},
template: '<plotly-view-layout :isEditing="isEditing"></plotly-view-layout>'
});
},
onEditModeChange: function (isEditing) {
component.isEditing = isEditing;
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}

View File

@ -1,285 +0,0 @@
<template>
<div class="l-view-section js-plotly-container"></div>
</template>
<script>
import _ from 'lodash';
import Plotly from 'plotly.js-dist';
import BoundedTableRowCollection from '../../telemetryTable/collections/BoundedTableRowCollection';
import TelemetryTableRow from '../../telemetryTable/TelemetryTableRow';
import TelemetryTableColumn from '../../telemetryTable/TelemetryTableColumn';
export default {
inject: ['openmct', 'domainObject'],
data: function () {
return {
bounds: this.openmct.time.bounds(),
plotData: {},
outstandingRequests: 0,
subscriptions: {},
plotComposition: undefined,
timestampKey: this.openmct.time.timeSystem().key
}
},
computed: {
getContainerHeight: function () {
return this.plotElement.parentNode.offsetHeight - 5;
},
getContainerWidth: function () {
return this.plotElement.parentNode.offsetWidth - 5;
}
},
mounted() {
this.plotElement = document.querySelector('.js-plotly-container');
this.openmct.time.on('bounds', this.updateDomain);
this.openmct.time.on('bounds', this.updateData);
this.loadComposition();
this.createPlot();
this.boundedRows = {};
this.limitEvaluators = {};
this.columnMaps = {};
this.drawBuffers = {};
this.telemetryObjects = [];
this.subscriptions = {};
this.boundedRowsUnlisteners = {};
this.traceIndices = {};
},
destroyed() {
Object.values(this.subscriptions)
.forEach(subscription => subscription());
this.openmct.time.off('bounds', this.updateDomain);
this.openmct.time.off('bounds', this.updateData);
Object.values(this.boundedRowsUnlisteners).forEach((unlisteners) => {
unlisteners.forEach(unlistener => unlistener());
});
this.plotComposition.off('add', this.addTelemetryObject);
this.plotComposition.off('remove', this.removeTelemetryObject);
},
methods: {
loadComposition() {
this.plotComposition = this.openmct.composition.get(this.domainObject);
this.plotComposition.on('add', this.addTelemetryObject);
this.plotComposition.on('remove', this.removeTelemetryObject);
this.plotComposition.load()
},
addTelemetryObject(telemetryObject) {
this.telemetryObjects.push(telemetryObject);
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.addTraceForObject(telemetryObject);
this.requestData(telemetryObject);
let subscription = this.subscribe(telemetryObject);
this.subscriptions[keyString] = subscription;
},
updateDomain(bounds, isTick) {
let newDomain = {
'xaxis.range': [
bounds.start,
bounds.end
]
};
Plotly.relayout(this.plotElement, newDomain);
},
updateData(bounds, isTick) {
if (!isTick) {
this.clearData();
this.telemetryObjects.forEach(telemetryObject => this.requestData(telemetryObject));
}
},
clearData() {
this.telemetryObjects.forEach(telemetryObject => this.resetTraceForObject(telemetryObject));
},
requestData(telemetryObject) {
return this.openmct.telemetry.request(telemetryObject)
.then(telemetryData => {
const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let columnMap = this.columnMaps[keyString];
let limitEvaluator = this.limitEvaluators[keyString];
const telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
this.boundedRows[keyString].add(telemetryRows);
});
},
subscribe(telemetryObject) {
const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let columnMap = this.columnMaps[keyString];
let limitEvaluator = this.limitEvaluators[keyString];
return this.openmct.telemetry.subscribe(telemetryObject, (datum) => {
let newRow = new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator);
this.boundedRows[keyString].add(newRow);
});
},
createPlot() {
let timeSystem = this.openmct.time.timeSystem();
let bounds = this.openmct.time.bounds();
let formatMetadata = {
key: timeSystem.key,
name: timeSystem.name,
format: timeSystem.timeFormat
}
this.timeFormatter = this.openmct.telemetry.getValueFormatter(formatMetadata);
let xRange = [
bounds.start,
bounds.end
];
let layout = {
hovermode: 'compare',
hoverdistance: -1,
autosize: true,
showlegend: true,
legend: {
y: 1.07,
"orientation": "h"
},
height: this.getContainerHeight,
font: {
family: "'Helvetica Neue', Helvetica, Arial, sans-serif",
size: "12px",
color: "#aaa"
},
xaxis: {
title: timeSystem.name,
type: 'date',
zeroline: false,
showgrid: false,
range: xRange
},
yaxis: {
zeroline: false,
showgrid: false,
tickwidth: 3,
tickcolor: 'transparent',
autorange: true
},
margin: {
l: 40,
r: 5,
b: 40,
t: 0
},
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent'
};
Plotly.newPlot(
this.plotElement,
[],
layout,
{
displayModeBar: false, // turns off hover-activated toolbar
staticPlot: true // turns off hover effects on datapoints
}
);
},
resetTraceForObject(telemetryObject) {
this.removeTraceForObject(telemetryObject);
this.addTraceForObject(telemetryObject);
},
removeTraceForObject(telemetryObject) {
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let index = this.traceIndices[keyString];
Plotly.deleteTraces(this.plotElement, index);
delete this.traceIndices[keyString];
this.recalculateTraceIndices();
this.boundedRowsUnlisteners[keyString].forEach((unlistener) => unlistener());
},
addTraceForObject(telemetryObject) {
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let boundedRows = new BoundedTableRowCollection(this.openmct);
this.boundedRows[keyString] = boundedRows;
this.traceIndices[keyString] = Object.keys(this.traceIndices).length;
this.recalculateTraceIndices();
Plotly.addTraces(this.plotElement, {type: "scattergl", x: [], y: []});
const metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
let columnMap = metadataValues.reduce((map, metadatum) => {
let column = new TelemetryTableColumn(this.openmct, metadatum);
map[metadatum.key] = column;
return map;
}, {});
const limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
const valueFormatter = this.openmct.telemetry.getValueFormatter(this.openmct.telemetry.getMetadata(telemetryObject).valuesForHints(['range'])[0]);
this.columnMaps[keyString] = columnMap;
this.limitEvaluators[keyString] = limitEvaluator;
let timeSystemKey = this.openmct.time.timeSystem().key;
let drawBuffer = {
keyString,
x: [],
y: []
};
this.drawBuffers[keyString] = drawBuffer;
const addRow = (rows) => {
if (rows instanceof Array) {
rows.forEach(row => {
drawBuffer.x.push(row.datum[timeSystemKey]);
drawBuffer.y.push(valueFormatter.format(row.datum));
})
} else {
drawBuffer.x.push(rows.datum[timeSystemKey]);
drawBuffer.y.push(valueFormatter.format(rows.datum));
}
this.scheduleDraw();
}
boundedRows.on('add', addRow);
this.boundedRowsUnlisteners[keyString] = [];
// boundedRows.on('remove', () => {
// console.log("removed rows");
// });
this.boundedRowsUnlisteners[keyString].push(() => {
boundedRows.off('add', addRow);
})
},
recalculateTraceIndices() {
Object.keys(this.traceIndices).forEach((key, indexOfKey) => {
this.traceIndices[key] = indexOfKey;
});
},
scheduleDraw() {
if (!this.drawing) {
this.drawing = true;
requestAnimationFrame(() => {
let dataForXAxes = [];
let dataForYAxes = [];
let traceIndices = [];
Object.values(this.drawBuffers).forEach((drawBuffer) => {
dataForXAxes.push(drawBuffer.x);
dataForYAxes.push(drawBuffer.y);
traceIndices.push(this.traceIndices[drawBuffer.keyString]);
drawBuffer.x = [];
drawBuffer.y = [];
});
Plotly.extendTraces(
this.plotElement,
{
x: dataForXAxes,
y: dataForYAxes
},
traceIndices
);
this.drawing = false;
});
}
}
}
}
</script>

View File

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

View File

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

View File

@ -34,7 +34,6 @@ define([
'./URLIndicatorPlugin/URLIndicatorPlugin', './URLIndicatorPlugin/URLIndicatorPlugin',
'./telemetryMean/plugin', './telemetryMean/plugin',
'./plot/plugin', './plot/plugin',
'./plotlyPlot/plugin',
'./telemetryTable/plugin', './telemetryTable/plugin',
'./staticRootPlugin/plugin', './staticRootPlugin/plugin',
'./notebook/plugin', './notebook/plugin',
@ -54,7 +53,8 @@ define([
'./themes/maelstrom', './themes/maelstrom',
'./themes/snow', './themes/snow',
'./URLTimeSettingsSynchronizer/plugin', './URLTimeSettingsSynchronizer/plugin',
'./notificationIndicator/plugin' './notificationIndicator/plugin',
'./newFolderAction/plugin'
], function ( ], function (
_, _,
UTCTimeSystem, UTCTimeSystem,
@ -69,7 +69,6 @@ define([
URLIndicatorPlugin, URLIndicatorPlugin,
TelemetryMean, TelemetryMean,
PlotPlugin, PlotPlugin,
PlotlyPlotPlugin,
TelemetryTablePlugin, TelemetryTablePlugin,
StaticRootPlugin, StaticRootPlugin,
Notebook, Notebook,
@ -89,7 +88,8 @@ define([
Maelstrom, Maelstrom,
Snow, Snow,
URLTimeSettingsSynchronizer, URLTimeSettingsSynchronizer,
NotificationIndicator NotificationIndicator,
NewFolderAction
) { ) {
var bundleMap = { var bundleMap = {
LocalStorage: 'platform/persistence/local', LocalStorage: 'platform/persistence/local',
@ -177,8 +177,8 @@ define([
plugins.ExampleImagery = ExampleImagery; plugins.ExampleImagery = ExampleImagery;
plugins.ImageryPlugin = ImageryPlugin; plugins.ImageryPlugin = ImageryPlugin;
plugins.Plot = PlotPlugin; plugins.Plot = PlotPlugin;
plugins.PlotlyPlot = PlotlyPlotPlugin.default;
plugins.TelemetryTable = TelemetryTablePlugin; plugins.TelemetryTable = TelemetryTablePlugin;
plugins.SummaryWidget = SummaryWidget; plugins.SummaryWidget = SummaryWidget;
plugins.TelemetryMean = TelemetryMean; plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin; plugins.URLIndicator = URLIndicatorPlugin;
@ -200,6 +200,7 @@ define([
plugins.ConditionWidget = ConditionWidgetPlugin.default; plugins.ConditionWidget = ConditionWidgetPlugin.default;
plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default; plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default;
plugins.NotificationIndicator = NotificationIndicator.default; plugins.NotificationIndicator = NotificationIndicator.default;
plugins.NewFolderAction = NewFolderAction.default;
return plugins; return plugins;
}); });

View File

@ -101,6 +101,12 @@ export default class RemoveAction {
appliesTo(objectPath) { appliesTo(objectPath) {
let parent = objectPath[1]; let parent = objectPath[1];
let parentType = parent && this.openmct.types.get(parent.type); let parentType = parent && this.openmct.types.get(parent.type);
let child = objectPath[0];
let locked = child.locked ? child.locked : parent && parent.locked;
if (locked) {
return false;
}
return parentType && return parentType &&
parentType.definition.creatable && parentType.definition.creatable &&

View File

@ -73,7 +73,6 @@ define(
* @private * @private
*/ */
addOne(row) { addOne(row) {
// console.log('SortedTableRowCollection addOne', row);
if (this.sortOptions === undefined) { if (this.sortOptions === undefined) {
throw 'Please specify sort options'; throw 'Please specify sort options';
} }

View File

@ -22,7 +22,12 @@
<template> <template>
<div <div
class="c-conductor" class="c-conductor"
:class="[isFixed ? 'is-fixed-mode' : 'is-realtime-mode']" :class="[
{ 'is-zooming': isZooming },
{ 'is-panning': isPanning },
{ 'alt-pressed': altPressed },
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
]"
> >
<form <form
ref="conductorForm" ref="conductorForm"
@ -52,7 +57,7 @@
type="text" type="text"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
@change="validateAllBounds(); submitForm()" @change="validateAllBounds('startDate'); submitForm()"
> >
<date-picker <date-picker
v-if="isFixed && isUTCBased" v-if="isFixed && isUTCBased"
@ -92,7 +97,7 @@
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
:disabled="!isFixed" :disabled="!isFixed"
@change="validateAllBounds(); submitForm()" @change="validateAllBounds('endDate'); submitForm()"
> >
<date-picker <date-picker
v-if="isFixed && isUTCBased" v-if="isFixed && isUTCBased"
@ -122,14 +127,25 @@
<conductor-axis <conductor-axis
class="c-conductor__ticks" class="c-conductor__ticks"
:bounds="rawBounds" :view-bounds="viewBounds"
@panAxis="setViewFromBounds" :is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/> />
</div> </div>
<div class="c-conductor__controls"> <div class="c-conductor__controls">
<!-- Mode, time system menu buttons and duration slider -->
<ConductorMode class="c-conductor__mode-select" /> <ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" /> <ConductorTimeSystem class="c-conductor__time-system-select" />
<ConductorHistory
v-if="isFixed"
class="c-conductor__history-select"
:bounds="openmct.time.bounds()"
:time-system="timeSystem"
/>
</div> </div>
<input <input
type="submit" type="submit"
@ -145,6 +161,7 @@ import ConductorTimeSystem from './ConductorTimeSystem.vue';
import DatePicker from './DatePicker.vue'; import DatePicker from './DatePicker.vue';
import ConductorAxis from './ConductorAxis.vue'; import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue'; import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorHistory from './ConductorHistory.vue'
const DEFAULT_DURATION_FORMATTER = 'duration'; const DEFAULT_DURATION_FORMATTER = 'duration';
@ -155,7 +172,8 @@ export default {
ConductorTimeSystem, ConductorTimeSystem,
DatePicker, DatePicker,
ConductorAxis, ConductorAxis,
ConductorModeIcon ConductorModeIcon,
ConductorHistory
}, },
data() { data() {
let bounds = this.openmct.time.bounds(); let bounds = this.openmct.time.bounds();
@ -165,6 +183,7 @@ export default {
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
return { return {
timeSystem: timeSystem,
timeFormatter: timeFormatter, timeFormatter: timeFormatter,
durationFormatter: durationFormatter, durationFormatter: durationFormatter,
offsets: { offsets: {
@ -175,29 +194,68 @@ export default {
start: timeFormatter.format(bounds.start), start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end) end: timeFormatter.format(bounds.end)
}, },
rawBounds: { viewBounds: {
start: bounds.start, start: bounds.start,
end: bounds.end end: bounds.end
}, },
isFixed: this.openmct.time.clock() === undefined, isFixed: this.openmct.time.clock() === undefined,
isUTCBased: timeSystem.isUTCBased, isUTCBased: timeSystem.isUTCBased,
showDatePicker: false showDatePicker: false,
altPressed: false,
isPanning: false,
isZooming: false
} }
}, },
mounted() { mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem()))); this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('bounds', this.setViewFromBounds); this.openmct.time.on('bounds', this.setViewFromBounds);
this.openmct.time.on('timeSystem', this.setTimeSystem); this.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock); this.openmct.time.on('clock', this.setViewFromClock);
this.openmct.time.on('clockOffsets', this.setViewFromOffsets) this.openmct.time.on('clockOffsets', this.setViewFromOffsets)
}, },
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
},
methods: { methods: {
handleKeyDown(event) {
if (event.key === 'Alt') {
this.altPressed = true;
}
},
handleKeyUp(event) {
if (event.key === 'Alt') {
this.altPressed = false;
}
},
pan(bounds) {
this.isPanning = true;
this.setViewFromBounds(bounds);
},
endPan(bounds) {
this.isPanning = false;
if (bounds) {
this.openmct.time.bounds(bounds);
}
},
zoom(bounds) {
this.isZooming = true;
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
endZoom(bounds) {
const _bounds = bounds ? bounds : this.openmct.time.bounds();
this.isZooming = false;
this.openmct.time.bounds(_bounds);
},
setTimeSystem(timeSystem) { setTimeSystem(timeSystem) {
this.timeSystem = timeSystem
this.timeFormatter = this.getFormatter(timeSystem.timeFormat); this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter( this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased; this.isUTCBased = timeSystem.isUTCBased;
}, },
setOffsetsFromView($event) { setOffsetsFromView($event) {
@ -237,8 +295,8 @@ export default {
setViewFromBounds(bounds) { setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start); this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end); this.formattedBounds.end = this.timeFormatter.format(bounds.end);
this.rawBounds.start = bounds.start; this.viewBounds.start = bounds.start;
this.rawBounds.end = bounds.end; this.viewBounds.end = bounds.end;
}, },
setViewFromOffsets(offsets) { setViewFromOffsets(offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start)); this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
@ -251,6 +309,15 @@ export default {
this.setOffsetsFromView(); this.setOffsetsFromView();
} }
}, },
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key)
.find(option => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
clearAllValidation() { clearAllValidation() {
if (this.isFixed) { if (this.isFixed) {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput); [this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
@ -262,36 +329,52 @@ export default {
input.setCustomValidity(''); input.setCustomValidity('');
input.title = ''; input.title = '';
}, },
validateAllBounds() { validateAllBounds(ref) {
return [this.$refs.startDate, this.$refs.endDate].every((input) => { if (!this.areBoundsFormatsValid()) {
let validationResult = true; return false;
let formattedDate; }
if (input === this.$refs.startDate) { let validationResult = true;
formattedDate = this.formattedBounds.start; const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
const limit = this.getBoundsLimit();
if (
this.timeSystem.isUTCBased
&& limit
&& boundsValues.end - boundsValues.start > limit
) {
if (input === currentInput) {
validationResult = "Start and end difference exceeds allowable limit";
}
} else { } else {
formattedDate = this.formattedBounds.end; if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
} }
return this.handleValidationResults(input, validationResult);
});
},
areBoundsFormatsValid() {
let validationResult = true;
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate = input === this.$refs.startDate
? this.formattedBounds.start
: this.formattedBounds.end
;
if (!this.timeFormatter.validate(formattedDate)) { if (!this.timeFormatter.validate(formattedDate)) {
validationResult = 'Invalid date'; validationResult = 'Invalid date';
} else {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
validationResult = this.openmct.time.validateBounds(boundsValues);
} }
if (validationResult !== true) { return this.handleValidationResults(input, validationResult);
input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else {
input.setCustomValidity('');
input.title = '';
return true;
}
}); });
}, },
validateAllOffsets(event) { validateAllOffsets(event) {
@ -315,17 +398,20 @@ export default {
validationResult = this.openmct.time.validateOffsets(offsetValues); validationResult = this.openmct.time.validateOffsets(offsetValues);
} }
if (validationResult !== true) { return this.handleValidationResults(input, validationResult);
input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else {
input.setCustomValidity('');
input.title = '';
return true;
}
}); });
}, },
handleValidationResults(input, validationResult) {
if (validationResult !== true) {
input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else {
input.setCustomValidity('');
input.title = '';
return true;
}
},
submitForm() { submitForm() {
// Allow Vue model to catch up to user input. // Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click) // Submitting form will cause validation messages to display (but only if triggered by button click)
@ -338,12 +424,12 @@ export default {
}, },
startDateSelected(date) { startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date); this.formattedBounds.start = this.timeFormatter.format(date);
this.validateAllBounds(); this.validateAllBounds('startDate');
this.submitForm(); this.submitForm();
}, },
endDateSelected(date) { endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date); this.formattedBounds.end = this.timeFormatter.format(date);
this.validateAllBounds(); this.validateAllBounds('endDate');
this.submitForm(); this.submitForm();
} }
} }

View File

@ -24,7 +24,12 @@
ref="axisHolder" ref="axisHolder"
class="c-conductor-axis" class="c-conductor-axis"
@mousedown="dragStart($event)" @mousedown="dragStart($event)"
></div> >
<div
class="c-conductor-axis__zoom-indicator"
:style="zoomStyle"
></div>
</div>
</template> </template>
<script> <script>
@ -43,52 +48,81 @@ const PIXELS_PER_TICK_WIDE = 200;
export default { export default {
inject: ['openmct'], inject: ['openmct'],
props: { props: {
bounds: { viewBounds: {
type: Object, type: Object,
required: true required: true
},
isFixed: {
type: Boolean,
required: true
},
altPressed: {
type: Boolean,
required: true
}
},
data() {
return {
inPanMode: false,
dragStartX: undefined,
dragX: undefined,
zoomStyle: {}
}
},
computed: {
inZoomMode() {
return !this.inPanMode;
} }
}, },
watch: { watch: {
bounds: { viewBounds: {
handler(bounds) { handler() {
this.setScale(); this.setScale();
}, },
deep: true deep: true
} }
}, },
mounted() { mounted() {
let axisHolder = this.$refs.axisHolder; let vis = d3Selection.select(this.$refs.axisHolder).append("svg:svg");
let height = axisHolder.offsetHeight;
let vis = d3Selection.select(axisHolder)
.append("svg:svg")
.attr("width", "100%")
.attr("height", height);
this.width = this.$refs.axisHolder.clientWidth;
this.xAxis = d3Axis.axisTop(); this.xAxis = d3Axis.axisTop();
this.dragging = false; this.dragging = false;
// draw x axis with labels. CSS is used to position them. // draw x axis with labels. CSS is used to position them.
this.axisElement = vis.append("g"); this.axisElement = vis.append("g")
.attr("class", "axis");
this.setViewFromTimeSystem(this.openmct.time.timeSystem()); this.setViewFromTimeSystem(this.openmct.time.timeSystem());
this.setAxisDimensions();
this.setScale(); this.setScale();
//Respond to changes in conductor //Respond to changes in conductor
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem); this.openmct.time.on("timeSystem", this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL); setInterval(this.resize, RESIZE_POLL_INTERVAL);
}, },
destroyed() {
},
methods: { methods: {
setAxisDimensions() {
const axisHolder = this.$refs.axisHolder;
const rect = axisHolder.getBoundingClientRect();
this.left = Math.round(rect.left);
this.width = axisHolder.clientWidth;
},
setScale() { setScale() {
if (!this.width) {
return;
}
let timeSystem = this.openmct.time.timeSystem(); let timeSystem = this.openmct.time.timeSystem();
let bounds = this.bounds;
if (timeSystem.isUTCBased) { if (timeSystem.isUTCBased) {
this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]); this.xScale.domain(
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
);
} else { } else {
this.xScale.domain([bounds.start, bounds.end]); this.xScale.domain(
[this.viewBounds.start, this.viewBounds.end]
);
} }
this.xAxis.scale(this.xScale); this.xAxis.scale(this.xScale);
@ -102,7 +136,7 @@ export default {
this.xAxis.ticks(this.width / PIXELS_PER_TICK); this.xAxis.ticks(this.width / PIXELS_PER_TICK);
} }
this.msPerPixel = (bounds.end - bounds.start) / this.width; this.msPerPixel = (this.viewBounds.end - this.viewBounds.start) / this.width;
}, },
setViewFromTimeSystem(timeSystem) { setViewFromTimeSystem(timeSystem) {
//The D3 scale used depends on the type of time system as d3 //The D3 scale used depends on the type of time system as d3
@ -120,9 +154,8 @@ export default {
}, },
getActiveFormatter() { getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem(); let timeSystem = this.openmct.time.timeSystem();
let isFixed = this.openmct.time.clock() === undefined;
if (isFixed) { if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat); return this.getFormatter(timeSystem.timeFormat);
} else { } else {
return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
@ -134,45 +167,131 @@ export default {
}).formatter; }).formatter;
}, },
dragStart($event) { dragStart($event) {
let isFixed = this.openmct.time.clock() === undefined; if (this.isFixed) {
if (isFixed) {
this.dragStartX = $event.clientX; this.dragStartX = $event.clientX;
if (this.altPressed) {
this.inPanMode = true;
}
document.addEventListener('mousemove', this.drag); document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.dragEnd, { document.addEventListener('mouseup', this.dragEnd, {
once: true once: true
}); });
if (this.inZoomMode) {
this.startZoom();
}
} }
}, },
drag($event) { drag($event) {
if (!this.dragging) { if (!this.dragging) {
this.dragging = true; this.dragging = true;
requestAnimationFrame(()=>{
let deltaX = $event.clientX - this.dragStartX; requestAnimationFrame(() => {
let percX = deltaX / this.width; this.dragX = $event.clientX;
let bounds = this.openmct.time.bounds(); this.inPanMode ? this.pan() : this.zoom();
let deltaTime = bounds.end - bounds.start;
let newStart = bounds.start - percX * deltaTime;
this.$emit('panAxis',{
start: newStart,
end: newStart + deltaTime
});
this.dragging = false; this.dragging = false;
}) });
} else {
console.log('Rejected drag due to RAF cap');
} }
}, },
dragEnd() { dragEnd() {
this.inPanMode ? this.endPan() : this.endZoom();
document.removeEventListener('mousemove', this.drag); document.removeEventListener('mousemove', this.drag);
this.openmct.time.bounds({ this.dragStartX = undefined;
start: this.bounds.start, this.dragX = undefined;
end: this.bounds.end },
pan() {
const panBounds = this.getPanBounds();
this.$emit('panAxis', panBounds);
},
endPan() {
const panBounds = this.dragStartX && this.dragX && this.dragStartX !== this.dragX
? this.getPanBounds()
: undefined;
this.$emit('endPan', panBounds);
this.inPanMode = false;
},
getPanBounds() {
const bounds = this.openmct.time.bounds();
const deltaTime = bounds.end - bounds.start;
const deltaX = this.dragX - this.dragStartX;
const percX = deltaX / this.width;
const panStart = bounds.start - percX * deltaTime;
return {
start: panStart,
end: panStart + deltaTime
};
},
startZoom() {
const x = this.scaleToBounds(this.dragStartX);
this.zoomStyle = {
left: `${this.dragStartX - this.left}px`
};
this.$emit('zoomAxis', {
start: x,
end: x
}); });
}, },
zoom() {
const zoomRange = this.getZoomRange();
this.zoomStyle = {
left: `${zoomRange.start - this.left}px`,
width: `${zoomRange.end - zoomRange.start}px`
};
this.$emit('zoomAxis', {
start: this.scaleToBounds(zoomRange.start),
end: this.scaleToBounds(zoomRange.end)
});
},
endZoom() {
const zoomRange = this.dragStartX && this.dragX && this.dragStartX !== this.dragX
? this.getZoomRange()
: undefined;
const zoomBounds = zoomRange
? {
start: this.scaleToBounds(zoomRange.start),
end: this.scaleToBounds(zoomRange.end)
}
: this.openmct.time.bounds();
this.zoomStyle = {};
this.$emit('endZoom', zoomBounds);
},
getZoomRange() {
const leftBound = this.left;
const rightBound = this.left + this.width;
const zoomStart = this.dragX < leftBound
? leftBound
: Math.min(this.dragX, this.dragStartX);
const zoomEnd = this.dragX > rightBound
? rightBound
: Math.max(this.dragX, this.dragStartX);
return {
start: zoomStart,
end: zoomEnd
};
},
scaleToBounds(value) {
const bounds = this.openmct.time.bounds();
const timeDelta = bounds.end - bounds.start;
const valueDelta = value - this.left;
const offset = valueDelta / this.width * timeDelta;
return bounds.start + offset;
},
resize() { resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) { if (this.$refs.axisHolder.clientWidth !== this.width) {
this.width = this.$refs.axisHolder.clientWidth; this.setAxisDimensions();
this.setScale(); this.setScale();
} }
} }

View File

@ -0,0 +1,200 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<button class="c-button--menu c-history-button icon-history"
@click.prevent="toggle"
>
<span class="c-button__label">History</span>
</button>
<div v-if="open"
class="c-menu c-conductor__history-menu"
>
<ul v-if="hasHistoryPresets">
<li
v-for="preset in presets"
:key="preset.label"
class="icon-clock"
@click="selectPresetBounds(preset.bounds)"
>
{{ preset.label }}
</li>
</ul>
<div
v-if="hasHistoryPresets"
class="c-menu__section-separator"
></div>
<div class="c-menu__section-hint">
Past timeframes, ordered by latest first
</div>
<ul>
<li
v-for="(timespan, index) in historyForCurrentTimeSystem"
:key="index"
class="icon-history"
@click="selectTimespan(timespan)"
>
{{ formatTime(timespan.start) }} - {{ formatTime(timespan.end) }}
</li>
</ul>
</div>
</div>
</template>
<script>
import toggleMixin from '../../ui/mixins/toggle-mixin';
const LOCAL_STORAGE_HISTORY_KEY = 'tcHistory';
const DEFAULT_RECORDS = 10;
export default {
inject: ['openmct', 'configuration'],
mixins: [toggleMixin],
props: {
bounds: {
type: Object,
required: true
},
timeSystem: {
type: Object,
required: true
}
},
data() {
return {
history: {}, // contains arrays of timespans {start, end}, array key is time system key
presets: []
}
},
computed: {
hasHistoryPresets() {
return this.timeSystem.isUTCBased && this.presets.length;
},
historyForCurrentTimeSystem() {
const history = this.history[this.timeSystem.key];
return history;
}
},
watch: {
bounds: {
handler() {
this.addTimespan();
},
deep: true
},
timeSystem: {
handler() {
this.loadConfiguration();
this.addTimespan();
},
deep: true
},
history: {
handler() {
this.persistHistoryToLocalStorage();
},
deep: true
}
},
mounted() {
this.getHistoryFromLocalStorage();
},
methods: {
getHistoryFromLocalStorage() {
if (localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY)) {
this.history = JSON.parse(localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY))
} else {
this.history = {};
this.persistHistoryToLocalStorage();
}
},
persistHistoryToLocalStorage() {
localStorage.setItem(LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(this.history));
},
addTimespan() {
const key = this.timeSystem.key;
let [...currentHistory] = this.history[key] || [];
const timespan = {
start: this.bounds.start,
end: this.bounds.end
};
const isNotEqual = function (entry) {
const start = entry.start !== this.start;
const end = entry.end !== this.end;
return start || end;
};
currentHistory = currentHistory.filter(isNotEqual, timespan);
while (currentHistory.length >= this.records) {
currentHistory.pop();
}
currentHistory.unshift(timespan);
this.history[key] = currentHistory;
},
selectTimespan(timespan) {
this.openmct.time.bounds(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
});
},
loadConfiguration() {
const configurations = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key);
this.presets = this.loadPresets(configurations);
this.records = this.loadRecords(configurations);
},
loadPresets(configurations) {
const configuration = configurations.find(option => option.presets);
const presets = configuration ? configuration.presets : [];
return presets;
},
loadRecords(configurations) {
const configuration = configurations.find(option => option.records);
const records = configuration ? configuration.records : DEFAULT_RECORDS;
return records;
},
formatTime(time) {
const formatter = this.openmct.telemetry.getValueFormatter({
format: this.timeSystem.timeFormat
}).formatter;
return formatter.format(time);
}
}
}
</script>

View File

@ -110,7 +110,7 @@ export default {
if (clock === undefined) { if (clock === undefined) {
return { return {
key: 'fixed', key: 'fixed',
name: 'Fixed Timespan Mode', name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.', description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular' cssClass: 'icon-tabular'
} }

View File

@ -13,7 +13,7 @@
text-rendering: geometricPrecision; text-rendering: geometricPrecision;
width: 100%; width: 100%;
height: 100%; height: 100%;
> g { > g.axis {
// Overall Tick holder // Overall Tick holder
transform: translateY($tickYPos); transform: translateY($tickYPos);
path { path {
@ -44,7 +44,6 @@
} }
body.desktop .is-fixed-mode & { body.desktop .is-fixed-mode & {
@include cursorGrab();
background-size: 3px 30%; background-size: 3px 30%;
background-color: $colorBodyBgSubtle; background-color: $colorBodyBgSubtle;
box-shadow: inset rgba(black, 0.4) 0 1px 1px; box-shadow: inset rgba(black, 0.4) 0 1px 1px;
@ -55,17 +54,6 @@
stroke: $colorBodyBgSubtle; stroke: $colorBodyBgSubtle;
transition: $transOut; transition: $transOut;
} }
&:hover,
&:active {
$c: $colorKeySubtle;
background-color: $c;
transition: $transIn;
svg text {
stroke: $c;
transition: $transIn;
}
}
} }
.is-realtime-mode & { .is-realtime-mode & {

View File

@ -57,6 +57,65 @@
} }
} }
&.is-fixed-mode {
.c-conductor-axis {
&__zoom-indicator {
border: 1px solid transparent;
display: none; // Hidden by default
}
}
&:not(.is-panning),
&:not(.is-zooming) {
.c-conductor-axis {
&:hover,
&:active {
cursor: col-resize;
filter: $timeConductorAxisHoverFilter;
}
}
}
&.is-panning,
&.is-zooming {
.c-conductor-input input {
// Styles for inputs while zooming or panning
background: rgba($timeConductorActiveBg, 0.4);
}
}
&.alt-pressed {
.c-conductor-axis:hover {
// When alt is being pressed and user is hovering over the axis, set the cursor
@include cursorGrab();
}
}
&.is-panning {
.c-conductor-axis {
@include cursorGrab();
background-color: $timeConductorActivePanBg;
transition: $transIn;
svg text {
stroke: $timeConductorActivePanBg;
transition: $transIn;
}
}
}
&.is-zooming {
.c-conductor-axis__zoom-indicator {
display: block;
position: absolute;
background: rgba($timeConductorActiveBg, 0.4);
border-left-color: $timeConductorActiveBg;
border-right-color: $timeConductorActiveBg;
top: 0; bottom: 0;
}
}
}
&.is-realtime-mode { &.is-realtime-mode {
.c-conductor__time-bounds { .c-conductor__time-bounds {
grid-template-columns: 20px auto 1fr auto auto; grid-template-columns: 20px auto 1fr auto auto;

View File

@ -142,6 +142,9 @@ $colorTimeHov: pullForward($colorTime, 10%);
$colorTimeSubtle: pushBack($colorTime, 20%); $colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(1.2);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #226074;
/************************************************** BROWSING */ /************************************************** BROWSING */
$browseFrameColor: pullForward($colorBodyBg, 10%); $browseFrameColor: pullForward($colorBodyBg, 10%);

View File

@ -146,6 +146,9 @@ $colorTimeHov: pullForward($colorTime, 10%);
$colorTimeSubtle: pushBack($colorTime, 20%); $colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(1.2);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #226074;
/************************************************** BROWSING */ /************************************************** BROWSING */
$browseFrameColor: pullForward($colorBodyBg, 10%); $browseFrameColor: pullForward($colorBodyBg, 10%);

View File

@ -132,7 +132,7 @@ $colorPausedFg: #fff;
// Base variations // Base variations
$colorBodyBgSubtle: pullForward($colorBodyBg, 5%); $colorBodyBgSubtle: pullForward($colorBodyBg, 5%);
$colorBodyBgSubtleHov: pushBack($colorKey, 50%); $colorBodyBgSubtleHov: pushBack($colorKey, 50%);
$colorKeySubtle: pushBack($colorKey, 10%); $colorKeySubtle: pushBack($colorKey, 20%);
// Time Colors // Time Colors
$colorTime: #618cff; $colorTime: #618cff;
@ -142,6 +142,9 @@ $colorTimeHov: pushBack($colorTime, 5%);
$colorTimeSubtle: pushBack($colorTime, 20%); $colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(0.8);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #A0CDE1;
/************************************************** BROWSING */ /************************************************** BROWSING */
$browseFrameColor: pullForward($colorBodyBg, 10%); $browseFrameColor: pullForward($colorBodyBg, 10%);

View File

@ -148,6 +148,7 @@ $glyph-icon-cursor-lock: '\e929';
$glyph-icon-flag: '\e92a'; $glyph-icon-flag: '\e92a';
$glyph-icon-eye-disabled: '\e92b'; $glyph-icon-eye-disabled: '\e92b';
$glyph-icon-notebook-page: '\e92c'; $glyph-icon-notebook-page: '\e92c';
$glyph-icon-unlocked: '\e92d';
$glyph-icon-arrows-right-left: '\ea00'; $glyph-icon-arrows-right-left: '\ea00';
$glyph-icon-arrows-up-down: '\ea01'; $glyph-icon-arrows-up-down: '\ea01';
$glyph-icon-bullet: '\ea02'; $glyph-icon-bullet: '\ea02';
@ -198,6 +199,11 @@ $glyph-icon-export: '\ea2e';
$glyph-icon-font-size: '\ea2f'; $glyph-icon-font-size: '\ea2f';
$glyph-icon-clear-data: '\ea30'; $glyph-icon-clear-data: '\ea30';
$glyph-icon-history: '\ea31'; $glyph-icon-history: '\ea31';
$glyph-icon-arrow-nav-to-parent: '\ea32';
$glyph-icon-crosshair-in-circle: '\ea33';
$glyph-icon-target: '\ea34';
$glyph-icon-items-collapse: '\ea35';
$glyph-icon-items-expand: '\ea36';
$glyph-icon-activity: '\eb00'; $glyph-icon-activity: '\eb00';
$glyph-icon-activity-mode: '\eb01'; $glyph-icon-activity-mode: '\eb01';
$glyph-icon-autoflow-tabular: '\eb02'; $glyph-icon-autoflow-tabular: '\eb02';
@ -240,6 +246,7 @@ $glyph-icon-command: '\eb26';
$glyph-icon-conditional: '\eb27'; $glyph-icon-conditional: '\eb27';
$glyph-icon-condition-widget: '\eb28'; $glyph-icon-condition-widget: '\eb28';
$glyph-icon-alphanumeric: '\eb29'; $glyph-icon-alphanumeric: '\eb29';
$glyph-icon-image-telemetry: '\eb2a';
/************************** GLYPHS AS DATA URI */ /************************** GLYPHS AS DATA URI */
// Only objects have been converted, for use in Create menu and folder views // Only objects have been converted, for use in Create menu and folder views

View File

@ -462,9 +462,17 @@ select {
text-shadow: $shdwMenuText; text-shadow: $shdwMenuText;
padding: $interiorMarginSm; padding: $interiorMarginSm;
box-shadow: $shdwMenu; box-shadow: $shdwMenu;
display: block; display: flex;
flex-direction: column;
position: absolute; position: absolute;
z-index: 100; z-index: 100;
> * {
flex: 0 0 auto;
//+ * {
// margin-top: $interiorMarginSm;
//}
}
} }
@mixin menuInner() { @mixin menuInner() {
@ -502,6 +510,23 @@ select {
.c-menu { .c-menu {
@include menuOuter(); @include menuOuter();
@include menuInner(); @include menuInner();
&__section-hint {
$m: $interiorMargin;
margin: $m 0;
padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
opacity: 0.6;
font-size: 0.9em;
font-style: italic;
}
&__section-separator {
$m: $interiorMargin;
border-top: 1px solid $colorInteriorBorder;
margin: $m 0;
padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
}
} }
.c-super-menu { .c-super-menu {

View File

@ -84,6 +84,7 @@
.icon-flag { @include glyphBefore($glyph-icon-flag); } .icon-flag { @include glyphBefore($glyph-icon-flag); }
.icon-eye-disabled { @include glyphBefore($glyph-icon-eye-disabled); } .icon-eye-disabled { @include glyphBefore($glyph-icon-eye-disabled); }
.icon-notebook-page { @include glyphBefore($glyph-icon-notebook-page); } .icon-notebook-page { @include glyphBefore($glyph-icon-notebook-page); }
.icon-unlocked { @include glyphBefore($glyph-icon-unlocked); }
.icon-arrows-right-left { @include glyphBefore($glyph-icon-arrows-right-left); } .icon-arrows-right-left { @include glyphBefore($glyph-icon-arrows-right-left); }
.icon-arrows-up-down { @include glyphBefore($glyph-icon-arrows-up-down); } .icon-arrows-up-down { @include glyphBefore($glyph-icon-arrows-up-down); }
.icon-bullet { @include glyphBefore($glyph-icon-bullet); } .icon-bullet { @include glyphBefore($glyph-icon-bullet); }
@ -134,6 +135,11 @@
.icon-font-size { @include glyphBefore($glyph-icon-font-size); } .icon-font-size { @include glyphBefore($glyph-icon-font-size); }
.icon-clear-data { @include glyphBefore($glyph-icon-clear-data); } .icon-clear-data { @include glyphBefore($glyph-icon-clear-data); }
.icon-history { @include glyphBefore($glyph-icon-history); } .icon-history { @include glyphBefore($glyph-icon-history); }
.icon-arrow-nav-to-parent { @include glyphBefore($glyph-icon-arrow-nav-to-parent); }
.icon-crosshair-in-circle { @include glyphBefore($glyph-icon-crosshair-in-circle); }
.icon-target { @include glyphBefore($glyph-icon-target); }
.icon-items-collapse { @include glyphBefore($glyph-icon-items-collapse); }
.icon-items-expand { @include glyphBefore($glyph-icon-items-expand); }
.icon-activity { @include glyphBefore($glyph-icon-activity); } .icon-activity { @include glyphBefore($glyph-icon-activity); }
.icon-activity-mode { @include glyphBefore($glyph-icon-activity-mode); } .icon-activity-mode { @include glyphBefore($glyph-icon-activity-mode); }
.icon-autoflow-tabular { @include glyphBefore($glyph-icon-autoflow-tabular); } .icon-autoflow-tabular { @include glyphBefore($glyph-icon-autoflow-tabular); }
@ -176,6 +182,7 @@
.icon-conditional { @include glyphBefore($glyph-icon-conditional); } .icon-conditional { @include glyphBefore($glyph-icon-conditional); }
.icon-condition-widget { @include glyphBefore($glyph-icon-condition-widget); } .icon-condition-widget { @include glyphBefore($glyph-icon-condition-widget); }
.icon-alphanumeric { @include glyphBefore($glyph-icon-alphanumeric); } .icon-alphanumeric { @include glyphBefore($glyph-icon-alphanumeric); }
.icon-image-telemetry { @include glyphBefore($glyph-icon-image-telemetry); }
/************************** 12 PX CLASSES */ /************************** 12 PX CLASSES */
// TODO: sync with 16px redo as of 10/25/18 // TODO: sync with 16px redo as of 10/25/18

View File

@ -114,25 +114,6 @@ mct-plot {
.plot-wrapper-axis-and-display-area { .plot-wrapper-axis-and-display-area {
position: relative; position: relative;
flex: 1 1 auto; flex: 1 1 auto;
.l-state-indicators {
color: $colorPausedBg;
position: absolute;
display: block;
font-size: 1.5em;
pointer-events: none;
top: $interiorMarginSm;
left: $interiorMarginSm;
z-index: 2;
> * + * {
margin-left: $interiorMarginSm;
}
.t-alert-unsynced {
display: none;
}
}
} }
.gl-plot-wrapper-display-area-and-x-axis { .gl-plot-wrapper-display-area-and-x-axis {
@ -294,6 +275,25 @@ mct-plot {
right: $m; right: $m;
} }
} }
.l-state-indicators {
color: $colorPausedBg;
position: absolute;
display: block;
font-size: 1.5em;
pointer-events: none;
top: $interiorMarginSm;
left: $interiorMarginSm;
z-index: 2;
> * + * {
margin-left: $interiorMarginSm;
}
.t-alert-unsynced {
display: none;
}
}
} }
.gl-plot-display-area, .gl-plot-display-area,
@ -432,6 +432,7 @@ mct-plot {
&__wrapper { &__wrapper {
// Holds view-control and both collapsed and expanded legends // Holds view-control and both collapsed and expanded legends
flex: 1 1 auto; flex: 1 1 auto;
overflow: auto; // Prevents collapsed legend from forcing scrollbars on higher parent containers
} }
&__view-control { &__view-control {

View File

@ -893,7 +893,7 @@ body.desktop {
.grid-row { .grid-row {
.grid-cell { .grid-cell {
padding: 3px $interiorMarginLg 3px 0; padding: 3px $interiorMarginLg 3px 0;
&[title] { &[title]:not([title=""]) {
// When a cell has a title, assume it's helpful text // When a cell has a title, assume it's helpful text
cursor: help; cursor: help;
} }

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,7 @@
<glyph unicode="&#xe914;" glyph-name="icon-hourglass" d="M1024 832h-1024c0-282.8 229.2-512 512-512s512 229.2 512 512zM512 448c-102.6 0-199 40-271.6 112.4-41.2 41.2-72 90.2-90.8 143.6h724.6c-18.8-53.4-49.6-102.4-90.8-143.6-72.4-72.4-168.8-112.4-271.4-112.4zM512 320c-282.8 0-512-229.2-512-512h1024c0 282.8-229.2 512-512 512z" /> <glyph unicode="&#xe914;" glyph-name="icon-hourglass" d="M1024 832h-1024c0-282.8 229.2-512 512-512s512 229.2 512 512zM512 448c-102.6 0-199 40-271.6 112.4-41.2 41.2-72 90.2-90.8 143.6h724.6c-18.8-53.4-49.6-102.4-90.8-143.6-72.4-72.4-168.8-112.4-271.4-112.4zM512 320c-282.8 0-512-229.2-512-512h1024c0 282.8-229.2 512-512 512z" />
<glyph unicode="&#xe915;" glyph-name="icon-info" d="M512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM512 704c70.6 0 128-57.4 128-128s-57.4-128-128-128c-70.6 0-128 57.4-128 128s57.4 128 128 128zM704 0h-384v128h64v256h256v-256h64v-128z" /> <glyph unicode="&#xe915;" glyph-name="icon-info" d="M512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM512 704c70.6 0 128-57.4 128-128s-57.4-128-128-128c-70.6 0-128 57.4-128 128s57.4 128 128 128zM704 0h-384v128h64v256h256v-256h64v-128z" />
<glyph unicode="&#xe916;" glyph-name="icon-link" d="M1024 320l-512 512v-307.2l-512-204.8v-256h512v-256z" /> <glyph unicode="&#xe916;" glyph-name="icon-link" d="M1024 320l-512 512v-307.2l-512-204.8v-256h512v-256z" />
<glyph unicode="&#xe917;" glyph-name="icon-lock" d="M832 448h-32v96c0 158.8-129.2 288-288 288s-288-129.2-288-288v-96h-32c-70.4 0-128-57.6-128-128v-384c0-70.4 57.6-128 128-128h640c70.4 0 128 57.6 128 128v384c0 70.4-57.6 128-128 128zM416 544c0 53 43 96 96 96s96-43 96-96v-96h-192v96z" /> <glyph unicode="&#xe917;" glyph-name="icon-lock" horiz-adv-x="768" d="M702 448h-62v128c0 141.385-114.615 256-256 256s-256-114.615-256-256v0-128h-64c-35.301-0.113-63.887-28.699-64-63.989v-512.011c0.113-35.301 28.699-63.887 63.989-64h638.011c35.301 0.113 63.887 28.699 64 63.989v512.011c-0.113 35.301-28.699 63.887-63.989 64h-0.011zM256 448v128c0 70.692 57.308 128 128 128s128-57.308 128-128v0-128z" />
<glyph unicode="&#xe918;" glyph-name="icon-minus" d="M960 192c35.2 0 64 28.8 64 64v128c0 35.2-28.8 64-64 64h-896c-35.2 0-64-28.8-64-64v-128c0-35.2 28.8-64 64-64h896z" /> <glyph unicode="&#xe918;" glyph-name="icon-minus" d="M960 192c35.2 0 64 28.8 64 64v128c0 35.2-28.8 64-64 64h-896c-35.2 0-64-28.8-64-64v-128c0-35.2 28.8-64 64-64h896z" />
<glyph unicode="&#xe919;" glyph-name="icon-people" d="M704 512h64c70.4 0 128 57.6 128 128v64c0 70.4-57.6 128-128 128h-64c-70.4 0-128-57.6-128-128v-64c0-70.4 57.6-128 128-128zM256 512h64c70.4 0 128 57.6 128 128v64c0 70.4-57.6 128-128 128h-64c-70.4 0-128-57.6-128-128v-64c0-70.4 57.6-128 128-128zM832 448h-192c-34.908 0-67.716-9.448-96-25.904 57.278-33.324 96-95.404 96-166.096v-448h384v448c0 105.6-86.4 192-192 192zM384 448h-192c-105.6 0-192-86.4-192-192v-448h576v448c0 105.6-86.4 192-192 192z" /> <glyph unicode="&#xe919;" glyph-name="icon-people" d="M704 512h64c70.4 0 128 57.6 128 128v64c0 70.4-57.6 128-128 128h-64c-70.4 0-128-57.6-128-128v-64c0-70.4 57.6-128 128-128zM256 512h64c70.4 0 128 57.6 128 128v64c0 70.4-57.6 128-128 128h-64c-70.4 0-128-57.6-128-128v-64c0-70.4 57.6-128 128-128zM832 448h-192c-34.908 0-67.716-9.448-96-25.904 57.278-33.324 96-95.404 96-166.096v-448h384v448c0 105.6-86.4 192-192 192zM384 448h-192c-105.6 0-192-86.4-192-192v-448h576v448c0 105.6-86.4 192-192 192z" />
<glyph unicode="&#xe91a;" glyph-name="icon-person" d="M768 576c0-105.6-86.4-192-192-192h-128c-105.6 0-192 86.4-192 192v64c0 105.6 86.4 192 192 192h128c105.6 0 192-86.4 192-192v-64zM64-192v192c0 140.8 115.2 256 256 256h384c140.8 0 256-115.2 256-256v-192z" /> <glyph unicode="&#xe91a;" glyph-name="icon-person" d="M768 576c0-105.6-86.4-192-192-192h-128c-105.6 0-192 86.4-192 192v64c0 105.6 86.4 192 192 192h128c105.6 0 192-86.4 192-192v-64zM64-192v192c0 140.8 115.2 256 256 256h384c140.8 0 256-115.2 256-256v-192z" />
@ -52,6 +52,7 @@
<glyph unicode="&#xe92a;" glyph-name="icon-flag" d="M192 192h832l-192 320 192 320h-896c-70.606-0.215-127.785-57.394-128-127.979v-896.021h192z" /> <glyph unicode="&#xe92a;" glyph-name="icon-flag" d="M192 192h832l-192 320 192 320h-896c-70.606-0.215-127.785-57.394-128-127.979v-896.021h192z" />
<glyph unicode="&#xe92b;" glyph-name="icon-eye-disabled" d="M209.46 223.32q-7.46 9.86-14.26 20.28c-14.737 21.984-27.741 47.184-37.759 73.847l-0.841 2.553c11.078 29.259 24.068 54.443 39.51 77.869l-0.91-1.469c23.221 34.963 50.705 64.8 82.207 89.793l0.793 0.607c57.663 45.719 130.179 75.053 209.311 79.947l1.069 0.053 114.48 140.88c-27.366 5.017-58.869 7.898-91.041 7.92h-0.019c-245.8 0-452.2-168-510.8-395.6 21.856-82.93 60.906-154.847 113.325-214.773l-0.525 0.613zM814.76 416.92q7.52-10 14.44-20.52c14.737-21.984 27.741-47.184 37.759-73.847l0.841-2.553c-10.859-29.216-23.863-54.416-39.447-77.748l0.847 1.348c-23.221-34.963-50.705-64.8-82.207-89.793l-0.793-0.607c-57.762-45.834-130.437-75.216-209.743-80.049l-1.057-0.051-114.46-140.86c27.346-4.988 58.817-7.84 90.955-7.84 0.037 0 0.074 0 0.111 0h-0.005c245.8 0 452.2 168 510.8 395.6-21.856 82.93-60.906 154.847-113.325 214.773l0.525-0.613zM832 832l-832-1024h192l832 1024h-192z" /> <glyph unicode="&#xe92b;" glyph-name="icon-eye-disabled" d="M209.46 223.32q-7.46 9.86-14.26 20.28c-14.737 21.984-27.741 47.184-37.759 73.847l-0.841 2.553c11.078 29.259 24.068 54.443 39.51 77.869l-0.91-1.469c23.221 34.963 50.705 64.8 82.207 89.793l0.793 0.607c57.663 45.719 130.179 75.053 209.311 79.947l1.069 0.053 114.48 140.88c-27.366 5.017-58.869 7.898-91.041 7.92h-0.019c-245.8 0-452.2-168-510.8-395.6 21.856-82.93 60.906-154.847 113.325-214.773l-0.525 0.613zM814.76 416.92q7.52-10 14.44-20.52c14.737-21.984 27.741-47.184 37.759-73.847l0.841-2.553c-10.859-29.216-23.863-54.416-39.447-77.748l0.847 1.348c-23.221-34.963-50.705-64.8-82.207-89.793l-0.793-0.607c-57.762-45.834-130.437-75.216-209.743-80.049l-1.057-0.051-114.46-140.86c27.346-4.988 58.817-7.84 90.955-7.84 0.037 0 0.074 0 0.111 0h-0.005c245.8 0 452.2 168 510.8 395.6-21.856 82.93-60.906 154.847-113.325 214.773l0.525-0.613zM832 832l-832-1024h192l832 1024h-192z" />
<glyph unicode="&#xe92c;" glyph-name="icon-notebook-page" d="M830 770h-830l-4-702c0-106.6 87.4-194 194-194h640c106.6 0 194 87.4 194 194v508c0 106.8-87.4 194-194 194zM832 386l-384-384-192 192v256l192-192 384 384v-256z" /> <glyph unicode="&#xe92c;" glyph-name="icon-notebook-page" d="M830 770h-830l-4-702c0-106.6 87.4-194 194-194h640c106.6 0 194 87.4 194 194v508c0 106.8-87.4 194-194 194zM832 386l-384-384-192 192v256l192-192 384 384v-256z" />
<glyph unicode="&#xe92d;" glyph-name="icon-unlocked" d="M768 832c-141.339-0.114-255.886-114.661-256-255.989v-128.011h-448c-35.301-0.113-63.887-28.699-64-63.989v-512.011c0.113-35.301 28.699-63.887 63.989-64h638.011c35.301 0.113 63.887 28.699 64 63.989v512.011c-0.113 35.301-28.699 63.887-63.989 64h-62.011v128c0 70.692 57.308 128 128 128s128-57.308 128-128v0-128h128v128c-0.114 141.339-114.661 255.886-255.989 256h-0.011z" />
<glyph unicode="&#xea00;" glyph-name="icon-arrows-right-left" d="M1024 320l-448-512v1024zM448 832l-448-512 448-512z" /> <glyph unicode="&#xea00;" glyph-name="icon-arrows-right-left" d="M1024 320l-448-512v1024zM448 832l-448-512 448-512z" />
<glyph unicode="&#xea01;" glyph-name="icon-arrows-up-down" d="M512 832l512-448h-1024zM0 256l512-448 512 448z" /> <glyph unicode="&#xea01;" glyph-name="icon-arrows-up-down" d="M512 832l512-448h-1024zM0 256l512-448 512 448z" />
<glyph unicode="&#xea02;" glyph-name="icon-bullet" d="M832 80c0-44-36-80-80-80h-480c-44 0-80 36-80 80v480c0 44 36 80 80 80h480c44 0 80-36 80-80v-480z" /> <glyph unicode="&#xea02;" glyph-name="icon-bullet" d="M832 80c0-44-36-80-80-80h-480c-44 0-80 36-80 80v480c0 44 36 80 80 80h480c44 0 80-36 80-80v-480z" />
@ -102,6 +103,11 @@
<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" 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="&#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="&#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="&#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" />
<glyph unicode="&#xea33;" glyph-name="icon-crosshair-in-circle" d="M512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM783.6 48.4c-54.634-54.8-125.77-93.12-205.322-106.874l-2.278-0.326v250.8h-128v-250.8c-161.302 28.062-286.738 153.497-314.468 312.5l-0.332 2.3h250.8v128h-250.8c28.062 161.302 153.497 286.738 312.5 314.468l2.3 0.332v-250.8h128v250.8c161.302-28.062 286.738-153.497 314.468-312.5l0.332-2.3h-250.8v-128h250.8c-14.080-81.83-52.4-152.966-107.191-207.591l-0.009-0.009z" />
<glyph unicode="&#xea34;" glyph-name="icon-target" d="M512 448c70.692 0 128-57.308 128-128s-57.308-128-128-128c-70.692 0-128 57.308-128 128v0c0.114 70.647 57.353 127.886 127.989 128h0.011zM512 576c-141.385 0-256-114.615-256-256s114.615-256 256-256c141.385 0 256 114.615 256 256v0c-0.114 141.339-114.661 255.886-255.989 256h-0.011zM512 704c211.87-0.128 383.575-171.912 383.575-383.8 0-211.967-171.833-383.8-383.8-383.8s-383.8 171.833-383.8 383.8c0 105.99 42.963 201.945 112.425 271.4v0c69.21 69.437 164.944 112.401 270.713 112.401 0.312 0 0.624 0 0.936-0.001h-0.048zM512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512z" />
<glyph unicode="&#xea35;" glyph-name="icon-items-collapse" d="M45.2 173.2h229.6l-274.8-274.6 90.6-90.6 274.6 274.8v-229.6h128v448h-448v-128zM1024 741.4l-90.6 90.6-274.6-274.8v229.6h-128v-448h448v128h-229.6l274.8 274.6z" />
<glyph unicode="&#xea36;" glyph-name="icon-items-expand" d="M448-64h-229.4l274.6 274.8-90.4 90.4-274.8-274.6v229.4h-128v-448h448v128zM530.8 429.2l90.4-90.4 274.8 274.6v-229.4h128v448h-448v-128h229.4l-274.6-274.8z" />
<glyph unicode="&#xeb00;" glyph-name="icon-activity" d="M576 768h-256l320-320h-290.256c-44.264 76.516-126.99 128-221.744 128h-128v-512h128c94.754 0 177.48 51.484 221.744 128h290.256l-320-320h256l448 448-448 448z" /> <glyph unicode="&#xeb00;" glyph-name="icon-activity" d="M576 768h-256l320-320h-290.256c-44.264 76.516-126.99 128-221.744 128h-128v-512h128c94.754 0 177.48 51.484 221.744 128h290.256l-320-320h256l448 448-448 448z" />
<glyph unicode="&#xeb01;" glyph-name="icon-activity-mode" d="M512 832c-214.8 0-398.8-132.4-474.8-320h90.8c56.8 0 108-24.8 143-64h241l-192 192h256l320-320-320-320h-256l192 192h-241c-35-39.2-86.2-64-143-64h-90.8c76-187.6 259.8-320 474.8-320 282.8 0 512 229.2 512 512s-229.2 512-512 512z" /> <glyph unicode="&#xeb01;" glyph-name="icon-activity-mode" d="M512 832c-214.8 0-398.8-132.4-474.8-320h90.8c56.8 0 108-24.8 143-64h241l-192 192h256l320-320-320-320h-256l192 192h-241c-35-39.2-86.2-64-143-64h-90.8c76-187.6 259.8-320 474.8-320 282.8 0 512 229.2 512 512s-229.2 512-512 512z" />
<glyph unicode="&#xeb02;" glyph-name="icon-autoflow-tabular" d="M192 832c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h64v1024h-64zM384 832h256v-1024h-256v1024zM832 832h-64v-704h256v512c0 105.6-86.4 192-192 192z" /> <glyph unicode="&#xeb02;" glyph-name="icon-autoflow-tabular" d="M192 832c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h64v1024h-64zM384 832h256v-1024h-256v1024zM832 832h-64v-704h256v512c0 105.6-86.4 192-192 192z" />
@ -144,4 +150,5 @@
<glyph unicode="&#xeb27;" glyph-name="icon-conditional" d="M512 832c-282.76 0-512-229.24-512-512s229.24-512 512-512 512 229.24 512 512-229.24 512-512 512zM512 64l-384 256 384 256 384-256z" /> <glyph unicode="&#xeb27;" glyph-name="icon-conditional" d="M512 832c-282.76 0-512-229.24-512-512s229.24-512 512-512 512 229.24 512 512-229.24 512-512 512zM512 64l-384 256 384 256 384-256z" />
<glyph unicode="&#xeb28;" glyph-name="icon-condition-widget" d="M832 832h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM512 64l-384 256 384 256 384-256z" /> <glyph unicode="&#xeb28;" glyph-name="icon-condition-widget" d="M832 832h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM512 64l-384 256 384 256 384-256z" />
<glyph unicode="&#xeb29;" glyph-name="icon-alphanumeric" d="M535.6 301.4c-8.4-1.6-17.2-3-26.2-4s-18.2-2.4-27.2-4c-10.196-1.861-18.808-4.010-27.21-6.633l1.61 0.433c-8.609-2.674-16.105-6.348-22.89-10.987l0.29 0.187c-6.693-4.517-12.283-10.107-16.663-16.585l-0.137-0.215c-4.6-6.8-7.4-15.6-8.8-26s-0.4-18.4 2.4-25.2c2.746-6.688 7.224-12.195 12.881-16.122l0.119-0.078c5.967-4.053 13.057-6.94 20.704-8.161l0.296-0.039c7.592-1.527 16.319-2.4 25.25-2.4 0.123 0 0.246 0 0.369 0h-0.019c22.2 0 39.6 3.6 52.6 11s23.2 16.2 30.2 26.4c6.273 8.873 11.271 19.191 14.426 30.285l0.174 0.715c1.853 6.809 3.601 15.41 4.855 24.169l0.145 1.231 5.2 41.6c-5.4-4.217-11.723-7.564-18.583-9.689l-0.417-0.111c-6.489-2.241-14.362-4.255-22.444-5.662l-0.956-0.138zM1024 448v192h-152l24 192h-192l-24-192h-256l24 192h-192l-24-192h-232v-192h208l-32-256h-176v-192h152l-24-192h192l24 192h256l-24-192h192l24 192h232v192h-208l32 256zM702.8 420.2l-26.4-211.8c-2.231-15.809-3.537-34.122-3.6-52.727v-0.073c0-16.8 2.2-29.4 6.4-37.8h-113.4c-1.342 5.556-2.338 12.122-2.781 18.84l-0.019 0.36c-0.261 3.524-0.409 7.634-0.409 11.778 0 2.962 0.076 5.907 0.226 8.832l-0.017-0.41c-18.663-17.401-41.395-30.694-66.597-38.289l-1.203-0.311c-22.627-6.956-48.639-10.974-75.586-11h-0.014c-0.764-0.011-1.666-0.018-2.569-0.018-18.098 0-35.598 2.563-52.156 7.345l1.325-0.328c-15.991 4.512-29.851 12.090-41.545 22.122l0.145-0.122c-11.233 9.982-19.792 22.733-24.624 37.192l-0.176 0.608c-5.2 15.2-6.4 33.4-3.8 54.4s9.4 42.2 19.4 57.2c9.524 14.399 21.535 26.346 35.532 35.512l0.468 0.288c13.387 8.662 28.922 15.533 45.512 19.765l1.088 0.235c13.436 3.792 30.801 7.554 48.47 10.41l2.93 0.39c17 2.6 33.8 4.6 50.4 6.2 16.628 1.527 31.69 4.070 46.349 7.643l-2.149-0.443c13 3 23.6 7.6 31.6 13.6s12.6 15 13.6 26.4 0.8 21.8-2.4 28.8c-2.849 6.902-7.542 12.56-13.468 16.517l-0.132 0.083c-6.217 4.011-13.604 6.78-21.543 7.774l-0.257 0.026c-7.897 1.277-17 2.007-26.274 2.007-0.537 0-1.073-0.002-1.609-0.007l0.082 0.001c-22 0-40-4.6-53.8-14.2s-23-25.2-28-47.2h-111.8c4.8 26.2 14.2 48 27.8 65.4 13.475 16.978 29.89 30.968 48.574 41.377l0.826 0.423c18.192 10.038 39.297 17.806 61.619 22.175l1.381 0.225c20.488 4.162 44.053 6.563 68.171 6.6h0.029c21.8-0.005 43.239-1.532 64.222-4.479l-2.422 0.279c20.641-2.809 39.324-8.783 56.401-17.461l-1.001 0.461c15.909-8.108 28.858-20.031 37.967-34.601l0.233-0.399c9-15 12.2-34.8 9-59.6z" /> <glyph unicode="&#xeb29;" glyph-name="icon-alphanumeric" d="M535.6 301.4c-8.4-1.6-17.2-3-26.2-4s-18.2-2.4-27.2-4c-10.196-1.861-18.808-4.010-27.21-6.633l1.61 0.433c-8.609-2.674-16.105-6.348-22.89-10.987l0.29 0.187c-6.693-4.517-12.283-10.107-16.663-16.585l-0.137-0.215c-4.6-6.8-7.4-15.6-8.8-26s-0.4-18.4 2.4-25.2c2.746-6.688 7.224-12.195 12.881-16.122l0.119-0.078c5.967-4.053 13.057-6.94 20.704-8.161l0.296-0.039c7.592-1.527 16.319-2.4 25.25-2.4 0.123 0 0.246 0 0.369 0h-0.019c22.2 0 39.6 3.6 52.6 11s23.2 16.2 30.2 26.4c6.273 8.873 11.271 19.191 14.426 30.285l0.174 0.715c1.853 6.809 3.601 15.41 4.855 24.169l0.145 1.231 5.2 41.6c-5.4-4.217-11.723-7.564-18.583-9.689l-0.417-0.111c-6.489-2.241-14.362-4.255-22.444-5.662l-0.956-0.138zM1024 448v192h-152l24 192h-192l-24-192h-256l24 192h-192l-24-192h-232v-192h208l-32-256h-176v-192h152l-24-192h192l24 192h256l-24-192h192l24 192h232v192h-208l32 256zM702.8 420.2l-26.4-211.8c-2.231-15.809-3.537-34.122-3.6-52.727v-0.073c0-16.8 2.2-29.4 6.4-37.8h-113.4c-1.342 5.556-2.338 12.122-2.781 18.84l-0.019 0.36c-0.261 3.524-0.409 7.634-0.409 11.778 0 2.962 0.076 5.907 0.226 8.832l-0.017-0.41c-18.663-17.401-41.395-30.694-66.597-38.289l-1.203-0.311c-22.627-6.956-48.639-10.974-75.586-11h-0.014c-0.764-0.011-1.666-0.018-2.569-0.018-18.098 0-35.598 2.563-52.156 7.345l1.325-0.328c-15.991 4.512-29.851 12.090-41.545 22.122l0.145-0.122c-11.233 9.982-19.792 22.733-24.624 37.192l-0.176 0.608c-5.2 15.2-6.4 33.4-3.8 54.4s9.4 42.2 19.4 57.2c9.524 14.399 21.535 26.346 35.532 35.512l0.468 0.288c13.387 8.662 28.922 15.533 45.512 19.765l1.088 0.235c13.436 3.792 30.801 7.554 48.47 10.41l2.93 0.39c17 2.6 33.8 4.6 50.4 6.2 16.628 1.527 31.69 4.070 46.349 7.643l-2.149-0.443c13 3 23.6 7.6 31.6 13.6s12.6 15 13.6 26.4 0.8 21.8-2.4 28.8c-2.849 6.902-7.542 12.56-13.468 16.517l-0.132 0.083c-6.217 4.011-13.604 6.78-21.543 7.774l-0.257 0.026c-7.897 1.277-17 2.007-26.274 2.007-0.537 0-1.073-0.002-1.609-0.007l0.082 0.001c-22 0-40-4.6-53.8-14.2s-23-25.2-28-47.2h-111.8c4.8 26.2 14.2 48 27.8 65.4 13.475 16.978 29.89 30.968 48.574 41.377l0.826 0.423c18.192 10.038 39.297 17.806 61.619 22.175l1.381 0.225c20.488 4.162 44.053 6.563 68.171 6.6h0.029c21.8-0.005 43.239-1.532 64.222-4.479l-2.422 0.279c20.641-2.809 39.324-8.783 56.401-17.461l-1.001 0.461c15.909-8.108 28.858-20.031 37.967-34.601l0.233-0.399c9-15 12.2-34.8 9-59.6z" />
<glyph unicode="&#xeb2a;" glyph-name="icon-image-telemetry" d="M512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM783.6 48.4c-69.581-69.675-165.757-112.776-272-112.776-212.298 0-384.4 172.102-384.4 384.4s172.102 384.4 384.4 384.4c212.298 0 384.4-172.102 384.4-384.4 0-0.008 0-0.017 0-0.025v0.001c0.001-0.264 0.001-0.575 0.001-0.887 0-105.769-42.964-201.503-112.391-270.703l-0.010-0.010zM704 448l-128-128-192 192-192-192c0-176.731 143.269-320 320-320s320 143.269 320 320v0z" />
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -18,7 +18,6 @@
@import "../plugins/folderView/components/list-item.scss"; @import "../plugins/folderView/components/list-item.scss";
@import "../plugins/folderView/components/list-view.scss"; @import "../plugins/folderView/components/list-view.scss";
@import "../plugins/imagery/components/imagery-view-layout.scss"; @import "../plugins/imagery/components/imagery-view-layout.scss";
@import "../plugins/plotlyPlot/components/plotly.scss";
@import "../plugins/telemetryTable/components/table-row.scss"; @import "../plugins/telemetryTable/components/table-row.scss";
@import "../plugins/telemetryTable/components/telemetry-filter-indicator.scss"; @import "../plugins/telemetryTable/components/telemetry-filter-indicator.scss";
@import "../plugins/tabs/components/tabs.scss"; @import "../plugins/tabs/components/tabs.scss";

View File

@ -90,7 +90,15 @@ export default {
this.openmct.objectViews.off('clearData', this.clearData); this.openmct.objectViews.off('clearData', this.clearData);
}, },
invokeEditModeHandler(editMode) { invokeEditModeHandler(editMode) {
this.currentView.onEditModeChange(editMode); let edit;
if (this.currentObject.locked) {
edit = false;
} else {
edit = editMode;
}
this.currentView.onEditModeChange(edit);
}, },
toggleEditView(editMode) { toggleEditView(editMode) {
this.clear(); this.clear();
@ -227,7 +235,11 @@ export default {
}, },
onDragOver(event) { onDragOver(event) {
if (this.hasComposableDomainObject(event)) { if (this.hasComposableDomainObject(event)) {
event.preventDefault(); if (this.isEditingAllowed()) {
event.preventDefault();
} else {
event.stopPropagation();
}
} }
}, },
addObjectToParent(event) { addObjectToParent(event) {
@ -283,6 +295,13 @@ export default {
this.currentView.onClearData(); this.currentView.onClearData();
} }
} }
},
isEditingAllowed() {
let browseObject = this.openmct.layout.$refs.browseObject.currentObject,
objectPath= this.currentObjectPath || this.objectPath,
parentObject = objectPath[1];
return [browseObject, parentObject, this.currentObject].every(object => !object.locked);
} }
} }
} }

View File

@ -58,7 +58,6 @@
&__label { &__label {
margin-left: $interiorMarginSm; margin-left: $interiorMarginSm;
margin-right: $interiorMargin;
white-space: nowrap; white-space: nowrap;
} }
} }

View File

@ -54,7 +54,6 @@ export default {
inject: ['openmct'], inject: ['openmct'],
components: { components: {
StylesInspectorView, StylesInspectorView,
// StylesInspectorView,
multipane, multipane,
pane, pane,
Elements, Elements,

View File

@ -143,7 +143,7 @@
&__label { &__label {
color: $colorInspectorPropName; color: $colorInspectorPropName;
&[title] { &[title]:not([title=""]) {
// When a cell has a title, assume it's helpful text // When a cell has a title, assume it's helpful text
cursor: help; cursor: help;
} }

View File

@ -41,7 +41,17 @@
/> />
<div class="l-browse-bar__actions"> <div class="l-browse-bar__actions">
<button <button
v-if="isViewEditable & !isEditing" v-if="isViewEditable && !isEditing"
:title="lockedOrUnlocked"
class="c-button"
:class="{
'icon-lock': domainObject.locked,
'icon-unlocked': !domainObject.locked
}"
@click="toggleLock(!domainObject.locked)"
></button>
<button
v-if="isViewEditable && !isEditing && !domainObject.locked"
class="l-browse-bar__actions__edit c-button c-button--major icon-pencil" class="l-browse-bar__actions__edit c-button c-button--major icon-pencil"
title="Edit" title="Edit"
@click="edit()" @click="edit()"
@ -161,6 +171,13 @@ export default {
return currentViewProvider.canEdit && currentViewProvider.canEdit(this.domainObject); return currentViewProvider.canEdit && currentViewProvider.canEdit(this.domainObject);
} }
return false; return false;
},
lockedOrUnlocked() {
if (this.domainObject.locked) {
return 'Locked for editing - click to unlock.';
} else {
return 'Unlocked for editing - click to lock.';
}
} }
}, },
watch: { watch: {
@ -271,6 +288,9 @@ export default {
}, },
goToParent() { goToParent() {
window.location.hash = this.parentUrl; window.location.hash = this.parentUrl;
},
toggleLock(flag) {
this.openmct.objects.mutate(this.domainObject, 'locked', flag);
} }
} }
} }

View File

@ -314,7 +314,7 @@
&__actions, &__actions,
&__end { &__end {
> * + * { > * + * {
margin-left: $interiorMarginSm; margin-left: $interiorMargin;
} }
} }