Compare commits

..

41 Commits

Author SHA1 Message Date
4992b99aa5 Refresh openmct require cache before each test 2020-04-21 13:40:34 -07:00
26838635b6 Ensures correct results are returned for conditions and criteria for a given telemetry datapoint (#2904)
* Ensures that results for a specific datapoint are evaluated atomically.
* Remove generating timestamp for telemetry data
* Get results directly instead of using events
* Refactor all/any telemetry criterion to use new evaluator
* Use current timesystem to compare latest
* AllTelemetryCriterion extends TelemetryCriterion

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:57:38 -07:00
11f2c35bb2 [Notebook]: Entries filter #2820 (#2864)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:33:50 -07:00
766f48c1ba Handles static and mixed styles for multiple items in a layout (#2907)
* Show non specific styles when updating multiple item styles
* Save sub object styles to it's domain object
* Layout UI tweak
* Fixes flexible layout bug.
* Fixes font size bug in telemetry view
* Fixes issues with newly places TVOs including transparent properties.
* Fixes #2908
* Say NO to 'transparent' === '__no_value'
- Fixes #2895;
* Ensure styles are correctly applied to domain objects and drawing objects when selected individually
* Ensure none treatment is correctly applied to objects when multple selecting
* Fix intial box border
* Tweaks to c-text-view layout
- Vertically center text;
- Normalize padding;
- Overflow: hidden;

* Tweaks to Clock and Timer layout
- Fixes #2893;
- Vertically center text;
- Normalize padding;
- Overflow: hidden;
- `position: absolute` when in Layout;

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:22:47 -07:00
da7b93f9b3 Notebook context menu (#2888)
Notebook popup menu fix
Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:17:01 -07:00
99c095a69f Fixed condition-improve-reorder branch (#2912)
* wip: changing to condition as drag target

* wip

* wip

* wip

* fixed dragging issues

* fixed dragging classes and added temp border on condition with dragging class

* Conditionals sanding and shimming

- CSS and `all-dragging`;

* wip

* fixed drag end issue and changed dragging class to go on parent condition h

* drag with counter

* wip

* wip

* wip

* return to logic in ConditionCollection.vue

* wip

* completed js part with highlighted c-condition-h on dragover

* restored grippy as draggable elem, improved isValidTarget

* fixed drag text bug

* added moveIndex prop in Condition.vue

* Conditionals drag reorder styling

- Moved `.is-drag-target` class up to conditions-h element;
- Renamed `.all-dragging` to `is-active-dragging`;
- Styling for `__drop-target` elements;

* fixed incorrect default for moveIndex in condition collection, unnecessary reset in condition

* fixed downward move reorder

* removed prevent from dragenter and drag leave, changed @blur to @change for name and output fields

* removed console log

* Repair merge-damaged conditionals.scss

- Manual merge from latest master;

* Test data layout tweaked

- Prevent c-cs__test-data__controls from collapsing;

Co-authored-by: Joel McKinnon <joel.g.mckinnon@nasa.gov>
Co-authored-by: Joel McKinnon <JoelMcKinnon@users.noreply.github.com>
2020-04-10 15:04:04 -07:00
f885e83505 Merge pull request #2911 from nasa/revert-2818-condition-improve-reorder
Revert "Condition improve reorder"
2020-04-10 14:35:41 -07:00
928bc4c68a Revert "Condition improve reorder (#2818)"
This reverts commit 46fedc1a30.
2020-04-10 14:32:48 -07:00
d5539c7ae4 Merge pull request #2898 from nasa/fix-enum-comparison
Fixes enum comparisons
2020-04-10 14:03:05 -07:00
c86a104fb6 Merge branch 'master' into fix-enum-comparison 2020-04-10 13:54:04 -07:00
46fedc1a30 Condition improve reorder (#2818)
* wip: changing to condition as drag target

* wip

* wip

* wip

* fixed dragging issues

* fixed dragging classes and added temp border on condition with dragging class

* Conditionals sanding and shimming

- CSS and `all-dragging`;

* wip

* fixed drag end issue and changed dragging class to go on parent condition h

* drag with counter

* wip

* wip

* wip

* return to logic in ConditionCollection.vue

* wip

* completed js part with highlighted c-condition-h on dragover

* restored grippy as draggable elem, improved isValidTarget

* fixed drag text bug

* added moveIndex prop in Condition.vue

* Conditionals drag reorder styling

- Moved `.is-drag-target` class up to conditions-h element;
- Renamed `.all-dragging` to `is-active-dragging`;
- Styling for `__drop-target` elements;

* fixed incorrect default for moveIndex in condition collection, unnecessary reset in condition

* fixed downward move reorder

* removed prevent from dragenter and drag leave, changed @blur to @change for name and output fields

* removed console log

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-04-10 10:02:33 -07:00
3b6ef9b44b Refactor duplicate code into functions 2020-04-09 15:33:52 -07:00
c68edd9b7d Fixes enum comparisons
Adds check for undefined
2020-04-09 09:14:31 -07:00
11574b7c40 Merge pull request #2861 from nasa/fix-telemetryview-styles
Styles for telemetry are stored on their container domain objects
2020-04-08 15:19:15 -07:00
abc2cd2413 Merge branch 'master' into fix-telemetryview-styles 2020-04-08 14:40:54 -07:00
5d74882646 Merge pull request #2871 from nasa/dave/conditionals-own-results
[Conditionals] Request and subscription results are mutually exclusive
2020-04-08 14:21:31 -07:00
9fe7f230e6 Merge branch 'master' into dave/conditionals-own-results 2020-04-08 14:16:50 -07:00
de4c5b3729 Disables conditional and static styles for hyperlinks and summary widgets. (#2887) 2020-04-08 12:30:59 -07:00
2a7901914a Merge branch 'master' into fix-telemetryview-styles 2020-04-08 11:57:33 -07:00
73b0fc6f79 Removes preventNone as per conversation with UX designer. 2020-04-08 11:46:12 -07:00
ddef16795c Conditionals and Notebook UI fixes (#2868)
- Significant fixes for Safari-compatible Flex layout in Condition Set
view;
- Changed visual approach to current-value section;
- Firefox scrollbar coloring
- Fix layout issues in Firefox;
- Consolidate Conditionals styles into single scss file;
- Fix test datum elements layout, better wrapping;
- Better approach to presence/absence of URL property in Condition
Widget;
- Fixes #2853;
- Fix errors in URL property handling in Condition Widget;
- Fixes #2853;
- Fixes #2867 - hide the View Switcher when an object is being edited;
- Refined titling on View Switcher and Notebook menu button;
- Cleaned up styles in l-browse-bar and moved into
ui/layout/layout.scss;
- Removed styles/_layout.scss;
- Hide the main view Edit button when in mobile
2020-04-08 09:36:23 -07:00
d188b9a056 do not mutate function args for criteria results either 2020-04-07 13:22:03 -07:00
f510f3edd0 Removes missed code 2020-04-07 11:59:24 -07:00
e05b0bb562 Address review comments:
Fixes telemetry view visibility and styling issue
Removes none option for border and background styles for drawing objects
2020-04-07 11:34:48 -07:00
713c5e9fb7 Merge branch 'master' into dave/conditionals-own-results 2020-04-06 18:56:08 -07:00
17bca04560 do not mutate function args 2020-04-06 18:53:48 -07:00
e0c5bca47d fix unit tests 2020-04-06 14:25:55 -07:00
cdc7c1af64 Removes unused data 2020-04-06 14:04:11 -07:00
3158baa998 Merge branch 'fix-telemetryview-styles' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-06 14:02:45 -07:00
698508fde4 Use the subobject view type to determine where styles should be saved 2020-04-06 14:02:06 -07:00
68a96989e1 Merge branch 'master' into fix-telemetryview-styles 2020-04-06 13:57:41 -07:00
46a6a43234 Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-06 13:56:30 -07:00
d41fc27b55 subscriptions should use latest timestamp 2020-04-06 11:17:59 -07:00
24bb96cc90 WIP detach request data from subscription data
extract getLatestTimestamp function
2020-04-03 18:39:26 -07:00
483ee173d6 pass correct args into off listener 2020-04-02 16:34:40 -07:00
8f81a45b9b Removes coverage branch 2020-04-02 10:30:23 -07:00
666459be87 Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-02 10:30:07 -07:00
d3fe2a6811 Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-01 15:52:55 -07:00
97b37edce4 Store telemetry styles on their container domain objects. 2020-04-01 15:51:40 -07:00
dd70bb470f Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-01 10:18:30 -07:00
072bf361de Store styles for telemetry on domain objects 2020-03-30 16:20:22 -07:00
68 changed files with 2277 additions and 1351 deletions

View File

@ -30,6 +30,7 @@ define([
"./src/controllers/CompositeController",
"./src/controllers/ColorController",
"./src/controllers/DialogButtonController",
"./src/controllers/SnapshotPreviewController",
"./res/templates/controls/autocomplete.html",
"./res/templates/controls/checkbox.html",
"./res/templates/controls/datetime.html",
@ -43,7 +44,8 @@ define([
"./res/templates/controls/menu-button.html",
"./res/templates/controls/dialog.html",
"./res/templates/controls/radio.html",
"./res/templates/controls/file-input.html"
"./res/templates/controls/file-input.html",
"./res/templates/controls/snap-view.html"
], function (
MCTForm,
MCTControl,
@ -54,6 +56,7 @@ define([
CompositeController,
ColorController,
DialogButtonController,
SnapshotPreviewController,
autocompleteTemplate,
checkboxTemplate,
datetimeTemplate,
@ -67,7 +70,8 @@ define([
menuButtonTemplate,
dialogTemplate,
radioTemplate,
fileInputTemplate
fileInputTemplate,
snapViewTemplate
) {
return {
@ -153,6 +157,10 @@ define([
{
"key": "file-input",
"template": fileInputTemplate
},
{
"key": "snap-view",
"template": snapViewTemplate
}
],
"controllers": [
@ -186,6 +194,14 @@ define([
"$scope",
"dialogService"
]
},
{
"key": "SnapshotPreviewController",
"implementation": SnapshotPreviewController,
"depends": [
"$scope",
"openmct"
]
}
],
"components": [

View File

@ -0,0 +1,36 @@
<!--
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.
-->
<span ng-controller="SnapshotPreviewController"
class='form-control shell'>
<span class='field control {{structure.cssClass}}'>
<image
class="c-ne__embed__snap-thumb"
src="{{imageUrl || structure.src}}"
ng-click="previewImage(imageUrl || structure.src)"
name="mctControl">
</image>
<br>
<a title="Annotate" class="s-button icon-pencil" ng-click="annotateImage(ngModel, field, imageUrl || structure.src)">
<span class="title-label">Annotate</span>
</a>
</span>
</span>

View File

@ -0,0 +1,131 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'painterro'
],
function (Painterro) {
function SnapshotPreviewController($scope, openmct) {
$scope.previewImage = function (imageUrl) {
let imageDiv = document.createElement('div');
imageDiv.classList = 'image-main s-image-main';
imageDiv.style.backgroundImage = `url(${imageUrl})`;
let previewImageOverlay = openmct.overlays.overlay(
{
element: imageDiv,
size: 'large',
buttons: [
{
label: 'Done',
callback: function () {
previewImageOverlay.dismiss();
}
}
]
}
);
};
$scope.annotateImage = function (ngModel, field, imageUrl) {
$scope.imageUrl = imageUrl;
let div = document.createElement('div'),
painterroInstance = {},
save = false;
div.id = 'snap-annotation';
let annotateImageOverlay = openmct.overlays.overlay(
{
element: div,
size: 'large',
buttons: [
{
label: 'Cancel',
callback: function () {
save = false;
painterroInstance.save();
annotateImageOverlay.dismiss();
}
},
{
label: 'Save',
callback: function () {
save = true;
painterroInstance.save();
annotateImageOverlay.dismiss();
}
}
]
}
);
painterroInstance = Painterro({
id: 'snap-annotation',
activeColor: '#ff0000',
activeColorAlpha: 1.0,
activeFillColor: '#fff',
activeFillColorAlpha: 0.0,
backgroundFillColor: '#000',
backgroundFillColorAlpha: 0.0,
defaultFontSize: 16,
defaultLineWidth: 2,
defaultTool: 'ellipse',
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
translation: {
name: 'en',
strings: {
lineColor: 'Line',
fillColor: 'Fill',
lineWidth: 'Size',
textColor: 'Color',
fontSize: 'Size',
fontStyle: 'Style'
}
},
saveHandler: function (image, done) {
if (save) {
let url = image.asBlob(),
reader = new window.FileReader();
reader.readAsDataURL(url);
reader.onloadend = function () {
$scope.imageUrl = reader.result;
ngModel[field] = reader.result;
};
} else {
ngModel.field = imageUrl;
console.warn('You cancelled the annotation!!!');
}
done(true);
}
}).show(imageUrl);
};
}
return SnapshotPreviewController;
}
);

View File

@ -23,8 +23,8 @@
import EventEmitter from 'EventEmitter';
import uuid from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { TRIGGER } from "./utils/constants";
import {computeCondition, computeConditionByLimit} from "./utils/evaluator";
import { evaluateResults } from './utils/evaluator';
import { getLatestTimestamp } from './utils/time';
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
/*
@ -56,28 +56,40 @@ export default class ConditionClass extends EventEmitter {
this.conditionManager = conditionManager;
this.id = conditionConfiguration.id;
this.criteria = [];
this.criteriaResults = {};
this.result = undefined;
this.latestTimestamp = {};
this.timeSystems = this.openmct.time.getAllTimeSystems();
if (conditionConfiguration.configuration.criteria) {
this.createCriteria(conditionConfiguration.configuration.criteria);
}
this.trigger = conditionConfiguration.configuration.trigger;
this.conditionManager.on('broadcastTelemetry', this.handleBroadcastTelemetry, this);
}
handleBroadcastTelemetry(datum) {
getResult(datum) {
if (!datum || !datum.id) {
console.log('no data received');
return;
}
this.criteria.forEach(criterion => {
if (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')) {
criterion.handleSubscription(datum, this.conditionManager.telemetryObjects);
} else {
criterion.emit(`subscription:${datum.id}`, datum);
if (!this.isTelemetryUsed(datum.id)) {
return;
}
this.criteria.forEach(criterion => {
if (this.isAnyOrAllTelemetry(criterion)) {
criterion.getResult(datum, this.conditionManager.telemetryObjects);
} else {
criterion.getResult(datum);
}
});
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
}
isAnyOrAllTelemetry(criterion) {
return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
}
isTelemetryUsed(id) {
return this.criteria.some(criterion => {
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
});
}
@ -89,7 +101,6 @@ export default class ConditionClass extends EventEmitter {
updateTrigger(trigger) {
if (this.trigger !== trigger) {
this.trigger = trigger;
this.handleConditionUpdated();
}
}
@ -133,7 +144,6 @@ export default class ConditionClass extends EventEmitter {
criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);
}
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
if (!this.criteria) {
this.criteria = [];
}
@ -162,22 +172,11 @@ export default class ConditionClass extends EventEmitter {
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
newCriterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
let criterion = found.item;
criterion.unsubscribe();
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
this.criteria.splice(found.index, 1, newCriterion);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id];
}
}
}
removeCriterion(id) {
if (this.destroyCriterion(id)) {
this.handleConditionUpdated();
}
}
@ -185,15 +184,12 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(id);
if (found) {
let criterion = found.item;
criterion.destroy();
// TODO this is passing the wrong args
criterion.off('criterionUpdated', (result) => {
this.handleCriterionUpdated(id, result);
criterion.off('criterionUpdated', (obj) => {
this.handleCriterionUpdated(obj);
});
criterion.destroy();
this.criteria.splice(found.index, 1);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id];
}
return true;
}
return false;
@ -203,59 +199,40 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(criterion.id);
if (found) {
this.criteria[found.index] = criterion.data;
// TODO nothing is listening to this
this.emitEvent('conditionUpdated', {
trigger: this.trigger,
criteria: this.criteria
});
}
}
updateCriteriaResults(eventData) {
const id = eventData.id;
if (this.findCriterion(id)) {
// The !! here is important to convert undefined to false otherwise the criteriaResults won't get deleted when the criteria is destroyed
this.criteriaResults[id] = !!eventData.data.result;
}
}
handleCriterionResult(eventData) {
this.updateCriteriaResults(eventData);
this.handleConditionUpdated(eventData.data);
}
requestLADConditionResult() {
const criteriaResults = this.criteria
let latestTimestamp;
let criteriaResults = {};
const criteriaRequests = this.criteria
.map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects}));
return Promise.all(criteriaResults)
return Promise.all(criteriaRequests)
.then(results => {
results.forEach(result => {
this.updateCriteriaResults(result);
this.latestTimestamp = this.getLatestTimestamp(this.latestTimestamp, result.data)
results.forEach(resultObj => {
const { id, data, data: { result } } = resultObj;
if (this.findCriterion(id)) {
criteriaResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
this.evaluate();
return {
id: this.id,
data: Object.assign({}, this.latestTimestamp, { result: this.result })
}
data: Object.assign(
{},
latestTimestamp,
{ result: evaluateResults(Object.values(criteriaResults), this.trigger) }
)
};
});
}
getTelemetrySubscriptions() {
return this.criteria.map(criterion => criterion.telemetryObjectIdAsString);
}
handleConditionUpdated(datum) {
// trigger an updated event so that consumers can react accordingly
this.evaluate();
this.emitEvent('conditionResultUpdated',
Object.assign({}, datum, { result: this.result })
);
}
getCriteria() {
return this.criteria;
}
@ -269,41 +246,7 @@ export default class ConditionClass extends EventEmitter {
return success;
}
evaluate() {
if (this.trigger && this.trigger === TRIGGER.XOR) {
this.result = computeConditionByLimit(this.criteriaResults, 1);
} else if (this.trigger && this.trigger === TRIGGER.NOT) {
this.result = computeConditionByLimit(this.criteriaResults, 0);
} else {
this.result = computeCondition(this.criteriaResults, this.trigger === TRIGGER.ALL);
}
}
getLatestTimestamp(current, compare) {
const timestamp = Object.assign({}, current);
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
if (!timestamp[timeSystem.key]
|| compare[timeSystem.key] > timestamp[timeSystem.key]
) {
timestamp[timeSystem.key] = compare[timeSystem.key];
}
});
return timestamp;
}
emitEvent(eventName, data) {
this.emit(eventName, {
id: this.id,
data: data
});
}
destroy() {
this.conditionManager.off('broadcastTelemetry', this.handleBroadcastTelemetry, this);
if (typeof this.stopObservingForChanges === 'function') {
this.stopObservingForChanges();
}
this.destroyCriteria();
}
}

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import Condition from "./Condition";
import { getLatestTimestamp } from './utils/time';
import uuid from "uuid";
import EventEmitter from 'EventEmitter';
@ -29,8 +30,7 @@ export default class ConditionManager extends EventEmitter {
super();
this.openmct = openmct;
this.conditionSetDomainObject = conditionSetDomainObject;
this.timeAPI = this.openmct.time;
this.latestTimestamp = {};
this.timeSystems = this.openmct.time.getAllTimeSystems();
this.composition = this.openmct.composition.get(conditionSetDomainObject);
this.composition.on('add', this.subscribeToTelemetry, this);
this.composition.on('remove', this.unsubscribeFromTelemetry, this);
@ -55,8 +55,9 @@ export default class ConditionManager extends EventEmitter {
this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas});
this.subscriptions[id] = this.openmct.telemetry.subscribe(
endpoint,
this.broadcastTelemetry.bind(this, id)
this.telemetryReceived.bind(this, id)
);
// TODO check if this is needed
this.updateConditionTelemetry();
}
@ -73,7 +74,6 @@ export default class ConditionManager extends EventEmitter {
}
initialize() {
this.conditionResults = {};
this.conditionClassCollection = [];
if (this.conditionSetDomainObject.configuration.conditionCollection.length) {
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
@ -95,7 +95,6 @@ export default class ConditionManager extends EventEmitter {
initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this);
condition.on('conditionResultUpdated', this.handleConditionResult.bind(this));
if (index !== undefined) {
this.conditionClassCollection.splice(index + 1, 0, condition);
} else {
@ -159,22 +158,16 @@ export default class ConditionManager extends EventEmitter {
removeCondition(index) {
let condition = this.conditionClassCollection[index];
condition.destroyCriteria();
condition.off('conditionResultUpdated', this.handleConditionResult.bind(this));
condition.destroy();
this.conditionClassCollection.splice(index, 1);
this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1);
if (this.conditionResults[condition.id] !== undefined) {
delete this.conditionResults[condition.id];
}
this.persistConditions();
this.handleConditionResult();
}
findConditionById(id) {
return this.conditionClassCollection.find(conditionClass => conditionClass.id === id);
}
//this.$set(this.conditionClassCollection, reorderEvent.newIndex, oldConditions[reorderEvent.oldIndex]);
reorderConditions(reorderPlan) {
let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection);
let newCollection = [];
@ -191,7 +184,23 @@ export default class ConditionManager extends EventEmitter {
let currentCondition = conditionCollection[conditionCollection.length-1];
for (let i = 0; i < conditionCollection.length - 1; i++) {
if (this.conditionResults[conditionCollection[i].id]) {
const condition = this.findConditionById(conditionCollection[i].id)
if (condition.result) {
//first condition to be true wins
currentCondition = conditionCollection[i];
break;
}
}
return currentCondition;
}
getCurrentConditionLAD(conditionResults) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length-1];
for (let i = 0; i < conditionCollection.length - 1; i++) {
if (conditionResults[conditionCollection[i].id]) {
//first condition to be true wins
currentCondition = conditionCollection[i];
break;
@ -200,59 +209,32 @@ export default class ConditionManager extends EventEmitter {
return currentCondition;
}
updateConditionResults(resultObj) {
if (!resultObj) {
return;
}
const id = resultObj.id;
if (this.findConditionById(id)) {
this.conditionResults[id] = resultObj.data.result;
}
this.updateTimestamp(resultObj.data);
}
handleConditionResult(resultObj) {
// update conditions results and then calculate the current condition
this.updateConditionResults(resultObj);
const currentCondition = this.getCurrentCondition();
this.emit('conditionSetResultUpdated',
Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
this.latestTimestamp
)
)
}
updateTimestamp(timestamp) {
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
if (!this.latestTimestamp[timeSystem.key]
|| timestamp[timeSystem.key] > this.latestTimestamp[timeSystem.key]
) {
this.latestTimestamp[timeSystem.key] = timestamp[timeSystem.key];
}
});
}
requestLADConditionSetOutput() {
if (!this.conditionClassCollection.length) {
return Promise.resolve([]);
}
return this.compositionLoad.then(() => {
const ladConditionResults = this.conditionClassCollection
let latestTimestamp;
let conditionResults = {};
const conditionRequests = this.conditionClassCollection
.map(condition => condition.requestLADConditionResult());
return Promise.all(ladConditionResults)
return Promise.all(conditionRequests)
.then((results) => {
results.forEach(resultObj => { this.updateConditionResults(resultObj); });
const currentCondition = this.getCurrentCondition();
results.forEach(resultObj => {
const { id, data, data: { result } } = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
const currentCondition = this.getCurrentConditionLAD(conditionResults);
return Object.assign(
{
@ -260,14 +242,34 @@ export default class ConditionManager extends EventEmitter {
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
this.latestTimestamp
latestTimestamp
);
});
});
}
broadcastTelemetry(id, datum) {
this.emit(`broadcastTelemetry`, Object.assign({}, this.createNormalizedDatum(datum, id), {id: id}));
telemetryReceived(id, datum) {
const normalizedDatum = this.createNormalizedDatum(datum, id);
const timeSystemKey = this.openmct.time.timeSystem().key;
let timestamp = {};
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
this.conditionClassCollection.forEach(condition => {
condition.getResult(normalizedDatum);
});
const currentCondition = this.getCurrentCondition();
this.emit('conditionSetResultUpdated',
Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
timestamp
)
)
}
getTestData(metadatum) {
@ -282,12 +284,16 @@ export default class ConditionManager extends EventEmitter {
}
createNormalizedDatum(telemetryDatum, id) {
return Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((normalizedDatum, metadatum) => {
const normalizedDatum = Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((datum, metadatum) => {
const testValue = this.getTestData(metadatum);
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
normalizedDatum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
return normalizedDatum;
datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
return datum;
}, {});
normalizedDatum.id = id;
return normalizedDatum;
}
updateTestData(testData) {
@ -303,14 +309,13 @@ export default class ConditionManager extends EventEmitter {
this.composition.off('add', this.subscribeToTelemetry, this);
this.composition.off('remove', this.unsubscribeFromTelemetry, this);
Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe());
this.subscriptions = undefined;
delete this.subscriptions;
if(this.stopObservingForChanges) {
this.stopObservingForChanges();
}
this.conditionClassCollection.forEach((condition) => {
condition.off('conditionResultUpdated', this.handleConditionResult);
condition.destroy();
})
}

View File

@ -49,6 +49,7 @@ describe('ConditionManager', () => {
};
let mockComposition;
let loader;
let mockTimeSystems;
function mockAngularComponents() {
let mockInjector = jasmine.createSpyObj('$injector', ['get']);
@ -111,10 +112,16 @@ describe('ConditionManager', () => {
openmct.objects.observe.and.returnValue(function () {});
openmct.objects.mutate.and.returnValue(function () {});
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.on('broadcastTelemetry', mockListener);
conditionMgr.on('telemetryReceived', mockListener);
});
it('creates a conditionCollection with a default condition', function () {

View File

@ -25,12 +25,12 @@ import {TRIGGER} from "./utils/constants";
import TelemetryCriterion from "./criterion/TelemetryCriterion";
let openmct = {},
mockListener,
testConditionDefinition,
testTelemetryObject,
conditionObj,
conditionManager,
mockBroadcastTelemetry;
mockTelemetryReceived,
mockTimeSystems;
describe("The condition", function () {
@ -38,10 +38,9 @@ describe("The condition", function () {
conditionManager = jasmine.createSpyObj('conditionManager',
['on']
);
mockBroadcastTelemetry = jasmine.createSpy('listener');
conditionManager.on('broadcastTelemetry', mockBroadcastTelemetry);
mockTelemetryReceived = jasmine.createSpy('listener');
conditionManager.on('telemetryReceived', mockTelemetryReceived);
mockListener = jasmine.createSpy('listener');
testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"},
type: "test-object",
@ -74,6 +73,12 @@ describe("The condition", function () {
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values);
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
testConditionDefinition = {
id: '123-456',
configuration: {
@ -97,8 +102,6 @@ describe("The condition", function () {
openmct,
conditionManager
);
conditionObj.on('conditionUpdated', mockListener);
});
it("generates criteria with the correct properties", function () {

View File

@ -109,7 +109,7 @@ export default class StyleRuleManager extends EventEmitter {
} else {
if (this.currentStyle) {
Object.keys(this.currentStyle).forEach(key => {
this.currentStyle[key] = 'transparent';
this.currentStyle[key] = '__no_value';
});
}
}

View File

@ -21,8 +21,16 @@
*****************************************************************************/
<template>
<div class="c-condition-h"
:class="{ 'is-drag-target': draggingOver }"
@dragover.prevent
@drop.prevent="dropCondition($event, conditionIndex)"
@dragenter="dragEnter($event, conditionIndex)"
@dragleave="dragLeave($event, conditionIndex)"
>
<div class="c-condition-h__drop-target"></div>
<div v-if="isEditing"
class="c-condition c-condition--edit js-condition-drag-wrapper"
class="c-condition c-condition--edit"
>
<!-- Edit view -->
<div class="c-condition__header">
@ -31,8 +39,7 @@
:class="[{ 'is-enabled': !condition.isDefault }, { 'hide-nice': condition.isDefault }]"
:draggable="!condition.isDefault"
@dragstart="dragStart"
@dragstop="dragStop"
@dragover.stop
@dragend="dragEnd"
></span>
<span class="c-condition__disclosure c-disclosure-triangle c-tree__item__view-control is-enabled"
@ -75,7 +82,7 @@
<input v-model="condition.configuration.name"
class="t-condition-input__name"
type="text"
@blur="persist"
@change="persist"
>
</span>
@ -98,7 +105,7 @@
v-model="condition.configuration.output"
class="t-condition-name-input"
type="text"
@blur="persist"
@change="persist"
>
</span>
</span>
@ -176,6 +183,7 @@
/>
</div>
</div>
</div>
</template>
<script>
@ -207,6 +215,14 @@ export default {
type: Array,
required: true,
default: () => []
},
isDragging: {
type: Boolean,
default: false
},
moveIndex: {
type: Number,
default: 0
}
},
data() {
@ -217,8 +233,8 @@ export default {
selectedOutputSelection: '',
outputOptions: ['false', 'true', 'string'],
criterionIndex: 0,
selectedTelemetryName: '',
selectedFieldName: ''
draggingOver: false,
isDefault: this.condition.isDefault
};
},
computed: {
@ -286,11 +302,39 @@ export default {
dragStart(e) {
e.dataTransfer.setData('dragging', e.target); // required for FF to initiate drag
e.dataTransfer.effectAllowed = "copyMove";
e.dataTransfer.setDragImage(e.target.closest('.js-condition-drag-wrapper'), 0, 0);
e.dataTransfer.setDragImage(e.target.closest('.c-condition-h'), 0, 0);
this.$emit('setMoveIndex', this.conditionIndex);
},
dragStop(e) {
e.dataTransfer.clearData();
dragEnd(event) {
this.dragStarted = false;
event.dataTransfer.clearData();
this.$emit('dragComplete');
},
dropCondition(event, targetIndex) {
if (!this.isDragging) { return }
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.isValidTarget(targetIndex)) {
this.dragElement = undefined;
this.draggingOver = false;
this.$emit('dropCondition', targetIndex);
}
},
dragEnter(event, targetIndex) {
if (!this.isDragging) { return }
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.isValidTarget(targetIndex)) {
this.dragElement = event.target.parentElement;
this.draggingOver = true;
}
},
dragLeave(event) {
if (event.target.parentElement === this.dragElement) {
this.draggingOver = false;
this.dragElement = undefined;
}
},
isValidTarget(targetIndex) {
return this.moveIndex !== targetIndex;
},
destroy() {
},

View File

@ -22,7 +22,6 @@
<template>
<section id="conditionCollection"
class="c-cs__conditions"
:class="{ 'is-expanded': expanded }"
>
<div class="c-cs__header c-section__header">
@ -53,30 +52,26 @@
<span class="c-cs-button__label">Add Condition</span>
</button>
<div class="c-cs__conditions-h">
<div v-for="(condition, index) in conditionCollection"
:key="condition.id"
class="c-condition-h"
<div class="c-cs__conditions-h"
:class="{ 'is-active-dragging': isDragging }"
>
<div v-if="isEditing"
class="c-c__drag-ghost"
@drop.prevent="dropCondition"
@dragenter="dragEnter"
@dragleave="dragLeave"
@dragover.prevent
></div>
<Condition :condition="condition"
<Condition v-for="(condition, index) in conditionCollection"
:key="condition.id"
:condition="condition"
:condition-index="index"
:telemetry="telemetryObjs"
:is-editing="isEditing"
:move-index="moveIndex"
:is-dragging="isDragging"
@updateCondition="updateCondition"
@removeCondition="removeCondition"
@cloneCondition="cloneCondition"
@setMoveIndex="setMoveIndex"
@dragComplete="dragComplete"
@dropCondition="dropCondition"
/>
</div>
</div>
</div>
</section>
</template>
@ -109,9 +104,10 @@ export default {
conditionResults: {},
conditions: [],
telemetryObjs: [],
moveIndex: Number,
moveIndex: undefined,
isDragging: false,
defaultOutput: undefined
defaultOutput: undefined,
dragCounter: 0
};
},
watch: {
@ -166,9 +162,7 @@ export default {
this.moveIndex = index;
this.isDragging = true;
},
dropCondition(e) {
let targetIndex = Array.from(document.querySelectorAll('.c-c__drag-ghost')).indexOf(e.target);
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
dropCondition(targetIndex) {
const oldIndexArr = Object.keys(this.conditionCollection);
const move = function (arr, old_index, new_index) {
while (old_index < 0) {
@ -194,20 +188,10 @@ export default {
}
this.reorder(reorderPlan);
e.target.classList.remove("dragging");
},
dragComplete() {
this.isDragging = false;
},
dragEnter(e) {
if (!this.isDragging) { return }
let targetIndex = Array.from(document.querySelectorAll('.c-c__drag-ghost')).indexOf(e.target);
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.moveIndex === targetIndex) { return }
e.target.classList.add("dragging");
},
dragLeave(e) {
e.target.classList.remove("dragging");
},
addTelemetryObject(domainObject) {
this.telemetryObjs.push(domainObject);
this.$emit('telemetryUpdated', this.telemetryObjs);

View File

@ -23,24 +23,26 @@
<template>
<div class="c-cs">
<section class="c-cs__current-output c-section">
<div class="c-cs__header c-section__header">
<span class="c-cs__header-label c-section__label">Current Output</span>
</div>
<div class="c-cs__content c-cs__current-output-value">
<span class="c-cs__current-output-value__label">Current Output</span>
<span class="c-cs__current-output-value__value">
<template v-if="currentConditionOutput">
{{ currentConditionOutput }}
</template>
<template v-else>
{{ defaultConditionOutput }}
</template>
</span>
</div>
</section>
<TestData :is-editing="isEditing"
<div class="c-cs__test-data-and-conditions-w">
<TestData class="c-cs__test-data"
:is-editing="isEditing"
:test-data="testData"
:telemetry="telemetryObjs"
@updateTestData="updateTestData"
/>
<ConditionCollection
<ConditionCollection class="c-cs__conditions"
:is-editing="isEditing"
:test-data="testData"
@conditionSetResultUpdated="updateCurrentOutput"
@ -48,6 +50,7 @@
@telemetryUpdated="updateTelemetry"
/>
</div>
</div>
</template>
<script>

View File

@ -23,7 +23,6 @@
<template>
<section v-show="isEditing"
id="test-data"
class="c-cs__test-data"
:class="{ 'is-expanded': expanded }"
>
<div class="c-cs__header c-section__header">
@ -37,7 +36,7 @@
<div v-if="expanded"
class="c-cs__content"
>
<div class="c-cdef__controls"
<div class="c-cs__test-data__controls c-cdef__controls"
:disabled="!telemetry.length"
>
<label class="c-toggle-switch">
@ -96,7 +95,7 @@
>
</span>
</span>
<div class="c-test-datum__buttons">
<div class="c-cs-test__buttons">
<button class="c-click-icon c-test-data__duplicate-button icon-duplicate"
title="Duplicate this test datum"
@click="addTestInput(testInput)"

View File

@ -1,116 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.c-cs {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
&__content {
display: flex;
flex-direction: column;
flex: 0 1 auto;
overflow: hidden;
> * {
flex: 0 0 auto;
overflow: hidden;
+ * {
margin-top: $interiorMarginSm;
}
}
.c-button {
align-self: start;
}
}
.is-editing & {
// Add some space to kick away from blue editing border indication
padding: $interiorMargin;
}
section {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__conditions-h {
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
&__conditions {
> * + * {
margin-top: $interiorMarginSm;
}
}
.hint {
padding: $interiorMarginSm;
}
/************************** SPECIFIC ITEMS */
&__current-output-value {
font-size: 1.25em;
padding: $interiorMargin;
}
}
/***************************** TEST DATA */
.c-cs-tests {
flex: 0 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.c-cs-test {
> * {
flex: 0 0 auto;
+ * {
margin-left: $interiorMargin;
}
}
&__controls {
display: flex;
flex: 1 1 auto;
> * + * {
margin-left: $interiorMargin;
}
}
}

View File

@ -1,133 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.c-condition,
.c-test-datum {
@include discreteItem();
display: flex;
padding: $interiorMargin;
&--edit {
line-height: 160%; // For layout when inputs wrap, like in criteria
}
}
.c-condition {
flex-direction: column;
min-width: 400px;
> * + * {
margin-top: $interiorMarginSm;
}
&--browse {
.c-condition__summary {
border-top: 1px solid $colorInteriorBorder;
padding-top: $interiorMargin;
}
}
/***************************** HEADER */
&__header {
$h: 22px;
display: flex;
align-items: start;
align-content: stretch;
overflow: hidden;
min-height: $h;
line-height: $h;
> * {
flex: 0 0 auto;
+ * {
margin-left: $interiorMarginSm;
}
}
}
&__drag-grippy {
transform: translateY(50%);
}
&__name {
font-weight: bold;
align-self: baseline; // Fixes bold line-height offset problem
}
&__output,
&__summary {
flex: 1 1 auto;
}
}
/***************************** CONDITION DEFINITION, EDITING */
.c-cdef {
display: grid;
grid-row-gap: $interiorMarginSm;
grid-column-gap: $interiorMargin;
grid-auto-columns: min-content 1fr max-content;
align-items: start;
min-width: 150px;
margin-left: 29px;
overflow: hidden;
&__criteria,
&__match-and-criteria {
display: contents;
}
&__label {
grid-column: 1;
text-align: right;
white-space: nowrap;
}
&__separator {
grid-column: 1 / span 3;
}
&__controls {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
grid-column: 2;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
grid-column: 3;
}
}
.c-c__drag-ghost {
width: 100%;
min-height: $interiorMarginSm;
&.dragging {
min-height: 5em;
background-color: lightblue;
border-radius: 2px;
}
}

View File

@ -0,0 +1,311 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/***************************** DRAGGING */
.is-active-dragging {
.c-condition-h__drop-target {
height: 3px;
margin-bottom: $interiorMarginSm;
}
}
.c-condition-h {
&__drop-target {
border-radius: $controlCr;
height: 0;
min-height: 0;
transition: background-color, height;
transition-duration: 150ms;
}
&.is-drag-target {
.c-condition > * {
pointer-events: none; // Keeps the JS drop handler from being intercepted by internal elements
}
.c-condition-h__drop-target {
background-color: rgba($colorKey, 0.7);
}
}
}
.c-cs {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
/************************** CONDITION SET LAYOUT */
&__current-output {
flex: 0 0 auto;
}
&__test-data-and-conditions-w {
display: flex;
flex-direction: column;
flex: 1 1 auto;
height: 100%;
overflow: hidden;
}
&__test-data,
&__conditions {
flex: 0 0 auto;
overflow: hidden;
}
&__test-data {
flex: 0 0 auto;
max-height: 50%;
&.is-expanded {
margin-bottom: $interiorMargin * 4;
}
}
&__conditions {
flex: 1 1 auto;
> * + * {
margin-top: $interiorMarginSm;
}
}
&__content {
display: flex;
flex-direction: column;
flex: 0 1 auto;
overflow: hidden;
> * {
flex: 0 0 auto;
overflow: hidden;
+ * {
margin-top: $interiorMarginSm;
}
}
.c-button {
align-self: start;
}
}
.is-editing & {
// Add some space to kick away from blue editing border indication
padding: $interiorMargin;
}
section {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__conditions-h {
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.hint {
padding: $interiorMarginSm;
}
/************************** SPECIFIC ITEMS */
&__current-output-value {
flex-direction: row;
align-items: baseline;
padding: 0 $interiorMargin $interiorMarginLg $interiorMargin;
> * {
padding: $interiorMargin 0; // Must do this to align label and value
}
&__label {
color: $colorInspectorSectionHeaderFg;
opacity: 0.9;
text-transform: uppercase;
}
&__value {
$p: $interiorMargin * 3;
font-size: 1.25em;
margin-left: $interiorMargin;
padding-left: $p;
padding-right: $p;
background: rgba(black, 0.2);
border-radius: 5px;
}
}
}
/***************************** CONDITIONS AND TEST DATUM ELEMENTS */
.c-condition,
.c-test-datum {
@include discreteItem();
display: flex;
padding: $interiorMargin;
line-height: 170%; // Aligns text with controls like selects
}
.c-cdef,
.c-cs-test {
&__controls {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
white-space: nowrap;
}
}
.c-condition {
flex-direction: column;
min-width: 400px;
> * + * {
margin-top: $interiorMarginSm;
}
&--browse {
.c-condition__summary {
border-top: 1px solid $colorInteriorBorder;
padding-top: $interiorMargin;
}
}
/***************************** HEADER */
&__header {
$h: 22px;
display: flex;
align-items: start;
align-content: stretch;
overflow: hidden;
min-height: $h;
line-height: $h;
> * {
flex: 0 0 auto;
+ * {
margin-left: $interiorMarginSm;
}
}
}
&__drag-grippy {
transform: translateY(50%);
}
&__name {
font-weight: bold;
align-self: baseline; // Fixes bold line-height offset problem
}
&__output,
&__summary {
flex: 1 1 auto;
}
}
/***************************** CONDITION DEFINITION, EDITING */
.c-cdef {
display: grid;
grid-row-gap: $interiorMarginSm;
grid-column-gap: $interiorMargin;
grid-auto-columns: min-content 1fr max-content;
align-items: start;
min-width: 150px;
margin-left: 29px;
overflow: hidden;
&__criteria,
&__match-and-criteria {
display: contents;
}
&__label {
grid-column: 1;
text-align: right;
white-space: nowrap;
}
&__separator {
grid-column: 1 / span 3;
}
&__controls {
align-items: flex-start;
grid-column: 2;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
grid-column: 3;
}
}
.c-c__drag-ghost {
width: 100%;
min-height: $interiorMarginSm;
&.dragging {
min-height: 5em;
background-color: lightblue;
border-radius: 2px;
}
}
/***************************** TEST DATA */
.c-cs__test-data {
&__controls {
flex: 0 0 auto;
}
}
.c-cs-tests {
flex: 0 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.c-cs-test {
> * + * {
margin-left: $interiorMargin;
}
}

View File

@ -105,6 +105,7 @@ import ConditionDescription from "@/plugins/condition/components/ConditionDescri
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import Vue from 'vue';
import PreviewAction from "@/ui/preview/PreviewAction.js";
import {getApplicableStylesForItem} from "@/plugins/condition/utils/styleUtils";
export default {
name: 'ConditionalStylesView',
@ -115,24 +116,8 @@ export default {
},
inject: [
'openmct',
'domainObject'
'selection'
],
props: {
itemId: {
type: String,
default: ''
},
initialStyles: {
type: Object,
default() {
return undefined;
}
},
canHide: {
type: Boolean,
default: false
}
},
data() {
return {
conditionalStyles: [],
@ -145,9 +130,11 @@ export default {
}
},
destroyed() {
this.openmct.editor.off('isEditing', this.setEditState);
this.removeListeners();
},
mounted() {
this.itemId = '';
this.getDomainObjectFromSelection();
this.previewAction = new PreviewAction(this.openmct);
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
let objectStyles = this.itemId ? this.domainObject.configuration.objectStyles[this.itemId] : this.domainObject.configuration.objectStyles;
@ -162,6 +149,49 @@ export default {
this.openmct.editor.on('isEditing', this.setEditState);
},
methods: {
isItemType(type, item) {
return item && (item.type === type);
},
getDomainObjectFromSelection() {
let layoutItem;
let domainObject;
if (this.selection[0].length > 1) {
//If there are more than 1 items in the this.selection[0] list, the first one could either be a sub domain object OR a layout drawing control.
//The second item in the this.selection[0] list is the container object (usually a layout)
layoutItem = this.selection[0][0].context.layoutItem;
const item = this.selection[0][0].context.item;
this.canHide = true;
if (item &&
(!layoutItem || (this.isItemType('subobject-view', layoutItem)))) {
domainObject = item;
} else {
domainObject = this.selection[0][1].context.item;
if (layoutItem) {
this.itemId = layoutItem.id;
}
}
} else {
domainObject = this.selection[0][0].context.item;
}
this.domainObject = domainObject;
this.initialStyles = getApplicableStylesForItem(domainObject, layoutItem);
this.$nextTick(() => {
this.removeListeners();
if (this.domainObject) {
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
this.stopObservingItems = this.openmct.objects.observe(this.domainObject, 'configuration.items', this.updateDomainObjectItemStyles);
}
});
},
removeListeners() {
if (this.stopObserving) {
this.stopObserving();
}
if (this.stopObservingItems) {
this.stopObservingItems();
}
},
initialize(conditionSetDomainObject) {
//If there are new conditions in the conditionSet we need to set those styles to default
this.conditionSetDomainObject = conditionSetDomainObject;
@ -258,6 +288,36 @@ export default {
this.persist(domainObjectStyles);
},
updateDomainObjectItemStyles(newItems) {
//check that all items that have been styles still exist. Otherwise delete those styles
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
let itemsToRemove = [];
let keys = Object.keys(domainObjectStyles);
keys.forEach((key) => {
if ((key !== 'styles') &&
(key !== 'staticStyle') &&
(key !== 'conditionSetIdentifier')) {
if (!(newItems.find(item => item.id === key))) {
itemsToRemove.push(key);
}
}
});
if (itemsToRemove.length) {
this.removeItemStyles(itemsToRemove, domainObjectStyles);
}
},
removeItemStyles(itemIds, domainObjectStyles) {
itemIds.forEach(itemId => {
if (domainObjectStyles[itemId]) {
domainObjectStyles[itemId] = undefined;
delete domainObjectStyles[this.itemId];
}
});
if (_.isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
this.persist(domainObjectStyles);
},
initializeConditionalStyles() {
if (!this.conditions) {
this.conditions = {};
@ -283,9 +343,15 @@ export default {
},
initializeStaticStyle(objectStyles) {
let staticStyle = objectStyles && objectStyles.staticStyle;
this.staticStyle = staticStyle || {
if (staticStyle) {
this.staticStyle = {
style: Object.assign({}, this.initialStyles, staticStyle.style)
};
} else {
this.staticStyle = {
style: Object.assign({}, this.initialStyles)
};
}
},
findStyleByConditionId(id) {
return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id);

View File

@ -0,0 +1,269 @@
/*****************************************************************************
* 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.
*****************************************************************************/
<template>
<div class="c-inspector__styles c-inspect-styles">
<div class="c-inspect-styles__header">
Object Style
</div>
<div class="c-inspect-styles__content">
<div v-if="isStaticAndConditionalStyles"
class="c-inspect-styles__mixed-static-and-conditional u-alert u-alert--block u-alert--with-icon"
>
Your selection includes one or more items that use Conditional Styling. Applying a static style below will replace any Conditional Styling with the new choice.
</div>
<div v-if="staticStyle"
class="c-inspect-styles__style"
>
<style-editor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
:mixed-styles="mixedStyles"
@persist="updateStaticStyle"
/>
</div>
</div>
</div>
</template>
<script>
import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionalStyleForItem } from "@/plugins/condition/utils/styleUtils";
export default {
name: 'MultiSelectStylesView',
components: {
StyleEditor
},
inject: [
'openmct',
'selection'
],
data() {
return {
staticStyle: undefined,
isEditing: this.openmct.editor.isEditing(),
mixedStyles: [],
isStaticAndConditionalStyles: false
}
},
destroyed() {
this.removeListeners();
},
mounted() {
this.items = [];
this.previewAction = new PreviewAction(this.openmct);
this.getObjectsAndItemsFromSelection();
this.initializeStaticStyle();
this.openmct.editor.on('isEditing', this.setEditState);
},
methods: {
isItemType(type, item) {
return item && (item.type === type);
},
hasConditionalStyles(domainObject, id) {
return getConditionalStyleForItem(domainObject, id) !== undefined;
},
getObjectsAndItemsFromSelection() {
let domainObject;
let subObjects = [];
//multiple selection
let itemInitialStyles = [];
let itemStyle;
this.selection.forEach((selectionItem) => {
const item = selectionItem[0].context.item;
const layoutItem = selectionItem[0].context.layoutItem;
if (item && this.isItemType('subobject-view', layoutItem)) {
subObjects.push(item);
itemStyle = getApplicableStylesForItem(item);
if (!this.isStaticAndConditionalStyles) {
this.isStaticAndConditionalStyles = this.hasConditionalStyles(item);
}
} else {
domainObject = selectionItem[1].context.item;
itemStyle = getApplicableStylesForItem(domainObject, layoutItem || item);
this.items.push({
id: layoutItem.id,
applicableStyles: itemStyle
});
if (!this.isStaticAndConditionalStyles) {
this.isStaticAndConditionalStyles = this.hasConditionalStyles(domainObject, layoutItem.id);
}
}
itemInitialStyles.push(itemStyle);
});
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
this.initialStyles = styles;
this.mixedStyles = mixedStyles;
this.domainObject = domainObject;
this.removeListeners();
if (this.domainObject) {
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
this.stopObservingItems = this.openmct.objects.observe(this.domainObject, 'configuration.items', this.updateDomainObjectItemStyles);
}
subObjects.forEach(this.registerListener);
},
updateDomainObjectItemStyles(newItems) {
//check that all items that have been styles still exist. Otherwise delete those styles
let keys = Object.keys(this.domainObject.configuration.objectStyles || {});
keys.forEach((key) => {
if ((key !== 'styles') &&
(key !== 'staticStyle') &&
(key !== 'conditionSetIdentifier')) {
if (!(newItems.find(item => item.id === key))) {
this.removeItemStyles(key);
}
}
});
},
registerListener(domainObject) {
let id = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.domainObjectsById) {
this.domainObjectsById = {};
}
if (!this.domainObjectsById[id]) {
this.domainObjectsById[id] = domainObject;
this.observeObject(domainObject, id);
}
},
observeObject(domainObject, id) {
let unobserveObject = this.openmct.objects.observe(domainObject, '*', function (newObject) {
this.domainObjectsById[id] = JSON.parse(JSON.stringify(newObject));
}.bind(this));
this.unObserveObjects.push(unobserveObject);
},
removeListeners() {
if (this.stopObserving) {
this.stopObserving();
}
if (this.stopObservingItems) {
this.stopObservingItems();
}
if (this.unObserveObjects) {
this.unObserveObjects.forEach((unObserveObject) => {
unObserveObject();
});
}
this.unObserveObjects = [];
},
removeItemStyles(itemId) {
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (itemId && domainObjectStyles[itemId]) {
domainObjectStyles[itemId] = undefined;
delete domainObjectStyles[this.itemId];
if (_.isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
this.persist(this.domainObject, domainObjectStyles);
}
},
removeConditionalStyles(domainObjectStyles, itemId) {
if (itemId) {
domainObjectStyles[itemId].conditionSetIdentifier = undefined;
delete domainObjectStyles[itemId].conditionSetIdentifier;
domainObjectStyles[itemId].styles = undefined;
delete domainObjectStyles[itemId].styles;
} else {
domainObjectStyles.conditionSetIdentifier = undefined;
delete domainObjectStyles.conditionSetIdentifier;
domainObjectStyles.styles = undefined;
delete domainObjectStyles.styles;
}
},
setEditState(isEditing) {
this.isEditing = isEditing;
},
initializeStaticStyle() {
this.staticStyle = {
style: Object.assign({}, this.initialStyles)
};
},
updateStaticStyle(staticStyle, property) {
//update the static style for each of the layoutItems as well as each sub object item
this.staticStyle = staticStyle;
this.persist(this.domainObject, this.getDomainObjectStyle(this.domainObject, property, this.items));
if (this.domainObjectsById) {
const keys = Object.keys(this.domainObjectsById);
keys.forEach(key => {
let domainObject = this.domainObjectsById[key];
this.persist(domainObject, this.getDomainObjectStyle(domainObject, property));
});
}
this.isStaticAndConditionalStyles = false;
let foundIndex = this.mixedStyles.indexOf(property);
if (foundIndex > -1) {
this.mixedStyles.splice(foundIndex, 1);
}
},
getDomainObjectStyle(domainObject, property, items) {
let domainObjectStyles = (domainObject.configuration && domainObject.configuration.objectStyles) || {};
if (items) {
items.forEach(item => {
let itemStaticStyle = {};
if (domainObjectStyles[item.id] && domainObjectStyles[item.id].staticStyle) {
itemStaticStyle = domainObjectStyles[item.id].staticStyle.style;
}
Object.keys(item.applicableStyles).forEach(key => {
if (property === key) {
itemStaticStyle[key] = this.staticStyle.style[key];
}
});
if (this.isStaticAndConditionalStyles) {
this.removeConditionalStyles(domainObjectStyles, item.id);
}
if (_.isEmpty(itemStaticStyle)) {
itemStaticStyle = undefined;
domainObjectStyles[item.id] = undefined;
} else {
domainObjectStyles[item.id] = Object.assign({}, { staticStyle: { style: itemStaticStyle } });
}
});
} else {
if (!domainObjectStyles.staticStyle) {
domainObjectStyles.staticStyle = {
style: {}
}
}
if (this.isStaticAndConditionalStyles) {
this.removeConditionalStyles(domainObjectStyles);
}
domainObjectStyles.staticStyle.style[property] = this.staticStyle.style[property];
}
return domainObjectStyles;
},
persist(domainObject, style) {
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
}
}
}
</script>

View File

@ -22,38 +22,41 @@
<template>
<div class="c-style">
<span class="c-style-thumb"
:class="{ 'is-style-invisible': styleItem.style.isStyleInvisible }"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : styleItem.style ]"
<span :class="[
{ 'is-style-invisible': styleItem.style.isStyleInvisible },
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
class="c-style-thumb"
>
<span class="c-style-thumb__text"
:class="{ 'hide-nice': !styleItem.style.color }"
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
>
ABC
</span>
</span>
<span class="c-toolbar">
<toolbar-color-picker v-if="styleItem.style.border"
<toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
class="c-style__toolbar-button--border-color u-menu-to--center"
:options="borderColorOption"
@change="updateStyleValue"
/>
<toolbar-color-picker v-if="styleItem.style.backgroundColor"
<toolbar-color-picker v-if="hasProperty(styleItem.style.backgroundColor)"
class="c-style__toolbar-button--background-color u-menu-to--center"
:options="backgroundColorOption"
@change="updateStyleValue"
/>
<toolbar-color-picker v-if="styleItem.style.color"
<toolbar-color-picker v-if="hasProperty(styleItem.style.color)"
class="c-style__toolbar-button--color u-menu-to--center"
:options="colorOption"
@change="updateStyleValue"
/>
<toolbar-button v-if="styleItem.style.imageUrl !== undefined"
<toolbar-button v-if="hasProperty(styleItem.style.imageUrl)"
class="c-style__toolbar-button--image-url"
:options="imageUrlOption"
@change="updateStyleValue"
/>
<toolbar-toggle-button v-if="styleItem.style.isStyleInvisible !== undefined"
<toolbar-toggle-button v-if="hasProperty(styleItem.style.isStyleInvisible)"
class="c-style__toolbar-button--toggle-visible"
:options="isStyleInvisibleOption"
@change="updateStyleValue"
@ -68,6 +71,7 @@ import ToolbarColorPicker from "@/ui/toolbar/components/toolbar-color-picker.vue
import ToolbarButton from "@/ui/toolbar/components/toolbar-button.vue";
import ToolbarToggleButton from "@/ui/toolbar/components/toolbar-toggle-button.vue";
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
export default {
name: 'StyleEditor',
@ -83,37 +87,52 @@ export default {
isEditing: {
type: Boolean
},
mixedStyles: {
type: Array,
default() {
return [];
}
},
styleItem: {
type: Object,
required: true
}
},
computed: {
itemStyle() {
return getStylesWithoutNoneValue(this.styleItem.style);
},
borderColorOption() {
let value = this.styleItem.style.border.replace('1px solid ', '');
return {
icon: 'icon-line-horz',
title: STYLE_CONSTANTS.borderColorTitle,
value: this.styleItem.style.border.replace('1px solid ', ''),
value: this.normalizeValueForSwatch(value),
property: 'border',
isEditing: this.isEditing
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('border') > -1
}
},
backgroundColorOption() {
let value = this.styleItem.style.backgroundColor;
return {
icon: 'icon-paint-bucket',
title: STYLE_CONSTANTS.backgroundColorTitle,
value: this.styleItem.style.backgroundColor,
value: this.normalizeValueForSwatch(value),
property: 'backgroundColor',
isEditing: this.isEditing
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('backgroundColor') > -1
}
},
colorOption() {
let value = this.styleItem.style.color;
return {
icon: 'icon-font',
title: STYLE_CONSTANTS.textColorTitle,
value: this.styleItem.style.color,
value: this.normalizeValueForSwatch(value),
property: 'color',
isEditing: this.isEditing
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('color') > -1
}
},
imageUrlOption() {
@ -138,7 +157,8 @@ export default {
property: 'imageUrl',
formKeys: ['url'],
value: {url: this.styleItem.style.imageUrl},
isEditing: this.isEditing
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
}
},
isStyleInvisibleOption() {
@ -163,7 +183,23 @@ export default {
}
},
methods: {
hasProperty(property) {
return property !== undefined;
},
normalizeValueForSwatch(value) {
if (value && value.indexOf('__no_value') > -1) {
return value.replace('__no_value', 'transparent');
}
return value;
},
normalizeValueForStyle(value) {
if (value && value === 'transparent') {
return '__no_value';
}
return value;
},
updateStyleValue(value, item) {
value = this.normalizeValueForStyle(value);
if (item.property === 'border') {
value = '1px solid ' + value;
}
@ -172,7 +208,7 @@ export default {
} else {
this.styleItem.style[item.property] = value;
}
this.$emit('persist', this.styleItem);
this.$emit('persist', this.styleItem, item.property);
}
}
}

View File

@ -20,11 +20,10 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import {OPERATIONS} from '../utils/operations';
import {computeCondition} from "@/plugins/condition/utils/evaluator";
import TelemetryCriterion from './TelemetryCriterion';
import { evaluateResults } from "../utils/evaluator";
export default class TelemetryCriterion extends EventEmitter {
export default class AllTelemetryCriterion extends TelemetryCriterion {
/**
* Subscribes/Unsubscribes to telemetry and emits the result
@ -34,23 +33,20 @@ export default class TelemetryCriterion extends EventEmitter {
* @param openmct
*/
constructor(telemetryDomainObjectDefinition, openmct) {
super();
super(telemetryDomainObjectDefinition, openmct);
}
this.openmct = openmct;
this.objectAPI = this.openmct.objects;
this.telemetryAPI = this.openmct.telemetry;
this.timeAPI = this.openmct.time;
this.id = telemetryDomainObjectDefinition.id;
this.telemetry = telemetryDomainObjectDefinition.telemetry;
this.operation = telemetryDomainObjectDefinition.operation;
this.telemetryObjects = Object.assign({}, telemetryDomainObjectDefinition.telemetryObjects);
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
initialize() {
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
this.telemetryDataCache = {};
}
isValid() {
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
}
updateTelemetry(telemetryObjects) {
this.telemetryObjects = Object.assign({}, telemetryObjects);
this.telemetryObjects = { ...telemetryObjects };
}
formatData(data, telemetryObjects) {
@ -68,60 +64,32 @@ export default class TelemetryCriterion extends EventEmitter {
});
const datum = {
result: computeCondition(this.telemetryDataCache, this.telemetry === 'all')
result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry)
};
if (data) {
// TODO check back to see if we should format times here
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key]
});
}
return datum;
}
handleSubscription(data, telemetryObjects) {
if(this.isValid()) {
this.emitEvent('criterionResultUpdated', this.formatData(data, telemetryObjects));
} else {
this.emitEvent('criterionResultUpdated', this.formatData({}, telemetryObjects));
}
getResult(data, telemetryObjects) {
const validatedData = this.isValid() ? data : {};
if (validatedData) {
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
}
findOperation(operation) {
for (let i=0; i < OPERATIONS.length; i++) {
if (operation === OPERATIONS[i].name) {
return OPERATIONS[i].operation;
Object.values(telemetryObjects).forEach(telemetryObject => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (this.telemetryDataCache[id] === undefined) {
this.telemetryDataCache[id] = false;
}
}
return null;
}
computeResult(data) {
let result = false;
if (data) {
let comparator = this.findOperation(this.operation);
let params = [];
params.push(data[this.metadata]);
if (this.input instanceof Array && this.input.length) {
this.input.forEach(input => params.push(input));
}
if (typeof comparator === 'function') {
result = comparator(params);
}
}
return result;
}
emitEvent(eventName, data) {
this.emit(eventName, {
id: this.id,
data: data
});
}
isValid() {
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
requestLAD(options) {
@ -139,7 +107,7 @@ export default class TelemetryCriterion extends EventEmitter {
let keys = Object.keys(Object.assign({}, options.telemetryObjects));
const telemetryRequests = keys
.map(key => this.telemetryAPI.request(
.map(key => this.openmct.telemetry.request(
options.telemetryObjects[key],
options
));
@ -163,10 +131,7 @@ export default class TelemetryCriterion extends EventEmitter {
}
destroy() {
this.emitEvent('criterionRemoved');
delete this.telemetryObjects;
delete this.telemetryDataCache;
delete this.telemetryObjectIdAsString;
delete this.telemetryObject;
}
}

View File

@ -36,20 +36,27 @@ export default class TelemetryCriterion extends EventEmitter {
super();
this.openmct = openmct;
this.objectAPI = this.openmct.objects;
this.telemetryAPI = this.openmct.telemetry;
this.timeAPI = this.openmct.time;
this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
this.id = telemetryDomainObjectDefinition.id;
this.telemetry = telemetryDomainObjectDefinition.telemetry;
this.operation = telemetryDomainObjectDefinition.operation;
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.telemetryObject = telemetryDomainObjectDefinition.telemetryObject;
this.telemetryObjectIdAsString = this.objectAPI.makeKeyString(telemetryDomainObjectDefinition.telemetry);
this.on(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription);
this.result = undefined;
this.initialize();
this.emitEvent('criterionUpdated', this);
}
initialize() {
this.telemetryObject = this.telemetryDomainObjectDefinition.telemetryObject;
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
updateTelemetry(telemetryObjects) {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
}
@ -60,20 +67,44 @@ export default class TelemetryCriterion extends EventEmitter {
};
if (data) {
// TODO check back to see if we should format times here
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key]
});
}
return datum;
}
handleSubscription(data) {
if(this.isValid()) {
this.emitEvent('criterionResultUpdated', this.formatData(data));
} else {
this.emitEvent('criterionResultUpdated', this.formatData({}));
getResult(data) {
const validatedData = this.isValid() ? data : {};
this.result = this.computeResult(validatedData);
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
if (!this.isValid()) {
return {
id: this.id,
data: this.formatData({})
};
}
return this.openmct.telemetry.request(
this.telemetryObject,
options
).then(results => {
const latestDatum = results.length ? results[results.length - 1] : {};
return {
id: this.id,
data: this.formatData(latestDatum)
};
});
}
findOperation(operation) {
@ -95,7 +126,7 @@ export default class TelemetryCriterion extends EventEmitter {
this.input.forEach(input => params.push(input));
}
if (typeof comparator === 'function') {
result = comparator(params);
result = !!comparator(params);
}
}
return result;
@ -108,42 +139,9 @@ export default class TelemetryCriterion extends EventEmitter {
});
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
if (!this.isValid()) {
return {
id: this.id,
data: this.formatData({})
};
}
return this.telemetryAPI.request(
this.telemetryObject,
options
).then(results => {
const latestDatum = results.length ? results[results.length - 1] : {};
return {
id: this.id,
data: this.formatData(latestDatum)
};
});
}
destroy() {
this.off(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription);
this.emitEvent('criterionRemoved');
delete this.telemetryObjectIdAsString;
delete this.telemetryObject;
delete this.telemetryObjectIdAsString;
}
}

View File

@ -54,7 +54,7 @@ describe("The telemetry criterion", function () {
key: "testSource",
source: "value",
name: "Test",
format: "enum"
format: "string"
}]
}
};
@ -80,8 +80,9 @@ describe("The telemetry criterion", function () {
testCriterionDefinition = {
id: 'test-criterion-id',
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
operation: 'lessThan',
metadata: 'sin',
operation: 'textContains',
metadata: 'value',
input: ['Hell'],
telemetryObject: testTelemetryObject
};
@ -100,12 +101,21 @@ describe("The telemetry criterion", function () {
expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key);
});
it("updates and emits event on new data from telemetry providers", function () {
spyOn(telemetryCriterion, 'emitEvent').and.callThrough();
telemetryCriterion.handleSubscription({
it("returns a result on new data from relevant telemetry providers", function () {
telemetryCriterion.getResult({
value: 'Hello',
utc: 'Hi'
utc: 'Hi',
id: testTelemetryObject.identifier.key
});
expect(telemetryCriterion.emitEvent).toHaveBeenCalled();
expect(telemetryCriterion.result).toBeTrue();
});
// it("does not return a result on new data from irrelavant telemetry providers", function () {
// telemetryCriterion.getResult({
// value: 'Hello',
// utc: 'Hi',
// id: '1234'
// });
// expect(telemetryCriterion.result).toBeFalse();
// });
});

View File

@ -19,36 +19,50 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { TRIGGER } from "./constants";
export const computeCondition = (resultMap, allMustBeTrue) => {
let result = false;
for (let key in resultMap) {
if (resultMap.hasOwnProperty(key)) {
result = resultMap[key];
if (allMustBeTrue && !result) {
//If we want all conditions to be true, then even one negative result should break.
break;
} else if (!allMustBeTrue && result) {
//If we want at least one condition to be true, then even one positive result should break.
break;
export const evaluateResults = (results, trigger) => {
if (trigger && trigger === TRIGGER.XOR) {
return matchExact(results, 1);
} else if (trigger && trigger === TRIGGER.NOT) {
return matchExact(results, 0);
} else if (trigger && trigger === TRIGGER.ALL) {
return matchAll(results);
} else {
return matchAny(results);
}
}
}
return result;
};
//Returns true only if limit number of results are satisfied
export const computeConditionByLimit = (resultMap, limit) => {
let trueCount = 0;
for (let key in resultMap) {
if (resultMap.hasOwnProperty(key)) {
if (resultMap[key]) {
trueCount++;
}
if (trueCount > limit) {
break;
function matchAll(results) {
for (const result of results) {
if (!result) {
return false;
}
}
return true;
}
function matchAny(results) {
for (const result of results) {
if (result) {
return true;
}
}
return false;
}
function matchExact(results, target) {
let matches = 0;
for (const result of results) {
if (result) {
matches++;
}
if (matches > target) {
return false;
}
}
return matches === target;
}
return trueCount === limit;
};

View File

@ -20,47 +20,185 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { computeConditionByLimit } from "./evaluator";
import { evaluateResults } from './evaluator';
import { TRIGGER } from './constants';
describe('evaluate results based on trigger', function () {
describe('evaluate results', () => {
// const allTrue = [true, true, true, true, true];
// const oneTrue = [false, false, false, false, true];
// const multipleTrue = [false, true, false, true, false];
// const noneTrue = [false, false, false, false, false];
// const allTrueWithUndefined = [true, true, true, undefined, true];
// const oneTrueWithUndefined = [undefined, undefined, undefined, undefined, true];
// const multipleTrueWithUndefined = [true, undefined, true, undefined, true];
// const allUndefined = [undefined, undefined, undefined, undefined, undefined];
// const singleTrue = [true];
// const singleFalse = [false];
// const singleUndefined = [undefined];
// const empty = [];
it('should evaluate to true if trigger is NOT', () => {
const results = {
result: false,
result1: false,
result2: false
};
const result = computeConditionByLimit(results, 0);
expect(result).toBeTrue();
});
const tests = [
{
name: 'allTrue',
values: [true, true, true, true, true],
any: true,
all: true,
not: false,
xor: false
}, {
name: 'oneTrue',
values: [false, false, false, false, true],
any: true,
all: false,
not: false,
xor: true
}, {
name: 'multipleTrue',
values: [false, true, false, true, false],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'noneTrue',
values: [false, false, false, false, false],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'allTrueWithUndefined',
values: [true, true, true, undefined, true],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'oneTrueWithUndefined',
values: [undefined, undefined, undefined, undefined, true],
any: true,
all: false,
not: false,
xor: true
}, {
name: 'multipleTrueWithUndefined',
values: [true, undefined, true, undefined, true],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'allUndefined',
values: [undefined, undefined, undefined, undefined, undefined],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'singleTrue',
values: [true],
any: true,
all: true,
not: false,
xor: true
}, {
name: 'singleFalse',
values: [false],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'singleUndefined',
values: [undefined],
any: false,
all: false,
not: true,
xor: false
}
// , {
// name: 'empty',
// values: [],
// any: false,
// all: false,
// not: true,
// xor: false
// }
];
it('should evaluate to false if trigger is NOT', () => {
const results = {
result: true,
result1: false,
result2: false
};
const result = computeConditionByLimit(results, 0);
expect(result).toBeFalse();
});
it('should evaluate to true if trigger is XOR', () => {
const results = {
result: false,
result1: true,
result2: false
};
const result = computeConditionByLimit(results, 1);
expect(result).toBeTrue();
});
it('should evaluate to false if trigger is XOR', () => {
const results = {
result: false,
result1: true,
result2: true
};
const result = computeConditionByLimit(results, 1);
expect(result).toBeFalse();
describe(`based on trigger ${TRIGGER.ANY}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.ANY);
expect(result).toEqual(test[TRIGGER.ANY])
});
});
});
describe(`based on trigger ${TRIGGER.ALL}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.ALL);
expect(result).toEqual(test[TRIGGER.ALL])
});
});
});
describe(`based on trigger ${TRIGGER.NOT}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.NOT);
expect(result).toEqual(test[TRIGGER.NOT])
});
});
});
describe(`based on trigger ${TRIGGER.XOR}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.XOR);
expect(result).toEqual(test[TRIGGER.XOR])
});
});
});
// it('should evaluate to true if trigger is NOT', () => {
// const results = {
// result: false,
// result1: false,
// result2: false
// };
// const result = computeConditionByLimit(results, 0);
// expect(result).toBeTrue();
// });
// it('should evaluate to false if trigger is NOT', () => {
// const results = {
// result: true,
// result1: false,
// result2: false
// };
// const result = computeConditionByLimit(results, 0);
// expect(result).toBeFalse();
// });
// it('should evaluate to true if trigger is XOR', () => {
// const results = {
// result: false,
// result1: true,
// result2: false
// };
// const result = computeConditionByLimit(results, 1);
// expect(result).toBeTrue();
// });
// it('should evaluate to false if trigger is XOR', () => {
// const results = {
// result: false,
// result1: true,
// result2: true
// };
// const result = computeConditionByLimit(results, 1);
// expect(result).toBeFalse();
// });
});

View File

@ -22,6 +22,18 @@
import _ from 'lodash';
const convertToNumbers = (input) => {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
return numberInputs;
};
const convertToStrings = (input) => {
let stringInputs = [];
input.forEach(inputValue => stringInputs.push(inputValue !== undefined ? inputValue.toString() : ''));
return stringInputs;
};
export const OPERATIONS = [
{
name: 'equalTo',
@ -98,8 +110,7 @@ export const OPERATIONS = [
{
name: 'between',
operation: function (input) {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
let numberInputs = convertToNumbers(input);
let larger = Math.max(...numberInputs.slice(1,3));
let smaller = Math.min(...numberInputs.slice(1,3));
return (numberInputs[0] > smaller) && (numberInputs[0] < larger);
@ -114,8 +125,7 @@ export const OPERATIONS = [
{
name: 'notBetween',
operation: function (input) {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
let numberInputs = convertToNumbers(input);
let larger = Math.max(...numberInputs.slice(1,3));
let smaller = Math.min(...numberInputs.slice(1,3));
return (numberInputs[0] < smaller) || (numberInputs[0] > larger);
@ -214,7 +224,8 @@ export const OPERATIONS = [
{
name: 'enumValueIs',
operation: function (input) {
return input[0] === input[1];
let stringInputs = convertToStrings(input);
return stringInputs[0] === stringInputs[1];
},
text: 'is',
appliesTo: ['enum'],
@ -226,7 +237,8 @@ export const OPERATIONS = [
{
name: 'enumValueIsNot',
operation: function (input) {
return input[0] !== input[1];
let stringInputs = convertToStrings(input);
return stringInputs[0] !== stringInputs[1];
},
text: 'is not',
appliesTo: ['enum'],
@ -238,9 +250,10 @@ export const OPERATIONS = [
{
name: 'valueIs',
operation: function (input) {
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
if (input[1]) {
const values = input[1].split(',');
return values.find((value) => input[0].toString() === _.trim(value.toString()));
return values.find((value) => lhsValue === _.trim(value.toString()));
}
return false;
},
@ -254,9 +267,10 @@ export const OPERATIONS = [
{
name: 'valueIsNot',
operation: function (input) {
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
if (input[1]) {
const values = input[1].split(',');
const found = values.find((value) => input[0].toString() === _.trim(value.toString()));
const found = values.find((value) => lhsValue === _.trim(value.toString()));
return !found;
}
return false;

View File

@ -25,8 +25,10 @@ let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueI
let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIsNot');
let isBetween = OPERATIONS.find((operation) => operation.name === 'between');
let isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween');
let enumIsOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIs');
let enumIsNotOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIsNot');
describe('Is one of and is not one of operations', function () {
describe('operations', function () {
it('should evaluate isOneOf to true for number inputs', () => {
const inputs = [45, "5,6,45,8"];
@ -87,4 +89,54 @@ describe('Is one of and is not one of operations', function () {
const inputs = ["45", "30", "50"];
expect(!!isNotBetween.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIs to true for number inputs', () => {
const inputs = [1, "1"];
expect(!!enumIsOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIs to true for string inputs', () => {
const inputs = ["45", "45"];
expect(!!enumIsOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIsNot to true for number inputs', () => {
const inputs = [45, "46"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIsNot to true for string inputs', () => {
const inputs = ["45", "46"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIs to false for number inputs', () => {
const inputs = [1, "2"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIs to false for string inputs', () => {
const inputs = ["45", "46"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to false for number inputs', () => {
const inputs = [45, "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to false for string inputs', () => {
const inputs = ["45", "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIs to false for undefined input', () => {
const inputs = [undefined, "45"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to true for undefined input', () => {
const inputs = [undefined, "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
});

View File

@ -19,27 +19,154 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const NONE_VALUE = '__no_value';
export const getStyleProp = (key, defaultValue) => {
let styleProp = undefined;
switch(key) {
case 'fill': styleProp = {
backgroundColor: defaultValue || 'transparent'
};
break;
case 'stroke': styleProp = {
border: '1px solid ' + (defaultValue || 'transparent')
};
break;
case 'color': styleProp = {
color: defaultValue || 'transparent'
};
break;
case 'url': styleProp = {
imageUrl: defaultValue || 'transparent'
};
break;
const styleProps = {
backgroundColor: {
svgProperty: 'fill',
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view' ||
type === 'telemetry-view' ||
type === 'box-view' ||
type === 'subobject-view');
}
},
border: {
svgProperty: 'stroke',
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view' ||
type === 'telemetry-view' ||
type === 'box-view' ||
type === 'image-view' ||
type === 'line-view'||
type === 'subobject-view');
}
},
color: {
svgProperty: 'color',
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view' ||
type === 'telemetry-view'||
type === 'subobject-view');
}
},
imageUrl: {
svgProperty: 'url',
noneValue: '',
applicableForType: type => {
return !type ? false : type === 'image-view';
}
}
return styleProp;
};
const aggregateStyleValues = (accumulator, currentStyle) => {
const styleKeys = Object.keys(currentStyle);
const properties = Object.keys(styleProps);
properties.forEach((property) => {
if (!accumulator[property]) {
accumulator[property] = [];
}
const found = styleKeys.find(key => key === property);
if (found) {
accumulator[property].push(currentStyle[found]);
}
});
return accumulator;
};
// Returns a union of styles used by multiple items.
// Styles that are common to all items but don't have the same value are added to the mixedStyles list
export const getConsolidatedStyleValues = (multipleItemStyles) => {
let aggregatedStyleValues = multipleItemStyles.reduce(aggregateStyleValues, {});
let styleValues = {};
let mixedStyles = [];
const properties = Object.keys(styleProps);
properties.forEach((property) => {
const values = aggregatedStyleValues[property];
if (values.length) {
if (values.every(value => value === values[0])) {
styleValues[property] = values[0];
} else {
styleValues[property] = '';
mixedStyles.push(property);
}
}
});
return {
styles: styleValues,
mixedStyles
};
};
const getStaticStyleForItem = (domainObject, id) => {
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
if (domainObjectStyles) {
if (id) {
if(domainObjectStyles[id] && domainObjectStyles[id].staticStyle) {
return domainObjectStyles[id].staticStyle.style;
}
} else if (domainObjectStyles.staticStyle) {
return domainObjectStyles.staticStyle.style;
}
}
};
export const getConditionalStyleForItem = (domainObject, id) => {
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
if (domainObjectStyles) {
if (id) {
if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) {
return domainObjectStyles[id].styles;
}
} else if (domainObjectStyles.staticStyle) {
return domainObjectStyles.styles;
}
}
};
//Returns either existing static styles or uses SVG defaults if available
export const getApplicableStylesForItem = (domainObject, item) => {
const type = item && item.type;
const id = item && item.id;
let style = {};
let staticStyle = getStaticStyleForItem(domainObject, id);
const properties = Object.keys(styleProps);
properties.forEach(property => {
const styleProp = styleProps[property];
if (styleProp.applicableForType(type)) {
let defaultValue;
if (staticStyle) {
defaultValue = staticStyle[property];
} else if (item) {
defaultValue = item[styleProp.svgProperty];
}
style[property] = defaultValue === undefined ? styleProp.noneValue : defaultValue;
}
});
return style;
};
export const getStylesWithoutNoneValue = (style) => {
if (_.isEmpty(style) || !style) {
return;
}
let styleObj = {};
const keys = Object.keys(style);
keys.forEach(key => {
if ((typeof style[key] === 'string')) {
if (style[key].indexOf('__no_value') > -1) {
style[key] = '';
} else {
styleObj[key] = style[key];
}
}
});
return styleObj;
};

View File

@ -0,0 +1,52 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export const getLatestTimestamp = (
currentTimestamp,
compareTimestamp,
timeSystems,
currentTimeSystem
) => {
let latest = { ...currentTimestamp };
const compare = { ...compareTimestamp };
const key = currentTimeSystem.key;
if (!latest || !latest[key]) {
latest = updateLatestTimeStamp(compare, timeSystems)
}
if (compare[key] > latest[key]) {
latest = updateLatestTimeStamp(compare, timeSystems)
}
return latest;
}
function updateLatestTimeStamp(timestamp, timeSystems) {
let latest = {};
timeSystems.forEach(timeSystem => {
latest[timeSystem.key] = timestamp[timeSystem.key];
});
return latest;
}

View File

@ -21,13 +21,14 @@
*****************************************************************************/
<template>
<a class="c-condition-widget"
:href="internalDomainObject.url"
<component :is="urlDefined ? 'a' : 'span'"
class="c-condition-widget"
:href="urlDefined ? internalDomainObject.url : null"
>
<div class="c-condition-widget__label">
{{ internalDomainObject.label }}
</div>
</a>
</component>
</template>
<script>
@ -38,6 +39,11 @@ export default {
internalDomainObject: this.domainObject
}
},
computed: {
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
},

View File

@ -28,10 +28,12 @@
border: 1px solid transparent;
display: inline-block;
padding: $interiorMarginLg $interiorMarginLg * 2;
cursor: inherit !important;
&[href] {
cursor: pointer !important;
}
a.c-condition-widget {
// Widget is conditionally made into a <a> when URL property has been defined
cursor: pointer !important;
pointer-events: inherit;
}
// Make Condition Widget expand when in a hidden frame Layout context

View File

@ -43,7 +43,7 @@ export default {
makeDefinition() {
return {
fill: '#717171',
stroke: 'transparent',
stroke: '',
x: 1,
y: 1,
width: 10,
@ -74,13 +74,14 @@ export default {
},
computed: {
style() {
return Object.assign({
if (this.itemStyle) {
return this.itemStyle;
} else {
return {
backgroundColor: this.item.fill,
border: '1px solid ' + this.item.stroke
}, this.itemStyle);
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
border: this.item.stroke ? '1px solid ' + this.item.stroke : ''
};
}
}
},
watch: {

View File

@ -74,13 +74,18 @@ export default {
},
computed: {
style() {
let backgroundImage = 'url(' + this.item.url + ')';
let border = '1px solid ' + this.item.stroke;
if (this.itemStyle) {
if (this.itemStyle.imageUrl !== undefined) {
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
}
border = this.itemStyle.border;
}
return {
backgroundImage: this.itemStyle ? ('url(' + this.itemStyle.imageUrl + ')') : 'url(' + this.item.url + ')',
border: (this.itemStyle && this.itemStyle.border) ? this.itemStyle.border : ('1px solid ' + this.item.stroke)
backgroundImage,
border
};
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
}
},
watch: {

View File

@ -127,8 +127,11 @@ export default {
return {x, y, x2, y2};
},
stroke() {
if (this.itemStyle && this.itemStyle.border) {
if (this.itemStyle) {
if (this.itemStyle.border) {
return this.itemStyle.border.replace('1px solid ', '');
}
return '';
} else {
return this.item.stroke;
}
@ -146,9 +149,6 @@ export default {
height: `${height}px`
};
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
},
startHandleClass() {
return START_HANDLE_QUADRANTS[this.vectorQuadrant];
},

View File

@ -30,14 +30,13 @@
<div
v-if="domainObject"
class="c-telemetry-view"
:class="styleClass"
:style="styleObject"
@contextmenu.prevent="showContextMenu"
>
<div
v-if="showLabel"
class="c-telemetry-view__label"
:class="[styleClass]"
:style="objectStyle"
>
<div class="c-telemetry-view__label-text">
{{ domainObject.name }}
@ -48,8 +47,7 @@
v-if="showValue"
:title="fieldName"
class="c-telemetry-view__value"
:class="[telemetryClass, !telemetryClass && styleClass]"
:style="!telemetryClass && objectStyle"
:class="[telemetryClass]"
>
<div class="c-telemetry-view__value-text">
{{ telemetryValue }}
@ -62,7 +60,7 @@
<script>
import LayoutFrame from './LayoutFrame.vue'
import printj from 'printj'
import StyleRuleManager from "../../condition/StyleRuleManager";
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5],
DEFAULT_POSITION = [1, 1],
@ -81,8 +79,8 @@ export default {
height: DEFAULT_TELEMETRY_DIMENSIONS[1],
displayMode: 'all',
value: metadata.getDefaultDisplayValue(),
stroke: "transparent",
fill: "transparent",
stroke: "",
fill: "",
color: "",
size: "13px"
};
@ -91,6 +89,7 @@ export default {
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
props: {
item: {
type: Object,
@ -113,8 +112,7 @@ export default {
datum: undefined,
formats: undefined,
domainObject: undefined,
currentObjectPath: undefined,
objectStyle: ''
currentObjectPath: undefined
}
},
computed: {
@ -127,15 +125,10 @@ export default {
return displayMode === 'all' || displayMode === 'value';
},
styleObject() {
return {
backgroundColor: this.item.fill,
borderColor: this.item.stroke,
color: this.item.color,
return Object.assign({}, {
fontSize: this.item.size
}
},
styleClass() {
return this.objectStyle && this.objectStyle.isStyleInvisible;
}, this.itemStyle);
},
fieldName() {
return this.valueMetadata && this.valueMetadata.name;
@ -190,15 +183,6 @@ export default {
this.removeSelectable();
}
if (this.unlistenStyles) {
this.unlistenStyles();
}
if (this.styleRuleManager) {
this.styleRuleManager.destroy();
delete this.styleRuleManager;
}
this.openmct.time.off("bounds", this.refreshData);
},
methods: {
@ -241,7 +225,6 @@ export default {
},
setObject(domainObject) {
this.domainObject = domainObject;
this.initObjectStyles();
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
@ -266,30 +249,6 @@ export default {
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
},
initObjectStyles() {
if (this.domainObject.configuration) {
this.styleRuleManager = new StyleRuleManager(this.domainObject.configuration.objectStyles, this.openmct, this.updateStyle.bind(this));
if (this.unlistenStyles) {
this.unlistenStyles();
}
this.unlistenStyles = this.openmct.objects.observe(this.domainObject, 'configuration.objectStyles', (newObjectStyle) => {
//Updating object styles in the inspector view will trigger this so that the changes are reflected immediately
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
});
}
},
updateStyle(styleObj) {
let keys = Object.keys(styleObj);
keys.forEach(key => {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('transparent') > -1)) {
if (styleObj[key]) {
styleObj[key] = '';
}
}
});
this.objectStyle = styleObj;
}
}
}

View File

@ -32,7 +32,7 @@
:class="[styleClass]"
:style="style"
>
{{ item.text }}
<div class="c-text-view__text">{{ item.text }}</div>
</div>
</layout-frame>
</template>
@ -44,8 +44,8 @@ import conditionalStylesMixin from "../mixins/objectStyles-mixin";
export default {
makeDefinition(openmct, gridSize, element) {
return {
fill: 'transparent',
stroke: 'transparent',
fill: '',
stroke: '',
size: '13px',
color: '',
x: 1,
@ -80,14 +80,8 @@ export default {
computed: {
style() {
return Object.assign({
backgroundColor: this.item.fill,
border: '1px solid ' + this.item.stroke,
color: this.item.color,
fontSize: this.item.size
}, this.itemStyle);
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
}
},
watch: {

View File

@ -16,6 +16,8 @@
.is-editing {
/******************* STYLES FOR C-FRAME WHILE EDITING */
.c-frame {
border: 1px solid rgba($editFrameColorHov, 0.3);
&:not([s-selected]) {
&:hover {
border: $editFrameBorderHov;

View File

@ -1,6 +1,8 @@
.c-text-view {
display: flex;
align-items: stretch;
align-items: center; // Vertically center text
overflow: hidden;
padding: $interiorMargin;
.c-frame & {
@include abs();

View File

@ -21,18 +21,20 @@
*****************************************************************************/
import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
export default {
inject: ['openmct'],
data() {
return {
itemStyle: this.itemStyle
itemStyle: undefined,
styleClass: ''
}
},
mounted() {
this.domainObject = this.$parent.domainObject;
this.parentDomainObject = this.$parent.domainObject;
this.itemId = this.item.id;
this.objectStyle = this.getObjectStyleForItem(this.domainObject.configuration.objectStyles);
this.objectStyle = this.getObjectStyleForItem(this.parentDomainObject.configuration.objectStyles);
this.initObjectStyles();
},
destroyed() {
@ -59,7 +61,7 @@ export default {
this.stopListeningObjectStyles();
}
this.stopListeningObjectStyles = this.openmct.objects.observe(this.domainObject, 'configuration.objectStyles', (newObjectStyle) => {
this.stopListeningObjectStyles = this.openmct.objects.observe(this.parentDomainObject, 'configuration.objectStyles', (newObjectStyle) => {
//Updating object styles in the inspector view will trigger this so that the changes are reflected immediately
let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle);
if (this.objectStyle !== newItemObjectStyle) {
@ -69,13 +71,8 @@ export default {
});
},
updateStyle(style) {
this.itemStyle = style;
let keys = Object.keys(this.itemStyle);
keys.forEach((key) => {
if ((typeof this.itemStyle[key] === 'string') && (this.itemStyle[key].indexOf('transparent') > -1)) {
delete this.itemStyle[key];
}
});
this.itemStyle = getStylesWithoutNoneValue(style);
this.styleClass = this.itemStyle && this.itemStyle.isStyleInvisible;
}
}
};

View File

@ -17,7 +17,7 @@
<div v-if="embed.snapshot"
class="c-ne__embed__time"
>
{{ createdOn }}
{{ formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss') }}
</div>
</div>
</div>
@ -27,7 +27,7 @@
import Moment from 'moment';
import PopupMenu from './popup-menu.vue';
import PreviewAction from '../../../ui/preview/PreviewAction';
import PainterroInstance from '../utils/painterroInstance';
import Painterro from 'painterro';
import RemoveDialog from '../utils/removeDialog';
import SnapshotTemplate from './snapshot-template.html';
import Vue from 'vue';
@ -56,10 +56,7 @@ export default {
popupMenuItems: []
}
},
computed: {
createdOn() {
return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss');
}
watch: {
},
mounted() {
this.addPopupMenuItems();
@ -80,42 +77,89 @@ export default {
this.popupMenuItems = [removeEmbed, preview];
},
annotateSnapshot() {
let painterroInstance = null;
const self = this;
let save = false;
let painterroInstance = {};
const annotateVue = new Vue({
template: '<div id="snap-annotation"></div>'
});
const annotateOverlay = this.openmct.overlays.overlay({
let annotateOverlay = self.openmct.overlays.overlay({
element: annotateVue.$mount().$el,
size: 'large',
dismissable: false,
buttons: [
{
label: 'Cancel',
emphasis: true,
callback: () => {
painterroInstance.dismiss();
callback: function () {
save = false;
painterroInstance.save();
annotateOverlay.dismiss();
}
},
{
label: 'Save',
callback: () => {
callback: function () {
save = true;
painterroInstance.save();
annotateOverlay.dismiss();
this.snapshotOverlay.dismiss();
this.openSnapshot();
}
}
],
onDestroy: () => {
onDestroy: function () {
annotateVue.$destroy(true);
}
});
painterroInstance = new PainterroInstance();
painterroInstance.callback = this.updateSnapshot;
painterroInstance.show(this.embed.snapshot.src);
painterroInstance = Painterro({
id: 'snap-annotation',
activeColor: '#ff0000',
activeColorAlpha: 1.0,
activeFillColor: '#fff',
activeFillColorAlpha: 0.0,
backgroundFillColor: '#000',
backgroundFillColorAlpha: 0.0,
defaultFontSize: 16,
defaultLineWidth: 2,
defaultTool: 'ellipse',
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
translation: {
name: 'en',
strings: {
lineColor: 'Line',
fillColor: 'Fill',
lineWidth: 'Size',
textColor: 'Color',
fontSize: 'Size',
fontStyle: 'Style'
}
},
saveHandler: function (image, done) {
if (save) {
const url = image.asBlob();
const reader = new window.FileReader();
reader.readAsDataURL(url);
reader.onloadend = function () {
const snapshot = reader.result;
const snapshotObject = {
src: snapshot,
type: url.type,
size: url.size,
modified: Date.now()
};
self.embed.snapshot = snapshotObject;
self.updateEmbed(self.embed);
};
} else {
console.log('You cancelled the annotation!!!');
}
done(true);
}
}).show(this.embed.snapshot.src);
},
changeLocation() {
this.openmct.time.stopClock();
@ -125,6 +169,9 @@ export default {
});
const link = this.embed.historicLink;
if (!link) {
return;
}
window.location.href = link;
const message = 'Time bounds changed to fixed timespan mode';
@ -142,21 +189,22 @@ export default {
removeDialog.show();
},
openSnapshot() {
const self = this;
const snapshot = new Vue({
data: () => {
return {
createdOn: this.createdOn,
embed: this.embed
embed: self.embed
};
},
methods: {
annotateSnapshot: this.annotateSnapshot.bind(this)
formatTime: self.formatTime,
annotateSnapshot: self.annotateSnapshot
},
template: SnapshotTemplate
}).$mount();
});
this.snapshotOverlay = this.openmct.overlays.overlay({
element: snapshot.$el,
const snapshotOverlay = this.openmct.overlays.overlay({
element: snapshot.$mount().$el,
onDestroy: () => { snapshot.$destroy(true) },
size: 'large',
dismissable: true,
@ -165,7 +213,7 @@ export default {
label: 'Done',
emphasis: true,
callback: () => {
this.snapshotOverlay.dismiss();
snapshotOverlay.dismiss();
}
}
]
@ -185,10 +233,6 @@ export default {
},
updateEmbed(embed) {
this.$emit('updateEmbed', embed);
},
updateSnapshot(snapshotObject) {
this.embed.snapshot = snapshotObject;
this.updateEmbed(this.embed);
}
}
}

View File

@ -1,13 +1,13 @@
<template>
<div class="c-notebook__entry c-ne has-local-controls"
@dragover="changeCursor"
@drop.capture="cancelEditMode"
@drop.prevent="dropOnEntry"
@dragover="dragover"
@drop.capture="dropCapture"
@drop.prevent="dropOnEntry(entry.id, $event)"
>
<div class="c-ne__time-and-content">
<div class="c-ne__time">
<span>{{ createdOnDate }}</span>
<span>{{ createdOnTime }}</span>
<span>{{ formatTime(entry.createdOn, 'YYYY-MM-DD') }}</span>
<span>{{ formatTime(entry.createdOn, 'HH:mm:ss') }}</span>
</div>
<div class="c-ne__content">
<div :id="entry.id"
@ -15,8 +15,8 @@
:class="{'c-input-inline' : !readOnly }"
:contenteditable="!readOnly"
:style="!entry.text.length ? defaultEntryStyle : ''"
@blur="updateEntryValue($event, entry.id)"
@focus="updateCurrentEntryValue($event, entry.id)"
@blur="textBlur($event, entry.id)"
@focus="textFocus($event, entry.id)"
>{{ entry.text.length ? entry.text : defaultText }}</div>
<div class="c-snapshots c-ne__embeds">
<NotebookEmbed v-for="embed in entry.embeds"
@ -114,32 +114,29 @@ export default {
defaultText: 'add description'
}
},
computed : {
createdOnDate() {
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
watch: {
entry() {
},
createdOnTime() {
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
readOnly(readOnly) {
},
selectedSection(selectedSection) {
},
selectedPage(selectedSection) {
}
},
mounted() {
this.updateEntries = this.updateEntries.bind(this);
this.dropOnEntry = this.dropOnEntry.bind(this);
},
beforeDestory() {
},
methods: {
cancelEditMode(event) {
const isEditing = this.openmct.editor.isEditing();
if (isEditing) {
this.openmct.editor.cancel();
}
},
changeCursor() {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
},
deleteEntry() {
const self = this;
const entryPosById = self.entryPosById(self.entry.id);
if (!self.domainObject || !self.selectedSection || !self.selectedPage || !self.entry.id) {
return;
}
const entryPosById = this.entryPosById(this.entry.id);
if (entryPosById === -1) {
return;
}
@ -154,7 +151,7 @@ export default {
callback: () => {
const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage);
entries.splice(entryPosById, 1);
self.updateEntries(entries);
this.updateEntries(entries);
dialog.dismiss();
}
},
@ -167,10 +164,24 @@ export default {
]
});
},
dropOnEntry($event) {
dragover() {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
},
dropCapture(event) {
const isEditing = this.openmct.editor.isEditing();
if (isEditing) {
this.openmct.editor.cancel();
}
},
dropOnEntry(entryId, $event) {
event.stopImmediatePropagation();
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const snapshotId = $event.dataTransfer.getData('snapshot/id');
if (snapshotId.length) {
this.moveSnapshot(snapshotId);
@ -179,7 +190,7 @@ export default {
const data = $event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
const entryPos = this.entryPosById(this.entry.id);
const entryPos = this.entryPosById(entryId);
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
@ -242,44 +253,7 @@ export default {
selection.removeAllRanges();
selection.addRange(range);
},
updateCurrentEntryValue($event) {
if (this.readOnly) {
return;
}
const target = $event.target
this.currentEntryValue = target ? target.innerText : '';
if (!this.entry.text.length) {
this.selectTextInsideElement(target);
}
},
updateEmbed(newEmbed) {
this.entry.embeds.some(e => {
const found = (e.id === newEmbed.id);
if (found) {
e = newEmbed;
}
return found;
});
this.updateEntry(this.entry);
},
updateEntry(newEntry) {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries.some(entry => {
const found = (entry.id === newEntry.id);
if (found) {
entry = newEntry;
}
return found;
});
this.updateEntries(entries);
},
updateEntryValue($event, entryId) {
textBlur($event, entryId) {
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
@ -298,6 +272,42 @@ export default {
this.updateEntries(entries);
}
},
textFocus($event) {
if (this.readOnly || !this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const target = $event.target
this.currentEntryValue = target ? target.innerText : '';
if (!this.entry.text.length) {
this.selectTextInsideElement(target);
}
},
updateEmbed(newEmbed) {
let embed = this.entry.embeds.find(e => e.id === newEmbed.id);
if (!embed) {
return;
}
embed = newEmbed;
this.updateEntry(this.entry);
},
updateEntry(newEntry) {
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries.forEach(entry => {
if (entry.id === newEntry.id) {
entry = newEntry;
}
});
this.updateEntries(entries);
},
updateEntries(entries) {
this.$emit('updateEntries', entries);
}

View File

@ -1,8 +1,8 @@
<template>
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu icon-notebook"
title="Switch view type"
title="Take a Notebook Snapshot"
@click="setNotebookTypes"
@click.stop="toggleMenu"
>
@ -64,7 +64,9 @@ export default {
const defaultNotebook = getDefaultNotebook();
if (defaultNotebook) {
const domainObject = await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
const domainObject = await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier)
.then(d => d);
if (!domainObject.location) {
clearDefaultNotebook();
} else {

View File

@ -31,7 +31,7 @@
<NotebookEmbed ref="notebookEmbed"
:key="snapshot.id"
:embed="snapshot"
:remove-action-string="'Delete This Snapshot'"
:remove-action-string="'Delete Snapshot'"
@updateEmbed="updateSnapshot"
@removeEmbed="removeSnapshot"
/>
@ -69,7 +69,7 @@ export default {
data() {
return {
popupMenuItems: [],
removeActionString: 'Delete All Snapshots',
removeActionString: 'Delete all snapshots',
snapshots: []
}
},
@ -78,6 +78,8 @@ export default {
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.snapshots = this.snapshotContainer.getSnapshots();
},
beforeDestory() {
},
methods: {
addPopupMenuItems() {
const removeSnapshot = {
@ -117,7 +119,7 @@ export default {
},
startEmbedDrag(snapshot, event) {
event.dataTransfer.setData('text/plain', snapshot.id);
event.dataTransfer.setData('openmct/snapshot/id', snapshot.id);
event.dataTransfer.setData('snapshot/id', snapshot.id);
},
updateSnapshot(snapshot) {
this.snapshotContainer.updateSnapshot(snapshot);

View File

@ -9,10 +9,8 @@
</div>
<SearchResults v-if="search.length"
ref="searchResults"
:domain-object="internalDomainObject"
:results="searchedEntries"
:results="getSearchResults()"
@changeSectionPage="changeSelectedSection"
@updateEntries="updateEntries"
/>
<div v-if="!search.length"
@ -51,13 +49,19 @@
class="c-notebook__controls__time"
>
<option value="0"
selected="selected"
:selected="showTime === 0"
>
Show all
</option>
<option value="1">Last hour</option>
<option value="8">Last 8 hours</option>
<option value="24">Last 24 hours</option>
<option value="1"
:selected="showTime === 1"
>Last hour</option>
<option value="8"
:selected="showTime === 8"
>Last 8 hours</option>
<option value="24"
:selected="showTime === 24"
>Last 24 hours</option>
</select>
<select v-model="defaultSort"
class="c-notebook__controls__time"
@ -134,16 +138,21 @@ export default {
},
computed: {
filteredAndSortedEntries() {
const filterTime = Date.now();
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
return pageEntries.sort(this.sortEntries);
const hours = parseInt(this.showTime, 10);
const filteredPageEntriesByTime = hours
? pageEntries.filter(entry => (filterTime - entry.createdOn) <= hours * 60 * 60 * 1000)
: pageEntries;
return this.defaultSort === 'oldest'
? filteredPageEntriesByTime
: [...filteredPageEntriesByTime].reverse();
},
pages() {
return this.getPages() || [];
},
searchedEntries() {
return this.getSearchResults();
},
sections() {
return this.internalDomainObject.configuration.sections || [];
},
@ -163,6 +172,8 @@ export default {
return this.sections.find(section => section.isSelected);
}
},
watch: {
},
beforeMount() {
this.throttledSearchItem = throttle(this.searchItem, 500);
},
@ -247,7 +258,7 @@ export default {
event.preventDefault();
event.stopImmediatePropagation();
const snapshotId = event.dataTransfer.getData('openmct/snapshot/id');
const snapshotId = event.dataTransfer.getData('snapshot/id');
if (snapshotId.length) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.newEntry(snapshot);
@ -423,11 +434,6 @@ export default {
searchItem(input) {
this.search = input;
},
sortEntries(right, left) {
return this.defaultSort === 'newest'
? left.createdOn - right.createdOn
: right.createdOn - left.createdOn;
},
toggleNav() {
this.showNav = !this.showNav;
},

View File

@ -70,6 +70,12 @@ export default {
return {
}
},
watch: {
},
mounted() {
},
destroyed() {
},
methods: {
deletePage(id) {
const selectedSection = this.sections.find(s => s.isSelected);

View File

@ -55,6 +55,8 @@ export default {
this.addPopupMenuItems();
this.toggleContentEditable();
},
destroyed() {
},
methods: {
addPopupMenuItems() {
const removePage = {

View File

@ -1,13 +1,10 @@
<template>
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="l-browse-bar__context-actions c-disclosure-button"
class="c-popup-menu-button c-disclosure-button"
title="popup menu"
@click="showMenuItems"
>
<span class="c-button__label"></span>
</button>
</div>
</template>
<script>

View File

@ -4,14 +4,12 @@
<div class="c-notebook__entries">
<NotebookEntry v-for="(result, index) in results"
:key="index"
:domain-object="domainObject"
:result="result"
:entry="result.entry"
:read-only="true"
:selected-page="result.page"
:selected-section="result.section"
:selected-page="null"
:selected-section="null"
@changeSectionPage="changeSectionPage"
@updateEntries="updateEntries"
/>
</div>
</div>
@ -21,17 +19,11 @@
import NotebookEntry from './notebook-entry.vue';
export default {
inject: ['openmct', 'snapshotContainer'],
inject: ['openmct', 'domainObject'],
components: {
NotebookEntry
},
props:{
domainObject: {
type: Object,
default() {
return {};
}
},
results: {
type: Array,
default() {
@ -39,12 +31,19 @@ export default {
}
}
},
data() {
return {}
},
watch: {
results(newResults) {}
},
destroyed() {
},
mounted() {
},
methods: {
changeSectionPage(data) {
this.$emit('changeSectionPage', data);
},
updateEntries(entries) {
this.$emit('updateEntries', entries);
}
}
}

View File

@ -57,6 +57,12 @@ export default {
return {
}
},
watch: {
},
mounted() {
},
destroyed() {
},
methods: {
deleteSection(id) {
const section = this.sections.find(s => s.id === id);

View File

@ -58,6 +58,8 @@ export default {
this.addPopupMenuItems();
this.toggleContentEditable();
},
destroyed() {
},
methods: {
addPopupMenuItems() {
const removeSection = {

View File

@ -139,6 +139,8 @@ export default {
this.addSection();
}
},
destroyed() {
},
methods: {
addPage() {
const pageTitle = this.pageTitle;

View File

@ -13,7 +13,7 @@
<div class="l-browse-bar__end">
<div class="l-browse-bar__snapshot-datetime">
SNAPSHOT {{ createdOn }}
SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div>
<a class="l-browse-bar__annotate-button c-button icon-pencil" title="Annotate" @click="annotateSnapshot">
<span class="title-label">Annotate</span>

View File

@ -1,75 +0,0 @@
import Painterro from 'painterro';
const defaultConfig = {
id: 'snap-annotation',
activeColor: '#ff0000',
activeColorAlpha: 1.0,
activeFillColor: '#fff',
activeFillColorAlpha: 0.0,
backgroundFillColor: '#000',
backgroundFillColorAlpha: 0.0,
defaultFontSize: 16,
defaultLineWidth: 2,
defaultTool: 'ellipse',
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
translation: {
name: 'en',
strings: {
lineColor: 'Line',
fillColor: 'Fill',
lineWidth: 'Size',
textColor: 'Color',
fontSize: 'Size',
fontStyle: 'Style'
}
}
};
export default class PainterroInstance {
constructor() {
this.callback = null;
this.config = Object.assign({}, defaultConfig);
this.config.id = this.config.id;
this.config.saveHandler = this.saveHandler.bind(this);
this.isSave = false;
this.painterro = Painterro(this.config);
this.painterroInstance = null;
}
dismiss() {
this.isSave = false;
this.painterroInstance.save();
}
save() {
this.isSave = true;
this.painterroInstance.save();
}
saveHandler(image, done) {
if (this.isSave) {
const self = this;
const url = image.asBlob();
const reader = new window.FileReader();
reader.readAsDataURL(url);
reader.onloadend = () => {
const snapshot = reader.result;
const snapshotObject = {
src: snapshot,
type: url.type,
size: url.size,
modified: Date.now()
};
self.callback(snapshotObject);
};
}
done(true);
}
show(src) {
this.painterroInstance = this.painterro.show(src);
}
}

View File

@ -13,7 +13,6 @@
@import "~styles/controls";
@import "~styles/forms";
@import "~styles/table";
@import "~styles/layout";
@import "~styles/legacy";
@import "~styles/legacy-plots";
@import "~styles/plotly";

View File

@ -13,7 +13,6 @@
@import "~styles/controls";
@import "~styles/forms";
@import "~styles/table";
@import "~styles/layout";
@import "~styles/legacy";
@import "~styles/legacy-plots";
@import "~styles/plotly";

View File

@ -13,7 +13,6 @@
@import "~styles/controls";
@import "~styles/forms";
@import "~styles/table";
@import "~styles/layout";
@import "~styles/legacy";
@import "~styles/legacy-plots";
@import "~styles/plotly";

View File

@ -122,13 +122,8 @@ button {
margin-left: $interiorMargin;
}
$c1: nth($mixedSettingBg, 1);
$c2: nth($mixedSettingBg, 2);
$mixedBgD: $mixedSettingBgSize $mixedSettingBgSize;
&--mixed {
// E.g. click-icons in toolbar that apply to multiple selected items with different settings
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
@include mixedBg();
}
&--swatched {
@ -151,13 +146,6 @@ button {
flex: 1 1 auto;
font-size: 1.1em;
}
&--mixed {
// Styling for swatched buttons when settings are mixed
> [class*='swatch'] {
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
}
}
}
}
@ -244,18 +232,10 @@ button {
/******************************************************** SECTION */
section {
flex: 0 0 auto;
flex: 0 1 auto;
overflow: hidden;
+ section {
margin-top: $interiorMargin;
&.is-expanded {
margin-bottom: $interiorMargin * 3;
}
}
&.is-expanded {
flex: 0 1 auto;
}
.c-section__header {
@ -829,6 +809,10 @@ select {
box-shadow: rgba($colorBodyFg, 0.4) 0 0 3px;
flex: 0 0 auto;
padding: $interiorMargin $interiorMarginLg;
&--mixed {
@include mixedBg();
}
}
/******************************************************** SLIDERS */

View File

@ -88,6 +88,12 @@ body.desktop {
background: $scrollbarThumbColorMenuHov;
}
}
div, span {
// Firefox
scrollbar-color: $scrollbarThumbColor $scrollbarTrackColorBg;
scrollbar-width: thin;
}
}
/******************************************************** HTML ENTITIES */

View File

@ -1,87 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/************************** BROWSE BAR */
.l-browse-bar {
display: flex;
align-items: center;
justify-content: space-between;
[class*="__"] {
// Removes extraneous horizontal white space
display: inline-flex;
}
&__start {
display: flex;
align-items: center;
flex: 1 1 auto;
margin-right: $interiorMargin;
min-width: 0; // Forces interior to compress when pushed on
}
&__end {
display: flex;
align-items: center;
flex: 0 0 auto;
[class*="__"] + [class*="__"] {
margin-left: $interiorMarginSm;
}
}
&__nav-to-parent-button,
&__disclosure-button {
flex: 0 0 auto;
}
&__nav-to-parent-button {
// This is an icon-button
$p: $interiorMargin;
margin-right: $interiorMargin;
padding-left: $p;
padding-right: $p;
.is-editing & {
display: none;
}
}
&__object-name--w {
flex: 0 1 auto;
@include headerFont(1.4em);
min-width: 0;
&:before {
// Icon
margin-right: $interiorMargin;
}
}
&__object-name {
flex: 0 1 auto;
}
&__object-details {
opacity: 0.5;
}
}

View File

@ -72,6 +72,7 @@
display: flex;
align-items: center;
font-size: 1.25em;
overflow: hidden;
> * {
flex: 0 0 auto;
@ -82,6 +83,12 @@
&__value {
color: $colorBodyFgEm;
}
.c-frame & {
// When in a Display or Flexible Layout
@include abs();
padding: $interiorMargin;
}
}
.c-clock {

View File

@ -50,6 +50,14 @@
}
/************************** EFFECTS */
@mixin mixedBg() {
$c1: nth($mixedSettingBg, 1);
$c2: nth($mixedSettingBg, 2);
$mixedBgD: $mixedSettingBgSize $mixedSettingBgSize;
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
}
@mixin pulse($animName: pulse, $dur: 500ms, $iteration: infinite, $opacity0: 0.5, $opacity100: 1) {
@keyframes #{$animName} {
0% { opacity: $opacity0; }

View File

@ -1,7 +1,6 @@
@import "../api/overlays/components/dialog-component.scss";
@import "../api/overlays/components/overlay-component.scss";
@import "../plugins/condition/components/condition.scss";
@import "../plugins/condition/components/condition-set.scss";
@import "../plugins/condition/components/conditionals.scss";
@import "../plugins/conditionWidget/components/condition-widget.scss";
@import "../plugins/condition/components/inspector/conditional-styles.scss";
@import "../plugins/displayLayout/components/box-view.scss";

View File

@ -1,6 +1,8 @@
import MCT from 'MCT';
export function createOpenMct() {
delete require.cache[require.resolve('openmct')];
const openmct = new MCT();
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.UTCTimeSystem());

View File

@ -104,7 +104,7 @@ export default {
keys.forEach(key => {
let firstChild = this.$el.querySelector(':first-child');
if (firstChild) {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('transparent') > -1)) {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
if (firstChild.style[key]) {
firstChild.style[key] = '';
}

View File

@ -81,7 +81,7 @@ export default {
}
},
mounted() {
this.excludeObjectTypes = ['folder', 'webPage', 'conditionSet'];
this.excludeObjectTypes = ['folder', 'webPage', 'conditionSet', 'summary-widget', 'hyperlink'];
this.openmct.selection.on('change', this.updateInspectorViews);
},
destroyed() {

View File

@ -26,8 +26,8 @@
<script>
import ConditionalStylesView from '../../plugins/condition/components/inspector/ConditionalStylesView.vue';
import MultiSelectStylesView from '../../plugins/condition/components/inspector/MultiSelectStylesView.vue';
import Vue from 'vue';
import { getStyleProp } from "../../plugins/condition/utils/styleUtils";
export default {
inject: ['openmct'],
@ -44,35 +44,9 @@ export default {
this.openmct.selection.off('change', this.updateSelection);
},
methods: {
getStyleProperties(item) {
let styleProps = {};
Object.keys(item).forEach((key) => {
Object.assign(styleProps, getStyleProp(key, item[key]));
});
return styleProps;
},
updateSelection(selection) {
if (selection.length > 0 && selection[0].length > 0) {
let isChildItem = false;
let domainObject = selection[0][0].context.item;
let layoutItem = {};
let styleProps = this.getStyleProperties({
fill: 'transparent',
stroke: 'transparent',
color: 'transparent'
});
if (selection[0].length > 1) {
isChildItem = true;
//If there are more than 1 items in the selection[0] list, the first one could either be a sub domain object OR a layout drawing control.
//The second item in the selection[0] list is the container object (usually a layout)
if (!domainObject) {
styleProps = {};
layoutItem = selection[0][0].context.layoutItem;
styleProps = this.getStyleProperties(layoutItem);
domainObject = selection[0][1].context.item;
}
}
let template = selection.length > 1 ? '<multi-select-styles-view></multi-select-styles-view>' : '<conditional-styles-view></conditional-styles-view>';
if (this.component) {
this.component.$destroy();
this.component = undefined;
@ -83,20 +57,14 @@ export default {
this.component = new Vue({
provide: {
openmct: this.openmct,
domainObject: domainObject
selection: selection
},
el: viewContainer,
components: {
ConditionalStylesView
ConditionalStylesView,
MultiSelectStylesView
},
data() {
return {
layoutItem,
styleProps,
isChildItem
}
},
template: '<conditional-styles-view :can-hide="isChildItem" :item-id="layoutItem.id" :initial-styles="styleProps"></conditional-styles-view>'
template: template
});
}
}

View File

@ -28,6 +28,7 @@
<div class="l-browse-bar__end">
<view-switcher
v-if="!isEditing"
:current-view="currentView"
:views="views"
@setView="setView"

View File

@ -6,7 +6,7 @@
<button
class="c-button--menu"
:class="currentView.cssClass"
title="Switch view type"
title="Change the current view"
@click.stop="toggleViewMenu"
>
<span class="c-button__label">

View File

@ -1,3 +1,24 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/******************************* SHELL */
.l-shell {
position: absolute;
@ -126,6 +147,9 @@
body.mobile & .l-shell__main-view-browse-bar {
margin-left: $mobileMenuIconD; // Make room for the hamburger!
.c-button[class*='__actions__edit'] {
display: none; // Hide the main view edit button when in mobile context
}
}
&__head {
@ -269,6 +293,79 @@
}
}
/************************** BROWSE BAR */
.l-browse-bar {
display: flex;
align-items: center;
justify-content: space-between;
[class*="__"] {
// Removes extraneous horizontal white space
display: inline-flex;
}
&__start,
&__end,
&__actions {
display: flex;
align-items: center;
}
&__actions,
&__end {
> * + * {
margin-left: $interiorMarginSm;
}
}
&__start {
flex: 1 1 auto;
margin-right: $interiorMargin;
min-width: 0; // Forces interior to compress when pushed on
}
&__end {
flex: 0 0 auto;
}
&__nav-to-parent-button,
&__disclosure-button {
flex: 0 0 auto;
}
&__nav-to-parent-button {
// This is an icon-button
$p: $interiorMargin;
margin-right: $interiorMargin;
padding-left: $p;
padding-right: $p;
.is-editing & {
display: none;
}
}
&__object-name--w,
&__object-name {
flex: 0 1 auto;
}
&__object-name--w {
@include headerFont(1.4em);
min-width: 0;
&:before {
// Icon
margin-right: $interiorMargin;
}
}
&__object-details {
opacity: 0.5;
}
}
/************************** DRAWER */
.c-drawer {
/* New sliding overlay or push element to contain things
* Designed for mobile and compact desktop scenarios
@ -332,4 +429,3 @@
}
}
}