Compare commits

...

50 Commits

Author SHA1 Message Date
352fe8ea7c Merge branch 'plotly-test' of https://github.com/nasa/openmct into plotly-test 2020-07-13 10:23:02 -07:00
29650f54de add unique id to plotElement 2020-07-13 10:22:31 -07:00
02d00aeb07 resolved conflict 2020-07-09 13:00:10 -07:00
fd21594e4a resolved conflict 2020-07-09 11:17:34 -07:00
5d0beb4351 reverted commit 2020-07-09 11:13:29 -07:00
f88d3bcaf3 Merge branch 'master' into plotly-test 2020-07-01 09:51:27 -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
c1102ed4b1 resolved merge conflict 2020-07-01 09:31:09 -07:00
fcd8a9a9c9 hide y-axis on empty plot, purge and recreate plot after removing only telemetry object 2020-07-01 09:28:48 -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
5f729640b2 added removeTelemetryObject 2020-06-30 13:59:59 -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
5fc12c771a fixed yaxis title, trace names in legend, markers 2020-06-30 11:39:35 -07:00
7931177497 resolved merge 2020-06-30 10:18:44 -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
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
56794b0ed5 using smaller plotly bundle 2020-06-29 12:10:32 -07:00
0398679abc Merge branch 'master' of https://github.com/nasa/openmct into plotly-test 2020-06-29 11:52:48 -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
6dd8d448df Merge pull request #3116 from nasa/new-folder-action
New folder action
2020-06-25 13:27:28 -07:00
1bc60f8108 wip: refactoring 2020-06-25 13:26:00 -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
122f3efa1f removed extend argument 2020-06-25 10:41:59 -07:00
7e4aac028b Merge branch 'master' into new-folder-action 2020-06-25 10:08:26 -07:00
32791b442d wip: refactor 2020-06-24 14:55:00 -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
7aaccdb286 wip: refactoring 2020-06-24 11:34:03 -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
81 changed files with 2781 additions and 661 deletions

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
"version": "1.0.0-snapshot",
"description": "The Open MCT core platform",
"devDependencies": {
"angular": "1.7.9",
"angular": ">=1.8.0",
"angular-route": "1.4.14",
"babel-eslint": "8.2.6",
"comma-separated-values": "^3.6.4",
@ -59,7 +59,8 @@
"node-bourbon": "^4.2.3",
"node-sass": "^4.9.2",
"painterro": "^0.2.65",
"plotly.js-dist": "^1.54.1",
"plotly.js-basic-dist-min": "^1.54.6",
"plotly.js-gl2d-dist-min": "^1.54.5",
"printj": "^1.2.1",
"raw-loader": "^0.5.1",
"request": "^2.69.0",

View File

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

View File

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

View File

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

View File

@ -269,6 +269,7 @@ define([
this.install(this.plugins.ConditionWidget());
this.install(this.plugins.URLTimeSettingsSynchronizer());
this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction());
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

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

View File

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

View File

@ -21,7 +21,7 @@
*****************************************************************************/
import LADTableViewProvider from './LADTableViewProvider';
import LADTableSetViewProvider from './LADTableSetViewProvider';
import LADTableCompositionPolicy from './LADTableCompositionPolicy';
import ladTableCompositionPolicy from './LADTableCompositionPolicy';
export default function plugin() {
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
} from 'utils/openmctLocation';
const TIME_EVENTS = ['bounds', 'timeSystem', 'clock', 'clockOffsets'];
const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets'];
const SEARCH_MODE = 'tc.mode';
const SEARCH_TIME_SYSTEM = 'tc.timeSystem';
const SEARCH_START_BOUND = 'tc.startBound';
@ -42,6 +42,7 @@ export default class URLTimeSettingsSynchronizer {
this.destroy = this.destroy.bind(this);
this.updateTimeSettings = this.updateTimeSettings.bind(this);
this.setUrlFromTimeApi = this.setUrlFromTimeApi.bind(this);
this.updateBounds = this.updateBounds.bind(this);
openmct.on('start', this.initialize);
openmct.on('destroy', this.destroy);
@ -54,7 +55,7 @@ export default class URLTimeSettingsSynchronizer {
TIME_EVENTS.forEach(event => {
this.openmct.time.on(event, this.setUrlFromTimeApi);
});
this.openmct.time.on('bounds', this.updateBounds);
}
destroy() {
@ -65,6 +66,7 @@ export default class URLTimeSettingsSynchronizer {
TIME_EVENTS.forEach(event => {
this.openmct.time.off(event, this.setUrlFromTimeApi);
});
this.openmct.time.on('bounds', this.updateBounds);
}
updateTimeSettings() {
@ -72,7 +74,6 @@ export default class URLTimeSettingsSynchronizer {
if (!this.isUrlUpdateInProgress) {
let timeParameters = this.parseParametersFromUrl();
if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters);
} else {
@ -138,6 +139,12 @@ export default class URLTimeSettingsSynchronizer {
}
}
updateBounds(bounds, isTick) {
if (!isTick) {
this.setUrlFromTimeApi();
}
}
setUrlFromTimeApi() {
let searchParams = getAllSearchParams();
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.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
if (!this.criteria) {
this.criteria = [];
}
@ -178,10 +179,12 @@ export default class Condition extends EventEmitter {
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
newCriterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
let criterion = found.item;
criterion.unsubscribe();
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
this.criteria.splice(found.index, 1, newCriterion);
this.updateDescription();
}
@ -194,6 +197,9 @@ export default class Condition extends EventEmitter {
criterion.off('criterionUpdated', (obj) => {
this.handleCriterionUpdated(obj);
});
criterion.off('telemetryIsStale', (obj) => {
this.handleStaleCriterion(obj);
});
criterion.destroy();
this.criteria.splice(found.index, 1);
this.updateDescription();
@ -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() {
const triggerDescription = this.getTriggerDescription();
let description = '';

View File

@ -315,6 +315,10 @@ export default class ConditionManager extends EventEmitter {
condition.getResult(normalizedDatum);
});
this.updateCurrentCondition(timestamp);
}
updateCurrentCondition(timestamp) {
const currentCondition = this.getCurrentCondition();
this.emit('conditionSetResultUpdated',

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@
import TelemetryCriterion from './TelemetryCriterion';
import { evaluateResults } from "../utils/evaluator";
import { getLatestTimestamp } from '../utils/time';
import {getLatestTimestamp, subscribeForStaleness} from '../utils/time';
import { getOperatorText } from "@/plugins/condition/utils/operations";
export default class AllTelemetryCriterion extends TelemetryCriterion {
@ -41,6 +41,32 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
initialize() {
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
this.telemetryDataCache = {};
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(this.telemetryObjects || {});
}
}
subscribeForStaleData(telemetryObjects) {
if (!this.stalenessSubscription) {
this.stalenessSubscription = {};
}
Object.values(telemetryObjects).forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (!this.stalenessSubscription[id]) {
this.stalenessSubscription[id] = subscribeForStaleness((data) => {
this.handleStaleTelemetry(id, data);
}, this.input[0]*1000);
}
})
}
handleStaleTelemetry(id, data) {
if (this.telemetryDataCache) {
this.telemetryDataCache[id] = true;
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
this.emitEvent('telemetryIsStale', data);
}
isValid() {
@ -50,6 +76,9 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
updateTelemetryObjects(telemetryObjects) {
this.telemetryObjects = { ...telemetryObjects };
this.removeTelemetryDataCache();
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(this.telemetryObjects || {});
}
}
removeTelemetryDataCache() {
@ -63,6 +92,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
});
telemetryCacheIds.forEach(id => {
delete (this.telemetryDataCache[id]);
delete (this.stalenessSubscription[id]);
});
}
@ -96,7 +126,14 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
const validatedData = this.isValid() ? data : {};
if (validatedData) {
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
if (this.isStalenessCheck()) {
if (this.stalenessSubscription[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 => {
@ -162,7 +199,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
getDescription() {
const telemetryDescription = this.telemetry === 'all' ? 'all telemetry' : 'any telemetry';
let metadataValue = this.metadata;
let metadataValue = (this.metadata === 'dataReceived' ? '' : this.metadata);
let inputValue = this.input;
if (this.metadata) {
const telemetryObjects = Object.values(this.telemetryObjects);
@ -182,5 +219,9 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
destroy() {
delete this.telemetryObjects;
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 { OPERATIONS, getOperatorText } from '../utils/operations';
import { subscribeForStaleness } from "../utils/time";
export default class TelemetryCriterion extends EventEmitter {
@ -43,6 +44,7 @@ export default class TelemetryCriterion extends EventEmitter {
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.result = undefined;
this.stalenessSubscription = undefined;
this.initialize();
this.emitEvent('criterionUpdated', this);
@ -51,14 +53,40 @@ export default class TelemetryCriterion extends EventEmitter {
initialize() {
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData()
}
}
subscribeForStaleData() {
if (this.stalenessSubscription) {
this.stalenessSubscription.clear();
}
this.stalenessSubscription = subscribeForStaleness(this.handleStaleTelemetry.bind(this), this.input[0]*1000);
}
handleStaleTelemetry(data) {
this.result = true;
this.emitEvent('telemetryIsStale', data);
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
isStalenessCheck() {
return this.metadata && this.metadata === 'dataReceived';
}
isValidInput() {
return this.input instanceof Array && this.input.length;
}
updateTelemetryObjects(telemetryObjects) {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData()
}
}
createNormalizedDatum(telemetryDatum, endpoint) {
@ -91,7 +119,14 @@ export default class TelemetryCriterion extends EventEmitter {
getResult(data) {
const validatedData = this.isValid() ? data : {};
this.result = this.computeResult(validatedData);
if (this.isStalenessCheck()) {
if (this.stalenessSubscription) {
this.stalenessSubscription.update(validatedData);
}
this.result = false;
} else {
this.result = this.computeResult(validatedData);
}
}
requestLAD() {
@ -136,7 +171,7 @@ export default class TelemetryCriterion extends EventEmitter {
let comparator = this.findOperation(this.operation);
let params = [];
params.push(data[this.metadata]);
if (this.input instanceof Array && this.input.length) {
if (this.isValidInput()) {
this.input.forEach(input => params.push(input));
}
if (typeof comparator === 'function') {
@ -191,7 +226,7 @@ export default class TelemetryCriterion extends EventEmitter {
description = `Unknown ${this.metadata} ${getOperatorText(this.operation, this.input)}`;
} else {
const metadataObject = this.getMetaDataObject(this.telemetryObject, this.metadata);
const metadataValue = this.getMetadataValueFromMetaData(metadataObject) || this.metadata;
const metadataValue = this.getMetadataValueFromMetaData(metadataObject) || (this.metadata === 'dataReceived' ? '' : this.metadata);
const inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input;
description = `${this.telemetryObject.name} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`;
}
@ -202,5 +237,8 @@ export default class TelemetryCriterion extends EventEmitter {
destroy() {
delete this.telemetryObject;
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 Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils";
import ConditionManager from "@/plugins/condition/ConditionManager";
describe('the plugin', function () {
let conditionSetDefinition;
let mockConditionSetDomainObject;
let mockListener;
let element;
let child;
let openmct;
let testTelemetryObject;
beforeAll(() => {
resetApplicationState(openmct);
});
beforeEach((done) => {
testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"},
type: "test-object",
name: "Test Object",
telemetry: {
valueMetadatas: [{
key: "some-key",
name: "Some attribute",
hints: {
range: 2
}
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
}, {
key: "testSource",
source: "value",
name: "Test",
format: "string"
}]
}
};
openmct = createOpenMct();
openmct.install(new ConditionPlugin());
@ -55,6 +86,8 @@ describe('the plugin', function () {
type: 'conditionSet'
};
mockListener = jasmine.createSpy('mockListener');
conditionSetDefinition.initialize(mockConditionSetDomainObject);
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) {
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;
}
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': {
value: 'telemetry-view',
name: 'Alphanumeric',
@ -95,28 +95,34 @@ define(['lodash'], function (_) {
class: 'icon-tabular-realtime'
}
},
applicableViews = {
APPLICABLE_VIEWS = {
'telemetry-view': [
viewTypes['telemetry.plot.overlay'],
viewTypes.table
VIEW_TYPES['telemetry.plot.overlay'],
VIEW_TYPES['telemetry.plot.stacked'],
VIEW_TYPES.table
],
'telemetry.plot.overlay': [
viewTypes['telemetry.plot.stacked'],
viewTypes.table,
viewTypes['telemetry-view']
VIEW_TYPES['telemetry.plot.stacked'],
VIEW_TYPES.table,
VIEW_TYPES['telemetry-view']
],
'telemetry.plot.stacked': [
VIEW_TYPES['telemetry.plot.overlay'],
VIEW_TYPES.table,
VIEW_TYPES['telemetry-view']
],
'table': [
viewTypes['telemetry.plot.overlay'],
viewTypes['telemetry.plot.stacked'],
viewTypes['telemetry-view']
VIEW_TYPES['telemetry.plot.overlay'],
VIEW_TYPES['telemetry.plot.stacked'],
VIEW_TYPES['telemetry-view']
],
'telemetry-view-multi': [
viewTypes['telemetry.plot.overlay'],
viewTypes['telemetry.plot.stacked'],
viewTypes.table
VIEW_TYPES['telemetry.plot.overlay'],
VIEW_TYPES['telemetry.plot.stacked'],
VIEW_TYPES.table
],
'telemetry.plot.overlay-multi': [
viewTypes['telemetry.plot.stacked']
VIEW_TYPES['telemetry.plot.stacked']
]
};
@ -510,7 +516,7 @@ define(['lodash'], function (_) {
selectedItemType = 'telemetry-view';
}
let viewOptions = applicableViews[selectedItemType];
let viewOptions = APPLICABLE_VIEWS[selectedItemType];
if (viewOptions) {
return {
@ -533,7 +539,7 @@ define(['lodash'], function (_) {
domainObject: selectedParent,
icon: "icon-object",
title: "Merge into a telemetry table or plot",
options: applicableViews['telemetry-view-multi'],
options: APPLICABLE_VIEWS['telemetry-view-multi'],
method: function (option) {
displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value);
}
@ -546,7 +552,7 @@ define(['lodash'], function (_) {
domainObject: selectedParent,
icon: "icon-object",
title: "Merge into a stacked plot",
options: applicableViews['telemetry.plot.overlay-multi'],
options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'],
method: function (option) {
displayLayoutContext.mergeMultipleOverlayPlots(selection, option.value);
}
@ -590,7 +596,7 @@ define(['lodash'], function (_) {
let selectedParent = selectionPath[1].context.item;
let layoutItem = selectionPath[0].context.layoutItem;
if (!layoutItem) {
if (!layoutItem || selectedParent.locked) {
return;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,80 @@
/*****************************************************************************
* 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 = 'New Folder';
this.key = 'newFolder';
this.description = 'Create a new folder';
this.cssClass = 'icon-folder';
this._openmct = openmct;
this._dialogForm = {
name: "New Folder Name",
sections: [
{
rows: [
{
key: "name",
control: "textfield",
name: "Folder Name",
required: false
}
]
}
]
};
}
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, {}).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() {
this.addPopupMenuItems();
this.exportImageService = this.openmct.$injector.get('exportImageService');
},
methods: {
addPopupMenuItems() {
@ -205,7 +206,7 @@ export default {
},
openSnapshot() {
const self = this;
const snapshot = new Vue({
this.snapshot = new Vue({
data: () => {
return {
embed: self.embed
@ -213,14 +214,15 @@ export default {
},
methods: {
formatTime: self.formatTime,
annotateSnapshot: self.annotateSnapshot
annotateSnapshot: self.annotateSnapshot,
exportImage: self.exportImage
},
template: SnapshotTemplate
});
const snapshotOverlay = this.openmct.overlays.overlay({
element: snapshot.$mount().$el,
onDestroy: () => { snapshot.$destroy(true) },
element: this.snapshot.$mount().$el,
onDestroy: () => { this.snapshot.$destroy(true) },
size: 'large',
dismissable: true,
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() {
const self = this;
const previewAction = new PreviewAction(self.openmct);

View File

@ -15,14 +15,32 @@
<div class="l-browse-bar__snapshot-datetime">
SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</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">
<span class="title-label">Annotate</span>
</a>
</div>
</div>
<div class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
<div
ref="snapshot-image"
class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
>
</div>
</div>

View File

@ -19,12 +19,12 @@
this source code distribution or the Licensing information page available
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-object="domainObject">
</mct-representation>
</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-object="domainObject">
</mct-representation>

View File

@ -30,8 +30,7 @@ define([
'./MCTChartPointSet',
'./MCTChartAlarmPointSet',
'../draw/DrawLoader',
'../lib/eventHelpers',
'lodash'
'../lib/eventHelpers'
],
function (
MCTChartLineLinear,
@ -39,8 +38,7 @@ function (
MCTChartPointSet,
MCTChartAlarmPointSet,
DrawLoader,
eventHelpers,
_
eventHelpers
) {
var MARKER_SIZE = 6.0,

View File

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

View File

@ -22,11 +22,9 @@
define([
'lodash',
'EventEmitter',
'../lib/eventHelpers'
], function (
_,
EventEmitter,
eventHelpers
) {

View File

@ -22,11 +22,9 @@
define([
'lodash',
'EventEmitter',
'../lib/eventHelpers'
], function (
_,
EventEmitter,
eventHelpers
) {

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
<template>
<div class="l-view-section js-plotly-container"></div>
<div :id="plotId"
class="l-view-section">
</div>
</template>
<script>
import _ from 'lodash';
import Plotly from 'plotly.js-dist';
// import Plotly from 'plotly.js-gl2d-dist-min';
import Plotly from 'plotly.js-basic-dist-min';
import BoundedTableRowCollection from '../../telemetryTable/collections/BoundedTableRowCollection';
import TelemetryTableRow from '../../telemetryTable/TelemetryTableRow';
import TelemetryTableColumn from '../../telemetryTable/TelemetryTableColumn';
@ -18,7 +20,9 @@ export default {
outstandingRequests: 0,
subscriptions: {},
plotComposition: undefined,
timestampKey: this.openmct.time.timeSystem().key
timestampKey: this.openmct.time.timeSystem().key,
yAxisLabel: '',
plotId: this.openmct.objects.makeKeyString(this.domainObject.identifier)
}
},
computed: {
@ -30,7 +34,7 @@ export default {
}
},
mounted() {
this.plotElement = document.querySelector('.js-plotly-container');
this.plotElement = document.getElementById(this.plotId);
this.openmct.time.on('bounds', this.updateDomain);
this.openmct.time.on('bounds', this.updateData);
@ -77,6 +81,17 @@ export default {
let subscription = this.subscribe(telemetryObject);
this.subscriptions[keyString] = subscription;
},
removeTelemetryObject(identifier) {
const keyString = this.openmct.objects.makeKeyString(identifier);
const index = this.telemetryObjects.findIndex(object => identifier.key === object.identifier.key);
this.unsubscribe(keyString);
this.removeTraceForObject(this.telemetryObjects[index]);
this.telemetryObjects = this.telemetryObjects.filter(object => !(identifier.key === object.identifier.key));
if (!this.telemetryObjects.length) {
Plotly.purge(this.plotElement);
this.createPlot();
}
},
updateDomain(bounds, isTick) {
let newDomain = {
'xaxis.range': [
@ -101,7 +116,6 @@ export default {
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);
});
@ -116,6 +130,10 @@ export default {
this.boundedRows[keyString].add(newRow);
});
},
unsubscribe(keyString) {
this.subscriptions[keyString]();
delete this.subscriptions[keyString];
},
createPlot() {
let timeSystem = this.openmct.time.timeSystem();
let bounds = this.openmct.time.bounds();
@ -157,7 +175,8 @@ export default {
showgrid: false,
tickwidth: 3,
tickcolor: 'transparent',
autorange: true
autorange: true,
visible: false
},
margin: {
l: 40,
@ -173,8 +192,8 @@ export default {
[],
layout,
{
displayModeBar: false, // turns off hover-activated toolbar
staticPlot: true // turns off hover effects on datapoints
displayModeBar: true, // turns off hover-activated toolbar
staticPlot: false // turns off hover effects on datapoints
}
);
@ -201,7 +220,20 @@ export default {
this.traceIndices[keyString] = Object.keys(this.traceIndices).length;
this.recalculateTraceIndices();
Plotly.addTraces(this.plotElement, {type: "scattergl", x: [], y: []});
Plotly.addTraces(this.plotElement, {
x: [],
y: [],
name: telemetryObject.name,
type: "scattergl",
mode: 'lines+markers',
marker: {
size: 5
},
line: {
shape: 'linear',
width: 1.5
}
});
const metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
@ -212,7 +244,10 @@ export default {
}, {});
const limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
const valueFormatter = this.openmct.telemetry.getValueFormatter(this.openmct.telemetry.getMetadata(telemetryObject).valuesForHints(['range'])[0]);
let layout_update = {
yaxis: {title: valueFormatter.valueMetadata.name, visible: true}
};
Plotly.update(this.plotElement, {}, layout_update)
this.columnMaps[keyString] = columnMap;
this.limitEvaluators[keyString] = limitEvaluator;
@ -240,9 +275,6 @@ export default {
boundedRows.on('add', addRow);
this.boundedRowsUnlisteners[keyString] = [];
// boundedRows.on('remove', () => {
// console.log("removed rows");
// });
this.boundedRowsUnlisteners[keyString].push(() => {
boundedRows.off('add', addRow);

View File

@ -54,7 +54,8 @@ define([
'./themes/maelstrom',
'./themes/snow',
'./URLTimeSettingsSynchronizer/plugin',
'./notificationIndicator/plugin'
'./notificationIndicator/plugin',
'./newFolderAction/plugin'
], function (
_,
UTCTimeSystem,
@ -89,7 +90,8 @@ define([
Maelstrom,
Snow,
URLTimeSettingsSynchronizer,
NotificationIndicator
NotificationIndicator,
NewFolderAction
) {
var bundleMap = {
LocalStorage: 'platform/persistence/local',
@ -200,6 +202,7 @@ define([
plugins.ConditionWidget = ConditionWidgetPlugin.default;
plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default;
plugins.NotificationIndicator = NotificationIndicator.default;
plugins.NewFolderAction = NewFolderAction.default;
return plugins;
});

View File

@ -101,6 +101,12 @@ export default class RemoveAction {
appliesTo(objectPath) {
let parent = objectPath[1];
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 &&
parentType.definition.creatable &&

View File

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

View File

@ -24,7 +24,12 @@
ref="axisHolder"
class="c-conductor-axis"
@mousedown="dragStart($event)"
></div>
>
<div
class="c-conductor-axis__zoom-indicator"
:style="zoomStyle"
></div>
</div>
</template>
<script>
@ -43,52 +48,81 @@ const PIXELS_PER_TICK_WIDE = 200;
export default {
inject: ['openmct'],
props: {
bounds: {
viewBounds: {
type: Object,
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: {
bounds: {
handler(bounds) {
viewBounds: {
handler() {
this.setScale();
},
deep: true
}
},
mounted() {
let axisHolder = this.$refs.axisHolder;
let height = axisHolder.offsetHeight;
let vis = d3Selection.select(axisHolder)
.append("svg:svg")
.attr("width", "100%")
.attr("height", height);
let vis = d3Selection.select(this.$refs.axisHolder).append("svg:svg");
this.width = this.$refs.axisHolder.clientWidth;
this.xAxis = d3Axis.axisTop();
this.dragging = false;
// 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.setAxisDimensions();
this.setScale();
//Respond to changes in conductor
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
destroyed() {
},
methods: {
setAxisDimensions() {
const axisHolder = this.$refs.axisHolder;
const rect = axisHolder.getBoundingClientRect();
this.left = Math.round(rect.left);
this.width = axisHolder.clientWidth;
},
setScale() {
if (!this.width) {
return;
}
let timeSystem = this.openmct.time.timeSystem();
let bounds = this.bounds;
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 {
this.xScale.domain([bounds.start, bounds.end]);
this.xScale.domain(
[this.viewBounds.start, this.viewBounds.end]
);
}
this.xAxis.scale(this.xScale);
@ -102,7 +136,7 @@ export default {
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) {
//The D3 scale used depends on the type of time system as d3
@ -120,9 +154,8 @@ export default {
},
getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem();
let isFixed = this.openmct.time.clock() === undefined;
if (isFixed) {
if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat);
} else {
return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
@ -134,45 +167,131 @@ export default {
}).formatter;
},
dragStart($event) {
let isFixed = this.openmct.time.clock() === undefined;
if (isFixed) {
if (this.isFixed) {
this.dragStartX = $event.clientX;
if (this.altPressed) {
this.inPanMode = true;
}
document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.dragEnd, {
once: true
});
if (this.inZoomMode) {
this.startZoom();
}
}
},
drag($event) {
if (!this.dragging) {
this.dragging = true;
requestAnimationFrame(()=>{
let deltaX = $event.clientX - this.dragStartX;
let percX = deltaX / this.width;
let bounds = this.openmct.time.bounds();
let deltaTime = bounds.end - bounds.start;
let newStart = bounds.start - percX * deltaTime;
this.$emit('panAxis',{
start: newStart,
end: newStart + deltaTime
});
requestAnimationFrame(() => {
this.dragX = $event.clientX;
this.inPanMode ? this.pan() : this.zoom();
this.dragging = false;
})
} else {
console.log('Rejected drag due to RAF cap');
});
}
},
dragEnd() {
this.inPanMode ? this.endPan() : this.endZoom();
document.removeEventListener('mousemove', this.drag);
this.openmct.time.bounds({
start: this.bounds.start,
end: this.bounds.end
this.dragStartX = undefined;
this.dragX = undefined;
},
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() {
if (this.$refs.axisHolder.clientWidth !== this.width) {
this.width = this.$refs.axisHolder.clientWidth;
this.setAxisDimensions();
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) {
return {
key: 'fixed',
name: 'Fixed Timespan Mode',
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular'
}

View File

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

View File

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

View File

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

View File

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

View File

@ -148,6 +148,7 @@ $glyph-icon-cursor-lock: '\e929';
$glyph-icon-flag: '\e92a';
$glyph-icon-eye-disabled: '\e92b';
$glyph-icon-notebook-page: '\e92c';
$glyph-icon-unlocked: '\e92d';
$glyph-icon-arrows-right-left: '\ea00';
$glyph-icon-arrows-up-down: '\ea01';
$glyph-icon-bullet: '\ea02';
@ -198,6 +199,11 @@ $glyph-icon-export: '\ea2e';
$glyph-icon-font-size: '\ea2f';
$glyph-icon-clear-data: '\ea30';
$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-mode: '\eb01';
$glyph-icon-autoflow-tabular: '\eb02';
@ -240,6 +246,7 @@ $glyph-icon-command: '\eb26';
$glyph-icon-conditional: '\eb27';
$glyph-icon-condition-widget: '\eb28';
$glyph-icon-alphanumeric: '\eb29';
$glyph-icon-image-telemetry: '\eb2a';
/************************** GLYPHS AS DATA URI */
// Only objects have been converted, for use in Create menu and folder views

View File

@ -462,9 +462,17 @@ select {
text-shadow: $shdwMenuText;
padding: $interiorMarginSm;
box-shadow: $shdwMenu;
display: block;
display: flex;
flex-direction: column;
position: absolute;
z-index: 100;
> * {
flex: 0 0 auto;
//+ * {
// margin-top: $interiorMarginSm;
//}
}
}
@mixin menuInner() {
@ -502,6 +510,23 @@ select {
.c-menu {
@include menuOuter();
@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 {

View File

@ -84,6 +84,7 @@
.icon-flag { @include glyphBefore($glyph-icon-flag); }
.icon-eye-disabled { @include glyphBefore($glyph-icon-eye-disabled); }
.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-up-down { @include glyphBefore($glyph-icon-arrows-up-down); }
.icon-bullet { @include glyphBefore($glyph-icon-bullet); }
@ -134,6 +135,11 @@
.icon-font-size { @include glyphBefore($glyph-icon-font-size); }
.icon-clear-data { @include glyphBefore($glyph-icon-clear-data); }
.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-mode { @include glyphBefore($glyph-icon-activity-mode); }
.icon-autoflow-tabular { @include glyphBefore($glyph-icon-autoflow-tabular); }
@ -176,6 +182,7 @@
.icon-conditional { @include glyphBefore($glyph-icon-conditional); }
.icon-condition-widget { @include glyphBefore($glyph-icon-condition-widget); }
.icon-alphanumeric { @include glyphBefore($glyph-icon-alphanumeric); }
.icon-image-telemetry { @include glyphBefore($glyph-icon-image-telemetry); }
/************************** 12 PX CLASSES */
// TODO: sync with 16px redo as of 10/25/18

View File

@ -114,25 +114,6 @@ mct-plot {
.plot-wrapper-axis-and-display-area {
position: relative;
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 {
@ -294,6 +275,25 @@ mct-plot {
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,
@ -432,6 +432,7 @@ mct-plot {
&__wrapper {
// Holds view-control and both collapsed and expanded legends
flex: 1 1 auto;
overflow: auto; // Prevents collapsed legend from forcing scrollbars on higher parent containers
}
&__view-control {

View File

@ -893,7 +893,7 @@ body.desktop {
.grid-row {
.grid-cell {
padding: 3px $interiorMarginLg 3px 0;
&[title] {
&[title]:not([title=""]) {
// When a cell has a title, assume it's helpful text
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="&#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="&#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="&#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" />
@ -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="&#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="&#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="&#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" />
@ -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="&#xea30;" glyph-name="icon-clear-data" d="M632 520l-120-120-120 120-80-80 120-120-120-120 80-80 120 120 120-120 80 80-120 120 120 120-80 80zM512 832c-282.76 0-512-86-512-192v-640c0-106 229.24-192 512-192s512 86 512 192v640c0 106-229.24 192-512 192zM512 0c-176.731 0-320 143.269-320 320s143.269 320 320 320c176.731 0 320-143.269 320-320v0c0-176.731-143.269-320-320-320v0z" />
<glyph unicode="&#xea31;" glyph-name="icon-history" d="M576 768c-247.4 0-448-200.6-448-448h-128l192-192 192 192h-128c0 85.4 33.2 165.8 93.8 226.2 60.4 60.6 140.8 93.8 226.2 93.8s165.8-33.2 226.2-93.8c60.6-60.4 93.8-140.8 93.8-226.2s-33.2-165.8-93.8-226.2c-60.4-60.6-140.8-93.8-226.2-93.8s-165.8 33.2-226.2 93.8l-90.6-90.6c81-81 193-131.2 316.8-131.2 247.4 0 448 200.6 448 448s-200.6 448-448 448zM576 560c-26.6 0-48-21.4-48-48v-211.8l142-142c9.4-9.4 21.6-14 34-14s24.6 4.6 34 14c18.8 18.8 18.8 49.2 0 67.8l-114 114v172c0 26.6-21.4 48-48 48z" />
<glyph unicode="&#xea32;" glyph-name="icon-arrow-up-to-parent" horiz-adv-x="1056" d="M643.427 6.739c-81.955 0.697-148.179 67.065-148.642 149.010v395.872l296.871-247.393v197.914l-395.828 329.857-395.828-328.62v-197.502l296.871 246.156v-396.241c0-190.905 155.239-346.556 346.144-346.968l412.321-0.825 0.412 197.914z" />
<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="&#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" />
@ -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="&#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="&#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>

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -90,7 +90,15 @@ export default {
this.openmct.objectViews.off('clearData', this.clearData);
},
invokeEditModeHandler(editMode) {
this.currentView.onEditModeChange(editMode);
let edit;
if (this.currentObject.locked) {
edit = false;
} else {
edit = editMode;
}
this.currentView.onEditModeChange(edit);
},
toggleEditView(editMode) {
this.clear();
@ -227,7 +235,11 @@ export default {
},
onDragOver(event) {
if (this.hasComposableDomainObject(event)) {
event.preventDefault();
if (this.isEditingAllowed()) {
event.preventDefault();
} else {
event.stopPropagation();
}
}
},
addObjectToParent(event) {
@ -283,6 +295,13 @@ export default {
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 {
margin-left: $interiorMarginSm;
margin-right: $interiorMargin;
white-space: nowrap;
}
}

View File

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

View File

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

View File

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

View File

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