Compare commits

...

51 Commits

Author SHA1 Message Date
5a690932e9 Merge branch 'master' of https://github.com/nasa/openmct into notebook-preview-action-fix 2020-04-01 09:59:57 -07:00
b00757150e use objectPathJSON 2020-04-01 09:58:30 -07:00
a81af1ce34 check format first (#2851)
default to number
2020-03-31 21:04:52 -07:00
64c5725687 pass correct object path, and remove notebook snapshot button from preview 2020-03-31 20:59:20 -07:00
ee4a81bdfd Conditionals feature (#2830)
Introduces conditional styling feature.
2020-03-31 15:56:06 -07:00
e7e5116773 [Notebook] V2.0 development #2666 (#2755)
* Notebook v2.0
Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-03-31 12:11:11 -07:00
7b060509f5 c-object-label changes brought over from topic-conditionals (#2823)
- SCSS mods;
- Markup in select objects
2020-03-30 17:30:53 -07:00
3ca9e9ae56 When view params change, get a new copy of the domainObject (#2813)
Update browseObject on object change
2020-03-30 17:27:10 -07:00
87d838c690 [Tables] Use parsed data to sort (#2785)
* Use parsed values when sorting
2020-03-30 16:57:20 -07:00
a5326a7b95 Merge pull request #2810 from nasa/toolbar-fixes-32720
[Toolbar] fix errors when navigated away from a layout that uses toolbar
2020-03-30 11:13:17 -07:00
6b00af6ece fix errors when navigated away from a layout that uses toolbar 2020-03-27 10:15:09 -07:00
eedc0f13bc adds formatting for strings (#2792) 2020-03-26 13:08:28 -07:00
99a9f5b3ba Merge pull request #2790 from nasa/telemetry-metadata-manager-fix-32620
[Metadata] Fails when trying to apply reasonable defaults to an undefined values array
2020-03-26 10:41:29 -07:00
4b535ade31 metadataManager should check for a values array
before calling map on it
2020-03-26 10:25:04 -07:00
51498c0e75 Merge pull request #2781 from nasa/global-filters-no-request-bug-fix
Global Filters not re-requesting on value change
2020-03-25 11:57:30 -07:00
6dbdebbe05 Merge branch 'master' into global-filters-no-request-bug-fix 2020-03-25 11:54:45 -07:00
c994227d5d Merge pull request #2769 from nasa/telemetry-table-duplicate-request-fix
[Telemetry Tables] Duplicate requests when users change time system
2020-03-25 11:42:34 -07:00
761ca7ad56 remove console log 2020-03-25 11:16:25 -07:00
ca022b8a28 make a copy of filters before checking isEqual
- to prevent using same pointer
2020-03-25 11:11:17 -07:00
ec978f3a35 when a time system is changed, it emits a timeSystem update as well as a bounds update, this caused telemetry table to request data twice when users changed time systems 2020-03-23 14:21:46 -07:00
459b2060a5 Merge pull request #2727 from nasa/swg-randomness
Adds customizable randomness factor to sine wave generator
2020-03-20 10:25:17 -07:00
ddfa611c44 Merge branch 'master' into swg-randomness 2020-03-09 13:11:31 -07:00
5fcc4eebe1 Add a re-calculate column width button (#2719)
* Add a recalculate Column width button

* Tweaks to telemetry table for recalculateColumnWidths

- Recalc button now hidden if isAutosizeEnabled === false;
- Recalc button label, title edited for clarity;
- Normalized button titles for other table buttons;
- Fixed `.c-separator` height issue;

* toggle between expand and autosize

* Tweaked button title text

* remove nested loop

* fix lint errors

* remove unecessary promise and use clientWidth instead of offsetWidth

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-03-09 10:31:26 -07:00
8d86c914a1 Merge pull request #2732 from nasa/minmax-fix-382020
Revert minMax to minmax
2020-03-09 10:05:42 -07:00
fab04519c6 fix minmax typo 2020-03-08 20:11:04 -07:00
9b8b63a0d8 Revert record size limit to 5000 2020-03-05 08:56:47 -08:00
a1b1fa464e Adds customizable randomness factor to sine wave generator. Fixes #2726 2020-03-05 08:55:05 -08:00
47c388450f Merge pull request #2707 from nasa/alternate-control-bar-fixes
[Telemetry Tables] - Alternate control bar fixes
2020-03-04 11:54:38 -08:00
dd9b567025 revert alternate control bar for telemetry table 2020-03-02 11:09:50 -08:00
629ca089cf Fixes for table's alternateControlBar
- Fixed hide/show of controls for better UX;
- Unit tested click select/deselect toggling;
2020-02-28 11:31:23 -08:00
300acd6ec8 remove row if user unmarks in showMarkedRowsOnly mode 2020-02-28 11:12:49 -08:00
62774678a7 enable alternatte control bar in telemetry tables for charles 2020-02-28 11:04:34 -08:00
35d1727dbf Merge pull request #2696 from nasa/marking-improvement-tables
An improvement for components using telemetry tables
2020-02-27 17:20:08 -08:00
8125a4f321 emit event when rows are marked - useful for other components using telemetry tables 2020-02-27 17:05:02 -08:00
fbbdf8cff7 Merge pull request #2607 from krynju/f#2594
Time x-axis tick labels reversed - fix #2596
2020-02-27 11:11:05 -08:00
b0edb19239 Merge branch 'master' into f#2594 2020-02-27 11:06:59 -08:00
85902b878e Update telemetry table for multisession (#2686)
* update telemetry table to ingest marked row data, add a new alterntate bar with includes row name, selected rows and show selected rows toggle

* Enhancements for alternate toolbar in telem tables

- .c-control-bar adds style enhancements and `__label` element;
- Added `label` prop, markup and styling to ToggleSwitch;
- ToggleSwitch layout enhanced;
- Unit tested in main view and placed in Display Layout;

* made improvements to row marking

* bug fixes for marking

* fix linting issues

* -Make reviewer requested changes
-Clarify prop for marking
-Include alternateControlBar in the marking prop
- - since it only makes sense for making

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
2020-02-27 10:27:57 -08:00
a0b7999ea2 Imagery fixes (#2668)
* Fix imagery-related styles and markup

- VERY WIP!!!
- Style modernizing;
- Also, padding fixes for pane.scss - unit test for regressions!

* Fix imagery-related styles and markup

- VERY WIP!!!
- Style modernizing WIP;
- Fixes to pane classes for better padding in vertical panes;

* Fix imagery-related styles and markup

- Migrated all imagery CSS into imagery-view-layout.scss from _legacy
.scss;
- CSS cleanups;
- Refactoring/simplification of thumb layout;
- Color fixed for $colorPausedFg in theme constants;

* Scroll to right instead of bottom, on autoscroll.

* Fix imagery-related styles and markup

- Make the most recent thumb visually distinct;
- Clicking a selected thumb now deselects it and unpauses the view;

* Imagery fixes

- Fixed thumb click logic to properly toggle paused when clicking a selected thumb;
- Improved CSS so that `selected` updates more quickly when selecting the latest thumb;
- Clicking the main image pause button now selects the proper thumb;

* Fix linting errors

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2020-02-25 11:47:27 -08:00
682601477c Add glyphs (#2667)
- icon-flag for use with VISTA Frame Accountability;
- icon-conditional, and bg-icon-conditional for Conditionals;
2020-02-13 13:11:24 -08:00
0a9d23d86f Merge pull request #2633 from nasa/pane-persistence
[Navigation Tree] save current state of navigation tree
2020-02-06 16:23:19 -08:00
379e37c881 Merge branch 'master' into pane-persistence 2020-02-06 16:17:59 -08:00
37ef269dce Merge pull request #2611 from nasa/filters-fetch-bug-fix
Check if filters are not equal before refetching
2020-02-06 16:16:12 -08:00
38deef6e72 Merge branch 'master' into filters-fetch-bug-fix 2020-02-03 13:45:26 -08:00
b6220288ac Merge branch 'master' into f#2594 2020-02-03 13:36:29 -08:00
2e82edb306 remove unused var 2020-01-27 13:44:56 -08:00
8f0e773ac1 remove storing navigated to local storage 2020-01-27 13:43:13 -08:00
223a0feada comments to describe localStorage implementation 2020-01-27 10:17:27 -08:00
0fd0da8331 rework local storage mechanisms
* use one navigated local storage item instead one per node

* use one expanded local storage item instead of one per node

* fix navigated

* collapse children when node collapsed
2020-01-27 01:06:40 -08:00
fa21911287 persist expanded and navigated state of node to local storage 2020-01-25 00:52:36 -08:00
03829af2ad check if filters are not equal before refetching 2020-01-05 20:56:53 -08:00
5d31806fb7 fix #2596 2019-12-22 01:31:23 +01:00
167 changed files with 10796 additions and 2077 deletions

View File

@ -1,5 +1,5 @@
<!--
Open MCT, Copyright (c) 2014-2018, United States Government
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
@ -18,4 +18,4 @@
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.
-->
-->

View File

@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@ -18,4 +18,4 @@
* 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.
*****************************************************************************/
*****************************************************************************/

View File

@ -9,7 +9,8 @@ define([
values: [
{
key: "name",
name: "Name"
name: "Name",
format: "string"
},
{
key: "utc",

View File

@ -31,6 +31,7 @@ define([
period: 10,
offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0
};
@ -52,7 +53,8 @@ define([
'period',
'offset',
'dataRateInHz',
'phase'
'phase',
'randomness'
];
request = request || {};

View File

@ -65,8 +65,8 @@
name: data.name,
utc: nextStep,
yesterday: nextStep - 60*60*24*1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase)
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
}
});
nextStep += step;
@ -99,6 +99,7 @@
var offset = request.offset;
var dataRateInHz = request.dataRateInHz;
var phase = request.phase;
var randomness = request.randomness;
var step = 1000 / dataRateInHz;
var nextStep = start - (start % step) + step;
@ -110,8 +111,8 @@
name: request.name,
utc: nextStep,
yesterday: nextStep - 60*60*24*1000,
sin: sin(nextStep, period, amplitude, offset, phase),
cos: cos(nextStep, period, amplitude, offset, phase)
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
});
}
self.postMessage({
@ -120,14 +121,14 @@
});
}
function cos(timestamp, period, amplitude, offset, phase) {
function cos(timestamp, period, amplitude, offset, phase, randomness) {
return amplitude *
Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + offset;
Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}
function sin(timestamp, period, amplitude, offset, phase) {
function sin(timestamp, period, amplitude, offset, phase, randomness) {
return amplitude *
Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + offset;
Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}
function sendError(error, message) {

View File

@ -122,6 +122,17 @@ define([
"telemetry",
"phase"
]
},
{
name: "Randomness",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "randomness",
required: true,
property: [
"telemetry",
"randomness"
]
}
],
initialize: function (object) {
@ -130,7 +141,8 @@ define([
amplitude: 1,
offset: 0,
dataRateInHz: 1,
phase: 0
phase: 0,
randomness: 0
};
}
});

View File

@ -64,6 +64,7 @@
"request": "^2.69.0",
"split": "^1.0.0",
"style-loader": "^1.0.1",
"uuid": "^3.3.3",
"v8-compile-cache": "^1.1.0",
"vue": "2.5.6",
"vue-loader": "^15.2.6",

View File

@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@ -264,6 +264,8 @@ define([
this.install(this.plugins.GoToOriginalAction());
this.install(this.plugins.ImportExport());
this.install(this.plugins.WebPage());
this.install(this.plugins.Condition());
this.install(this.plugins.ConditionWidget());
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@ -81,7 +81,7 @@ define([
function TelemetryMetadataManager(metadata) {
this.metadata = metadata;
this.valueMetadatas = this.metadata.values.map(applyReasonableDefaults);
this.valueMetadatas = this.metadata.values ? this.metadata.values.map(applyReasonableDefaults) : [];
}
/**

View File

@ -81,6 +81,24 @@ define([
return printj.sprintf(formatString, baseFormat.call(this, value));
};
}
if (valueMetadata.format === 'string') {
this.formatter.parse = function (value) {
if (value === undefined) {
return '';
}
if (typeof value === 'string') {
return value;
} else {
return value.toString();
}
};
this.formatter.format = function (value) {
return value;
};
this.formatter.validate = function (value) {
return typeof value === 'string';
};
}
}
TelemetryValueFormatter.prototype.parse = function (datum) {

View File

@ -21,22 +21,24 @@
*****************************************************************************/
<template>
<table class="c-table c-lad-table">
<thead>
<tr>
<th>Name</th>
<th>Timestamp</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<lad-row
v-for="item in items"
:key="item.key"
:domain-object="item.domainObject"
/>
</tbody>
</table>
<div class="c-lad-table-wrapper">
<table class="c-table c-lad-table">
<thead>
<tr>
<th>Name</th>
<th>Timestamp</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<lad-row
v-for="item in items"
:key="item.key"
:domain-object="item.domainObject"
/>
</tbody>
</table>
</div>
</template>
<script>

View File

@ -0,0 +1,309 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import uuid from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { TRIGGER } from "./utils/constants";
import {computeCondition, computeConditionByLimit} from "./utils/evaluator";
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
/*
* conditionConfiguration = {
* id: uuid,
* trigger: 'any'/'all'/'not','xor',
* criteria: [
* {
* telemetry: '',
* operation: '',
* input: [],
* metadata: ''
* }
* ]
* }
*/
export default class ConditionClass extends EventEmitter {
/**
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
* @constructor
* @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
* @param openmct
*/
constructor(conditionConfiguration, openmct, conditionManager) {
super();
this.openmct = openmct;
this.conditionManager = conditionManager;
this.id = conditionConfiguration.id;
this.criteria = [];
this.criteriaResults = {};
this.result = undefined;
this.latestTimestamp = {};
if (conditionConfiguration.configuration.criteria) {
this.createCriteria(conditionConfiguration.configuration.criteria);
}
this.trigger = conditionConfiguration.configuration.trigger;
this.conditionManager.on('broadcastTelemetry', this.handleBroadcastTelemetry, this);
}
handleBroadcastTelemetry(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);
}
});
}
update(conditionConfiguration) {
this.updateTrigger(conditionConfiguration.configuration.trigger);
this.updateCriteria(conditionConfiguration.configuration.criteria);
}
updateTrigger(trigger) {
if (this.trigger !== trigger) {
this.trigger = trigger;
this.handleConditionUpdated();
}
}
generateCriterion(criterionConfiguration) {
return {
id: uuid(),
telemetry: criterionConfiguration.telemetry || '',
telemetryObject: this.conditionManager.telemetryObjects[this.openmct.objects.makeKeyString(criterionConfiguration.telemetry)],
operation: criterionConfiguration.operation || '',
input: criterionConfiguration.input === undefined ? [] : criterionConfiguration.input,
metadata: criterionConfiguration.metadata || ''
};
}
createCriteria(criterionConfigurations) {
criterionConfigurations.forEach((criterionConfiguration) => {
this.addCriterion(criterionConfiguration);
});
}
updateCriteria(criterionConfigurations) {
this.destroyCriteria();
this.createCriteria(criterionConfigurations);
}
updateTelemetry() {
this.criteria.forEach((criterion) => {
criterion.updateTelemetry(this.conditionManager.telemetryObjects);
});
}
/**
* adds criterion to the condition.
*/
addCriterion(criterionConfiguration) {
let criterion;
let criterionConfigurationWithId = this.generateCriterion(criterionConfiguration || null);
if (criterionConfiguration.telemetry && (criterionConfiguration.telemetry === 'any' || criterionConfiguration.telemetry === 'all')) {
criterion = new AllTelemetryCriterion(criterionConfigurationWithId, this.openmct);
} else {
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 = [];
}
this.criteria.push(criterion);
return criterionConfigurationWithId.id;
}
findCriterion(id) {
let criterion;
for (let i=0, ii=this.criteria.length; i < ii; i ++) {
if (this.criteria[i].id === id) {
criterion = {
item: this.criteria[i],
index: i
}
}
}
return criterion;
}
updateCriterion(id, criterionConfiguration) {
let found = this.findCriterion(id);
if (found) {
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();
}
}
destroyCriterion(id) {
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);
});
this.criteria.splice(found.index, 1);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id];
}
return true;
}
return false;
}
handleCriterionUpdated(criterion) {
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
.map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects}));
return Promise.all(criteriaResults)
.then(results => {
results.forEach(result => {
this.updateCriteriaResults(result);
this.latestTimestamp = this.getLatestTimestamp(this.latestTimestamp, result.data)
});
this.evaluate();
return {
id: this.id,
data: Object.assign({}, this.latestTimestamp, { result: this.result })
}
});
}
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;
}
destroyCriteria() {
let success = true;
//looping through the array backwards since destroyCriterion modifies the criteria array
for (let i=this.criteria.length-1; i >= 0; i--) {
success = success && this.destroyCriterion(this.criteria[i].id);
}
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

@ -0,0 +1,314 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Condition from "./Condition";
import uuid from "uuid";
import EventEmitter from 'EventEmitter';
export default class ConditionManager extends EventEmitter {
constructor(conditionSetDomainObject, openmct) {
super();
this.openmct = openmct;
this.conditionSetDomainObject = conditionSetDomainObject;
this.timeAPI = this.openmct.time;
this.latestTimestamp = {};
this.composition = this.openmct.composition.get(conditionSetDomainObject);
this.composition.on('add', this.subscribeToTelemetry, this);
this.composition.on('remove', this.unsubscribeFromTelemetry, this);
this.compositionLoad = this.composition.load();
this.subscriptions = {};
this.telemetryObjects = {};
this.testData = {conditionTestData: [], applied: false};
this.initialize();
this.stopObservingForChanges = this.openmct.objects.observe(this.conditionSetDomainObject, '*', (newDomainObject) => {
this.conditionSetDomainObject = newDomainObject;
});
}
subscribeToTelemetry(endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
if (this.subscriptions[id]) {
console.log('subscription already exists');
return;
}
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.updateConditionTelemetry();
}
unsubscribeFromTelemetry(endpointIdentifier) {
const id = this.openmct.objects.makeKeyString(endpointIdentifier);
if (!this.subscriptions[id]) {
console.log('no subscription to remove');
return;
}
this.subscriptions[id]();
delete this.subscriptions[id];
delete this.telemetryObjects[id];
}
initialize() {
this.conditionResults = {};
this.conditionClassCollection = [];
if (this.conditionSetDomainObject.configuration.conditionCollection.length) {
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
this.initCondition(conditionConfiguration, index);
});
}
}
updateConditionTelemetry() {
this.conditionClassCollection.forEach((condition) => condition.updateTelemetry());
}
updateCondition(conditionConfiguration, index) {
let condition = this.conditionClassCollection[index];
condition.update(conditionConfiguration);
this.conditionSetDomainObject.configuration.conditionCollection[index] = conditionConfiguration;
this.persistConditions();
}
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 {
this.conditionClassCollection.unshift(condition);
}
}
createCondition(conditionConfiguration) {
let conditionObj;
if (conditionConfiguration) {
conditionObj = {
...conditionConfiguration,
id: uuid(),
configuration: {
...conditionConfiguration.configuration,
name: `Copy of ${conditionConfiguration.configuration.name}`
}
};
} else {
conditionObj = {
id: uuid(),
configuration: {
name: 'Unnamed Condition',
output: 'false',
trigger: 'all',
criteria: [{
telemetry: '',
operation: '',
input: [],
metadata: ''
}]
},
summary: ''
};
}
return conditionObj;
}
addCondition() {
this.createAndSaveCondition();
}
cloneCondition(conditionConfiguration, index) {
this.createAndSaveCondition(index, JSON.parse(JSON.stringify(conditionConfiguration)));
}
createAndSaveCondition(index, conditionConfiguration) {
const newCondition = this.createCondition(conditionConfiguration);
if (index !== undefined) {
this.conditionSetDomainObject.configuration.conditionCollection.splice(index + 1, 0, newCondition);
} else {
this.conditionSetDomainObject.configuration.conditionCollection.unshift(newCondition);
}
this.initCondition(newCondition, index);
this.persistConditions();
}
removeCondition(index) {
let condition = this.conditionClassCollection[index];
condition.destroyCriteria();
condition.off('conditionResultUpdated', this.handleConditionResult.bind(this));
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 = [];
reorderPlan.forEach((reorderEvent) => {
let item = oldConditions[reorderEvent.oldIndex];
newCollection.push(item);
this.conditionSetDomainObject.configuration.conditionCollection = newCollection;
});
this.persistConditions();
}
getCurrentCondition() {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length-1];
for (let i = 0; i < conditionCollection.length - 1; i++) {
if (this.conditionResults[conditionCollection[i].id]) {
//first condition to be true wins
currentCondition = conditionCollection[i];
break;
}
}
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 || this.conditionClassCollection.length === 1) {
return Promise.resolve([]);
}
return this.compositionLoad.then(() => {
const ladConditionResults = this.conditionClassCollection
.map(condition => condition.requestLADConditionResult());
return Promise.all(ladConditionResults)
.then((results) => {
results.forEach(resultObj => { this.updateConditionResults(resultObj); });
const currentCondition = this.getCurrentCondition();
return Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
this.latestTimestamp
);
});
});
}
broadcastTelemetry(id, datum) {
this.emit(`broadcastTelemetry`, Object.assign({}, this.createNormalizedDatum(datum, id), {id: id}));
}
getTestData(metadatum) {
let data = undefined;
if (this.testData.applied) {
const found = this.testData.conditionTestInputs.find((testInput) => (testInput.metadata === metadatum.source));
if (found) {
data = found.value;
}
}
return data;
}
createNormalizedDatum(telemetryDatum, id) {
return Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((normalizedDatum, 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;
}, {});
}
updateTestData(testData) {
this.testData = testData;
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs);
}
persistConditions() {
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionCollection', this.conditionSetDomainObject.configuration.conditionCollection);
}
destroy() {
this.composition.off('add', this.subscribeToTelemetry, this);
this.composition.off('remove', this.unsubscribeFromTelemetry, this);
Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe());
this.subscriptions = undefined;
if(this.stopObservingForChanges) {
this.stopObservingForChanges();
}
this.conditionClassCollection.forEach((condition) => {
condition.off('conditionResultUpdated', this.handleConditionResult);
condition.destroy();
})
}
}

View File

@ -0,0 +1,126 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ConditionManager from './ConditionManager';
describe('ConditionManager', () => {
let conditionMgr;
let mockListener;
let openmct = {};
let mockCondition = {
isDefault: true,
id: '1234-5678',
configuration: {
criteria: []
}
};
let conditionSetDomainObject = {
identifier: {
namespace: "",
key: "600a7372-8d48-4dc4-98b6-548611b1ff7e"
},
type: "conditionSet",
location: "mine",
configuration: {
conditionCollection: [
mockCondition
]
}
};
let mockComposition;
let loader;
function mockAngularComponents() {
let mockInjector = jasmine.createSpyObj('$injector', ['get']);
let mockInstantiate = jasmine.createSpy('mockInstantiate');
mockInstantiate.and.returnValue(mockInstantiate);
let mockDomainObject = {
useCapability: function () {
return mockCondition;
}
};
mockInstantiate.and.callFake(function () {
return mockDomainObject;
});
mockInjector.get.and.callFake(function (service) {
return {
'instantiate': mockInstantiate
}[service];
});
openmct.$injector = mockInjector;
}
beforeEach(function () {
mockAngularComponents();
mockListener = jasmine.createSpy('mockListener');
loader = {};
loader.promise = new Promise(function (resolve, reject) {
loader.resolve = resolve;
loader.reject = reject;
});
mockComposition = jasmine.createSpyObj('compositionCollection', [
'load',
'on',
'off'
]);
mockComposition.load.and.callFake(() => {
setTimeout(() => {
loader.resolve();
});
return loader.promise;
});
mockComposition.on('add', mockListener);
mockComposition.on('remove', mockListener);
openmct.composition = jasmine.createSpyObj('compositionAPI', [
'get'
]);
openmct.composition.get.and.returnValue(mockComposition);
openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString', 'observe', 'mutate']);
openmct.objects.get.and.returnValues(new Promise(function (resolve, reject) {
resolve(conditionSetDomainObject);
}), new Promise(function (resolve, reject) {
resolve(mockCondition);
}));
openmct.objects.makeKeyString.and.returnValue(conditionSetDomainObject.identifier.key);
openmct.objects.observe.and.returnValue(function () {});
openmct.objects.mutate.and.returnValue(function () {});
conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.on('broadcastTelemetry', mockListener);
});
it('creates a conditionCollection with a default condition', function () {
expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(1);
let defaultConditionId = conditionMgr.conditionClassCollection[0].id;
expect(defaultConditionId).toEqual(mockCondition.id);
});
});

View File

@ -0,0 +1,33 @@
/*****************************************************************************
* 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 default function ConditionSetCompositionPolicy(openmct) {
return {
allow: function (parent, child) {
if (parent.type === 'conditionSet' && !openmct.telemetry.isTelemetryObject(child)) {
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,85 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ConditionSetCompositionPolicy from './ConditionSetCompositionPolicy';
describe('ConditionSetCompositionPolicy', () => {
let policy;
let testTelemetryObject;
let openmct = {};
let parentDomainObject;
beforeAll(function () {
testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}]
}
};
openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']);
openmct.objects.get.and.returnValue(testTelemetryObject);
openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key);
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject']);
policy = new ConditionSetCompositionPolicy(openmct);
parentDomainObject = {};
});
it('returns true for object types that are not conditionSets', function () {
parentDomainObject.type = 'random';
openmct.telemetry.isTelemetryObject.and.returnValue(false);
expect(policy.allow(parentDomainObject, {})).toBe(true);
});
it('returns false for object types that are not telemetry objects when parent is a conditionSet', function () {
parentDomainObject.type = 'conditionSet';
openmct.telemetry.isTelemetryObject.and.returnValue(false);
expect(policy.allow(parentDomainObject, {})).toBe(false);
});
it('returns true for object types that are telemetry objects when parent is a conditionSet', function () {
parentDomainObject.type = 'conditionSet';
openmct.telemetry.isTelemetryObject.and.returnValue(true);
expect(policy.allow(parentDomainObject, testTelemetryObject)).toBe(true);
});
it('returns true for object types that are telemetry objects when parent is not a conditionSet', function () {
parentDomainObject.type = 'random';
openmct.telemetry.isTelemetryObject.and.returnValue(true);
expect(policy.allow(parentDomainObject, testTelemetryObject)).toBe(true);
});
});

View File

@ -0,0 +1,68 @@
/*****************************************************************************
* 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 default class ConditionSetMetadataProvider {
constructor(openmct) {
this.openmct = openmct;
}
supportsMetadata(domainObject) {
return domainObject.type === 'conditionSet';
}
getDomains(domainObject) {
return this.openmct.time.getAllTimeSystems().map(function (ts, i) {
return {
key: ts.key,
name: ts.name,
format: ts.timeFormat,
hints: {
domain: i
}
};
});
}
getMetadata(domainObject) {
const enumerations = domainObject.configuration.conditionCollection
.map((condition, index) => {
return {
string: condition.configuration.output,
value: index
};
});
return {
values: this.getDomains().concat([
{
name: 'Output',
key: 'output',
format: 'enum',
enumerations: enumerations,
hints: {
range: 1
}
}
])
};
}
}

View File

@ -0,0 +1,86 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ConditionManager from './ConditionManager'
export default class ConditionSetTelemetryProvider {
constructor(openmct) {
this.openmct = openmct;
this.conditionManagerPool = {};
}
isTelemetryObject(domainObject) {
return domainObject.type === 'conditionSet';
}
supportsRequest(domainObject) {
return domainObject.type === 'conditionSet';
}
supportsSubscribe(domainObject) {
return domainObject.type === 'conditionSet';
}
request(domainObject) {
let conditionManager = this.getConditionManager(domainObject);
return conditionManager.requestLADConditionSetOutput()
.then(latestOutput => {
return latestOutput ? [latestOutput] : [];
});
}
subscribe(domainObject, callback) {
let conditionManager = this.getConditionManager(domainObject);
conditionManager.on('conditionSetResultUpdated', callback);
return this.destroyConditionManager.bind(this, this.openmct.objects.makeKeyString(domainObject.identifier));
}
/**
* returns conditionManager instance for corresponding domain object
* creates the instance if it is not yet created
* @private
*/
getConditionManager(domainObject) {
const id = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.conditionManagerPool[id]) {
this.conditionManagerPool[id] = new ConditionManager(domainObject, this.openmct);
}
return this.conditionManagerPool[id];
}
/**
* cleans up and destroys conditionManager instance for corresponding domain object id
* can be called manually for views that only request but do not subscribe to data
*/
destroyConditionManager(id) {
if (this.conditionManagerPool[id]) {
this.conditionManagerPool[id].off('conditionSetResultUpdated');
this.conditionManagerPool[id].destroy();
delete this.conditionManagerPool[id];
}
}
}

View File

@ -0,0 +1,33 @@
/*****************************************************************************
* 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.
*****************************************************************************/
function ConditionSetViewPolicy() {
}
ConditionSetViewPolicy.prototype.allow = function (view, domainObject) {
if (domainObject.getModel().type === 'conditionSet') {
return view.key === 'conditionSet.view';
}
return true;
}
export default ConditionSetViewPolicy;

View File

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

View File

@ -0,0 +1,136 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Condition from "./Condition";
import {TRIGGER} from "./utils/constants";
import TelemetryCriterion from "./criterion/TelemetryCriterion";
let openmct = {},
mockListener,
testConditionDefinition,
testTelemetryObject,
conditionObj,
conditionManager,
mockBroadcastTelemetry;
describe("The condition", function () {
beforeEach (() => {
conditionManager = jasmine.createSpyObj('conditionManager',
['on']
);
mockBroadcastTelemetry = jasmine.createSpy('listener');
conditionManager.on('broadcastTelemetry', mockBroadcastTelemetry);
mockListener = jasmine.createSpy('listener');
testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}]
}
};
conditionManager.telemetryObjects = {
"test-object": testTelemetryObject
};
openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']);
openmct.objects.get.and.returnValue(new Promise(function (resolve, reject) {
resolve(testTelemetryObject);
})); openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key);
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', 'subscribe', 'getMetadata']);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values);
testConditionDefinition = {
id: '123-456',
configuration: {
name: 'mock condition',
output: 'mock output',
trigger: TRIGGER.ANY,
criteria: [
{
id: '1234-5678-9999-0000',
operation: 'equalTo',
input: ['0'],
metadata: 'value',
telemetry: testTelemetryObject.identifier
}
]
}
};
conditionObj = new Condition(
testConditionDefinition,
openmct,
conditionManager
);
conditionObj.on('conditionUpdated', mockListener);
});
it("generates criteria with the correct properties", function () {
const testCriterion = testConditionDefinition.configuration.criteria[0];
let criterion = conditionObj.generateCriterion(testCriterion);
expect(criterion.id).toBeDefined();
expect(criterion.operation).toEqual(testCriterion.operation);
expect(criterion.input).toEqual(testCriterion.input);
expect(criterion.metadata).toEqual(testCriterion.metadata);
expect(criterion.telemetry).toEqual(testCriterion.telemetry);
});
it("initializes with an id", function () {
expect(conditionObj.id).toBeDefined();
});
it("initializes with criteria from the condition definition", function () {
expect(conditionObj.criteria.length).toEqual(1);
let criterion = conditionObj.criteria[0];
expect(criterion instanceof TelemetryCriterion).toBeTrue();
expect(criterion.operator).toEqual(testConditionDefinition.configuration.criteria[0].operator);
expect(criterion.input).toEqual(testConditionDefinition.configuration.criteria[0].input);
expect(criterion.metadata).toEqual(testConditionDefinition.configuration.criteria[0].metadata);
});
it("initializes with the trigger from the condition definition", function () {
expect(conditionObj.trigger).toEqual(testConditionDefinition.configuration.trigger);
});
it("destroys all criteria for a condition", function () {
const result = conditionObj.destroyCriteria();
expect(result).toBeTrue();
expect(conditionObj.criteria.length).toEqual(0);
});
});

View File

@ -0,0 +1,128 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
export default class StyleRuleManager extends EventEmitter {
constructor(styleConfiguration, openmct, callback) {
super();
this.openmct = openmct;
this.callback = callback;
if (styleConfiguration) {
this.initialize(styleConfiguration);
if (styleConfiguration.conditionSetIdentifier) {
this.subscribeToConditionSet();
} else {
this.applyStaticStyle();
}
}
}
initialize(styleConfiguration) {
this.conditionSetIdentifier = styleConfiguration.conditionSetIdentifier;
this.staticStyle = styleConfiguration.staticStyle;
this.updateConditionStylesMap(styleConfiguration.styles || []);
}
subscribeToConditionSet() {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {
this.openmct.telemetry.request(conditionSetDomainObject)
.then(output => {
if (output && output.length) {
this.handleConditionSetResultUpdated(output[0]);
}
});
this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, output => this.handleConditionSetResultUpdated(output));
});
}
updateObjectStyleConfig(styleConfiguration) {
if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) {
this.initialize(styleConfiguration || {});
this.destroy();
} else {
let isNewConditionSet = !this.conditionSetIdentifier ||
!this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier);
this.initialize(styleConfiguration);
//Only resubscribe if the conditionSet has changed.
if (isNewConditionSet) {
this.subscribeToConditionSet();
}
}
}
updateConditionStylesMap(conditionStyles) {
let conditionStyleMap = {};
conditionStyles.forEach((conditionStyle) => {
if (conditionStyle.conditionId) {
conditionStyleMap[conditionStyle.conditionId] = conditionStyle.style;
} else {
conditionStyleMap.static = conditionStyle.style;
}
});
this.conditionalStyleMap = conditionStyleMap;
}
handleConditionSetResultUpdated(resultData) {
let foundStyle = this.conditionalStyleMap[resultData.conditionId];
if (foundStyle) {
if (foundStyle !== this.currentStyle) {
this.currentStyle = foundStyle;
}
this.updateDomainObjectStyle();
} else {
this.applyStaticStyle();
}
}
updateDomainObjectStyle() {
if (this.callback) {
this.callback(Object.assign({}, this.currentStyle));
}
}
applyStaticStyle() {
if (this.staticStyle) {
this.currentStyle = this.staticStyle.style;
} else {
if (this.currentStyle) {
Object.keys(this.currentStyle).forEach(key => {
this.currentStyle[key] = 'transparent';
});
}
}
this.updateDomainObjectStyle();
}
destroy() {
this.applyStaticStyle();
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
delete this.stopProvidingTelemetry;
this.conditionSetIdentifier = undefined;
}
}

View File

@ -0,0 +1,325 @@
/*****************************************************************************
* 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 v-if="isEditing"
class="c-condition c-condition--edit js-condition-drag-wrapper"
>
<!-- Edit view -->
<div class="c-condition__header">
<span class="c-condition__drag-grippy c-grippy c-grippy--vertical-drag"
title="Drag to reorder conditions"
:class="[{ 'is-enabled': !condition.isDefault }, { 'hide-nice': condition.isDefault }]"
:draggable="!condition.isDefault"
@dragstart="dragStart"
@dragstop="dragStop"
@dragover.stop
></span>
<span class="c-condition__disclosure c-disclosure-triangle c-tree__item__view-control is-enabled"
:class="{ 'c-disclosure-triangle--expanded': expanded }"
@click="expanded = !expanded"
></span>
<span class="c-condition__name">{{ condition.configuration.name }}</span>
<!-- TODO: description should be derived from criteria -->
<span class="c-condition__summary">
<template v-if="!canEvaluateCriteria">
Define criteria
</template>
<span v-else>
<condition-description :show-label="false"
:condition="condition"
/>
</span>
</span>
<div class="c-condition__buttons">
<button v-if="!condition.isDefault"
class="c-click-icon c-condition__duplicate-button icon-duplicate"
title="Duplicate this condition"
@click="cloneCondition"
></button>
<button v-if="!condition.isDefault"
class="c-click-icon c-condition__delete-button icon-trash"
title="Delete this condition"
@click="removeCondition"
></button>
</div>
</div>
<div v-if="expanded"
class="c-condition__definition c-cdef"
>
<span class="c-cdef__separator c-row-separator"></span>
<span class="c-cdef__label">Condition Name</span>
<span class="c-cdef__controls">
<input v-model="condition.configuration.name"
class="t-condition-input__name"
type="text"
@blur="persist"
>
</span>
<span class="c-cdef__label">Output</span>
<span class="c-cdef__controls">
<span class="c-cdef__control">
<select v-model="selectedOutputSelection"
@change="setOutputValue"
>
<option v-for="option in outputOptions"
:key="option"
:value="option"
>
{{ initCap(option) }}
</option>
</select>
</span>
<span class="c-cdef__control">
<input v-if="selectedOutputSelection === outputOptions[2]"
v-model="condition.configuration.output"
class="t-condition-name-input"
type="text"
@blur="persist"
>
</span>
</span>
<div v-if="!condition.isDefault"
class="c-cdef__match-and-criteria"
>
<span class="c-cdef__separator c-row-separator"></span>
<span class="c-cdef__label">Match</span>
<span class="c-cdef__controls">
<select v-model="condition.configuration.trigger"
@change="persist"
>
<option v-for="option in triggers"
:key="option.value"
:value="option.value"
> {{ option.label }}</option>
</select>
</span>
<template v-if="telemetry.length || condition.configuration.criteria.length">
<div v-for="(criterion, index) in condition.configuration.criteria"
:key="index"
class="c-cdef__criteria"
>
<Criterion :telemetry="telemetry"
:criterion="criterion"
:index="index"
:trigger="condition.configuration.trigger"
:is-default="condition.configuration.criteria.length === 1"
@persist="persist"
/>
<div class="c-cdef__criteria__buttons">
<button class="c-click-icon c-cdef__criteria-duplicate-button icon-duplicate"
title="Duplicate this criteria"
@click="cloneCriterion(index)"
></button>
<button v-if="!(condition.configuration.criteria.length === 1)"
class="c-click-icon c-cdef__criteria-duplicate-button icon-trash"
title="Delete this criteria"
@click="removeCriterion(index)"
></button>
</div>
</div>
</template>
<div class="c-cdef__separator c-row-separator"></div>
<div class="c-cdef__controls"
:disabled="!telemetry.length"
>
<button
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
@click="addCriteria"
>
<span class="c-button__label">Add Criteria</span>
</button>
</div>
</div>
</div>
</div>
<div v-else
class="c-condition c-condition--browse"
>
<!-- Browse view -->
<div class="c-condition__header">
<span class="c-condition__name">
{{ condition.configuration.name }}
</span>
<span class="c-condition__output">
Output: {{ condition.configuration.output }}
</span>
</div>
<div class="c-condition__summary">
<condition-description :show-label="false"
:condition="condition"
/>
</div>
</div>
</template>
<script>
import Criterion from './Criterion.vue';
import ConditionDescription from "./ConditionDescription.vue";
import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
export default {
inject: ['openmct'],
components: {
Criterion,
ConditionDescription
},
props: {
condition: {
type: Object,
required: true
},
conditionIndex: {
type: Number,
required: true
},
isEditing: {
type: Boolean,
required: true
},
telemetry: {
type: Array,
required: true,
default: () => []
}
},
data() {
return {
currentCriteria: this.currentCriteria,
expanded: true,
trigger: 'all',
selectedOutputSelection: '',
outputOptions: ['false', 'true', 'string'],
criterionIndex: 0,
selectedTelemetryName: '',
selectedFieldName: ''
};
},
computed: {
triggers() {
const keys = Object.keys(TRIGGER);
const triggerOptions = [];
keys.forEach((trigger) => {
triggerOptions.push({
value: TRIGGER[trigger],
label: TRIGGER_LABEL[TRIGGER[trigger]]
});
});
return triggerOptions;
},
canEvaluateCriteria: function () {
let criteria = this.condition.configuration.criteria;
if (criteria.length) {
let lastCriterion = criteria[criteria.length - 1];
if (lastCriterion.telemetry &&
lastCriterion.operation &&
(lastCriterion.input.length ||
lastCriterion.operation === 'isDefined' ||
lastCriterion.operation === 'isUndefined')) {
return true;
}
}
return false;
}
},
destroyed() {
this.destroy();
},
mounted() {
this.setOutputSelection();
},
methods: {
setOutputSelection() {
let conditionOutput = this.condition.configuration.output;
if (conditionOutput) {
if (conditionOutput !== 'false' && conditionOutput !== 'true') {
this.selectedOutputSelection = 'string';
} else {
this.selectedOutputSelection = conditionOutput;
}
}
},
setOutputValue() {
if (this.selectedOutputSelection === 'string') {
this.condition.configuration.output = '';
} else {
this.condition.configuration.output = this.selectedOutputSelection;
}
this.persist();
},
addCriteria() {
const criteriaObject = {
telemetry: '',
operation: '',
input: '',
metadata: ''
};
this.condition.configuration.criteria.push(criteriaObject);
},
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);
this.$emit('setMoveIndex', this.conditionIndex);
},
dragStop(e) {
e.dataTransfer.clearData();
},
destroy() {
},
removeCondition(ev) {
this.$emit('removeCondition', this.conditionIndex);
},
cloneCondition(ev) {
this.$emit('cloneCondition', {
condition: this.condition,
index: this.conditionIndex
});
},
removeCriterion(index) {
this.condition.configuration.criteria.splice(index, 1);
this.persist();
},
cloneCriterion(index) {
const clonedCriterion = JSON.parse(JSON.stringify(this.condition.configuration.criteria[index]));
this.condition.configuration.criteria.splice(index + 1, 0, clonedCriterion);
this.persist();
},
persist() {
this.$emit('updateCondition', {
condition: this.condition,
index: this.conditionIndex
});
},
initCap: function (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
}
</script>

View File

@ -0,0 +1,245 @@
/*****************************************************************************
* 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>
<section id="conditionCollection"
class="c-cs__conditions"
:class="{ 'is-expanded': expanded }"
>
<div class="c-cs__header c-section__header">
<span
class="c-disclosure-triangle c-tree__item__view-control is-enabled"
:class="{ 'c-disclosure-triangle--expanded': expanded }"
@click="expanded = !expanded"
></span>
<div class="c-cs__header-label c-section__label">Conditions</div>
</div>
<div v-if="expanded"
class="c-cs__content"
>
<div v-show="isEditing"
class="hint"
:class="{ 's-status-icon-warning-lo': !telemetryObjs.length }"
>
<template v-if="!telemetryObjs.length">Drag telemetry into this Condition Set to configure Conditions and add criteria.</template>
<template v-else>The first condition to match is the one that is applied. Drag conditions to reorder.</template>
</div>
<button
v-show="isEditing"
id="addCondition"
class="c-button c-button--major icon-plus labeled"
@click="addCondition"
>
<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 v-if="isEditing"
class="c-c__drag-ghost"
@drop.prevent="dropCondition"
@dragenter="dragEnter"
@dragleave="dragLeave"
@dragover.prevent
></div>
<Condition :condition="condition"
:condition-index="index"
:telemetry="telemetryObjs"
:is-editing="isEditing"
@updateCondition="updateCondition"
@removeCondition="removeCondition"
@cloneCondition="cloneCondition"
@setMoveIndex="setMoveIndex"
/>
</div>
</div>
</div>
</section>
</template>
<script>
import Condition from './Condition.vue';
import ConditionManager from '../ConditionManager';
export default {
inject: ['openmct', 'domainObject'],
components: {
Condition
},
props: {
isEditing: Boolean,
testData: {
type: Object,
required: true,
default: () => {
return {
applied: false,
conditionTestInputs: []
}
}
}
},
data() {
return {
expanded: true,
conditionCollection: [],
conditionResults: {},
conditions: [],
telemetryObjs: [],
moveIndex: Number,
isDragging: false,
defaultOutput: undefined
};
},
watch: {
defaultOutput(newOutput, oldOutput) {
this.$emit('updateDefaultOutput', newOutput);
},
testData: {
handler() {
this.updateTestData();
},
deep: true
}
},
destroyed() {
this.composition.off('add', this.addTelemetryObject);
this.composition.off('remove', this.removeTelemetryObject);
if(this.conditionManager) {
this.conditionManager.off('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
this.conditionManager.destroy();
}
if (this.stopObservingForChanges) {
this.stopObservingForChanges();
}
},
mounted() {
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addTelemetryObject);
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
this.conditionCollection = this.domainObject.configuration.conditionCollection;
this.observeForChanges();
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
this.conditionManager.on('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
this.updateDefaultCondition();
},
methods: {
handleConditionSetResultUpdated(data) {
this.$emit('conditionSetResultUpdated', data)
},
observeForChanges() {
this.stopObservingForChanges = this.openmct.objects.observe(this.domainObject, 'configuration.conditionCollection', (newConditionCollection) => {
this.conditionCollection = newConditionCollection;
this.updateDefaultCondition();
});
},
updateDefaultCondition() {
const defaultCondition = this.domainObject.configuration.conditionCollection
.find(conditionConfiguration => conditionConfiguration.isDefault);
this.defaultOutput = defaultCondition.configuration.output;
},
setMoveIndex(index) {
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
const oldIndexArr = Object.keys(this.conditionCollection);
const move = function (arr, old_index, new_index) {
while (old_index < 0) {
old_index += arr.length;
}
while (new_index < 0) {
new_index += arr.length;
}
if (new_index >= arr.length) {
var k = new_index - arr.length;
while ((k--) + 1) {
arr.push(undefined);
}
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
return arr;
}
const newIndexArr = move(oldIndexArr, this.moveIndex, targetIndex);
const reorderPlan = [];
for (let i = 0; i < oldIndexArr.length; i++) {
reorderPlan.push({oldIndex: Number(newIndexArr[i]), newIndex: i});
}
this.reorder(reorderPlan);
e.target.classList.remove("dragging");
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);
},
removeTelemetryObject(identifier) {
let index = _.findIndex(this.telemetryObjs, (obj) => {
let objId = this.openmct.objects.makeKeyString(obj.identifier);
let id = this.openmct.objects.makeKeyString(identifier);
return objId === id;
});
if (index > -1) {
this.telemetryObjs.splice(index, 1);
}
},
addCondition() {
this.conditionManager.addCondition();
},
updateCondition(data) {
this.conditionManager.updateCondition(data.condition, data.index);
},
removeCondition(index) {
this.conditionManager.removeCondition(index);
},
reorder(reorderPlan) {
this.conditionManager.reorderConditions(reorderPlan);
},
cloneCondition(data) {
this.conditionManager.cloneCondition(data.condition, data.index);
},
updateTestData() {
this.conditionManager.updateTestData(this.testData);
}
}
}
</script>

View File

@ -0,0 +1,145 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-style__condition-desc">
<span v-if="showLabel && condition"
class="c-style__condition-desc__name c-condition__name"
>
{{ condition.configuration.name }}
</span>
<span v-for="(criterionDescription, index) in criterionDescriptions"
:key="criterionDescription"
class="c-style__condition-desc__text"
>
<template v-if="!index">When</template>
{{ criterionDescription }}
<template v-if="index < (criterionDescriptions.length-1)">{{ triggerDescription }}</template>
</span>
</div>
</template>
<script>
import { TRIGGER } from "@/plugins/condition/utils/constants";
import { OPERATIONS } from "@/plugins/condition/utils/operations";
export default {
name: 'ConditionDescription',
inject: [
'openmct'
],
props: {
showLabel: {
type: Boolean,
default: false
},
condition: {
type: Object,
default() {
return undefined;
}
}
},
data() {
return {
criterionDescriptions: [],
triggerDescription: ''
}
},
watch: {
condition: {
handler(val) {
this.getConditionDescription();
},
deep: true
}
},
mounted() {
this.getConditionDescription();
},
methods: {
getTriggerDescription(trigger) {
let description = '';
switch(trigger) {
case TRIGGER.ANY:
case TRIGGER.XOR:
description = 'or';
break;
case TRIGGER.ALL:
case TRIGGER.NOT: description = 'and';
break;
}
return description;
},
getConditionDescription() {
if (this.condition) {
this.triggerDescription = this.getTriggerDescription(this.condition.configuration.trigger);
this.criterionDescriptions = [];
this.condition.configuration.criteria.forEach((criterion, index) => {
this.getCriterionDescription(criterion, index);
});
if (this.condition.isDefault) {
this.criterionDescriptions.splice(0, 0, 'all else fails');
}
} else {
this.criterionDescriptions = [];
}
},
getCriterionDescription(criterion, index) {
this.openmct.objects.get(criterion.telemetry).then((telemetryObject) => {
if (telemetryObject.type === 'unknown') {
let description = `Unknown ${criterion.metadata} ${this.getOperatorText(criterion.operation, criterion.input)}`;
this.criterionDescriptions.splice(index, 0, description);
} else {
let metadataValue = criterion.metadata;
let inputValue = criterion.input;
if (criterion.metadata) {
this.telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
const metadataObj = this.telemetryMetadata.valueMetadatas.find((metadata) => metadata.key === criterion.metadata);
if (metadataObj) {
if (metadataObj.name) {
metadataValue = metadataObj.name;
}
if(metadataObj.enumerations && inputValue.length) {
if (metadataObj.enumerations[inputValue[0]] && metadataObj.enumerations[inputValue[0]].string) {
inputValue = [metadataObj.enumerations[inputValue[0]].string];
}
}
}
}
let description = `${telemetryObject.name} ${metadataValue} ${this.getOperatorText(criterion.operation, inputValue)}`;
if (this.criterionDescriptions[index]) {
this.criterionDescriptions[index] = description;
} else {
this.criterionDescriptions.splice(index, 0, description);
}
}
});
},
getOperatorText(operationName, values) {
const found = OPERATIONS.find((operation) => operation.name === operationName);
return found ? found.getDescription(values) : '';
}
}
}
</script>

View File

@ -0,0 +1,80 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div v-if="conditionErrors.length"
class="c-condition__errors"
>
<div v-for="(error, index) in conditionErrors"
:key="index"
class="u-alert u-alert--block u-alert--with-icon"
>{{ error.message.errorText }} {{ error.additionalInfo }}
</div>
</div>
</template>
<script>
import { ERROR } from "@/plugins/condition/utils/constants";
export default {
name: 'ConditionError',
inject: [
'openmct'
],
props: {
condition: {
type: Object,
default() {
return undefined;
}
}
},
data() {
return {
conditionErrors: []
}
},
mounted() {
this.getConditionErrors();
},
methods: {
getConditionErrors() {
if (this.condition) {
this.condition.configuration.criteria.forEach((criterion, index) => {
this.getCriterionErrors(criterion, index);
});
}
},
getCriterionErrors(criterion, index) {
this.openmct.objects.get(criterion.telemetry).then((telemetryObject) => {
if (telemetryObject.type === 'unknown') {
this.conditionErrors.push({
message: ERROR.TELEMETRY_NOT_FOUND,
additionalInfo: criterion.telemetry ? `Key: ${this.openmct.objects.makeKeyString(criterion.telemetry)}` : ''
});
}
});
}
}
}
</script>

View File

@ -0,0 +1,97 @@
/*****************************************************************************
* 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-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">
<template v-if="currentConditionOutput">
{{ currentConditionOutput }}
</template>
<template v-else>
{{ defaultConditionOutput }}
</template>
</div>
</section>
<TestData :is-editing="isEditing"
:test-data="testData"
:telemetry="telemetryObjs"
@updateTestData="updateTestData"
/>
<ConditionCollection
:is-editing="isEditing"
:test-data="testData"
@conditionSetResultUpdated="updateCurrentOutput"
@updateDefaultOutput="updateDefaultOutput"
@telemetryUpdated="updateTelemetry"
/>
</div>
</template>
<script>
import TestData from './TestData.vue';
import ConditionCollection from './ConditionCollection.vue';
export default {
inject: ["openmct", "domainObject"],
components: {
TestData,
ConditionCollection
},
props: {
isEditing: Boolean
},
data() {
return {
currentConditionOutput: '',
defaultConditionOutput: '',
telemetryObjs: [],
testData: {}
}
},
mounted() {
this.conditionSetIdentifier = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.testData = {
applied: false,
conditionTestInputs: this.domainObject.configuration.conditionTestData || []
};
},
methods: {
updateCurrentOutput(currentConditionResult) {
this.currentConditionOutput = currentConditionResult.output;
},
updateDefaultOutput(output) {
this.currentConditionOutput = output;
},
updateTelemetry(telemetryObjs) {
this.telemetryObjs = telemetryObjs;
},
updateTestData(testData) {
this.testData = testData;
}
}
};
</script>

View File

@ -0,0 +1,295 @@
/*****************************************************************************
* 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="u-contents">
<div class="c-cdef__separator c-row-separator"></div>
<span class="c-cdef__label">{{ setRowLabel }}</span>
<span class="c-cdef__controls">
<span class="c-cdef__control">
<select ref="telemetrySelect"
v-model="criterion.telemetry"
@change="updateMetadataOptions"
>
<option value="">- Select Telemetry -</option>
<option value="all">all telemetry</option>
<option value="any">any telemetry</option>
<option v-for="telemetryOption in telemetry"
:key="telemetryOption.identifier.key"
:value="telemetryOption.identifier"
>
{{ telemetryOption.name }}
</option>
</select>
</span>
<span v-if="criterion.telemetry"
class="c-cdef__control"
>
<select ref="metadataSelect"
v-model="criterion.metadata"
@change="updateOperations"
>
<option value="">- Select Field -</option>
<option v-for="option in telemetryMetadataOptions"
:key="option.key"
:value="option.key"
>
{{ option.name }}
</option>
</select>
</span>
<span v-if="criterion.telemetry && criterion.metadata"
class="c-cdef__control"
>
<select v-model="criterion.operation"
@change="updateInputVisibilityAndValues"
>
<option value="">- Select Comparison -</option>
<option v-for="option in filteredOps"
:key="option.name"
:value="option.name"
>
{{ option.text }}
</option>
</select>
<template v-if="!enumerations.length">
<span v-for="(item, inputIndex) in inputCount"
:key="inputIndex"
class="c-cdef__control__inputs"
>
<input v-model="criterion.input[inputIndex]"
class="c-cdef__control__input"
:type="setInputType"
@blur="persist"
>
<span v-if="inputIndex < inputCount-1">and</span>
</span>
</template>
<span v-else>
<span v-if="inputCount && criterion.operation"
class="c-cdef__control"
>
<select v-model="criterion.input[0]"
@change="persist"
>
<option v-for="option in enumerations"
:key="option.string"
:value="option.value.toString()"
>
{{ option.string }}
</option>
</select>
</span>
</span>
</span>
</span>
</div>
</template>
<script>
import { OPERATIONS } from '../utils/operations';
import { INPUT_TYPES } from '../utils/operations';
export default {
inject: ['openmct'],
props: {
criterion: {
type: Object,
required: true
},
telemetry: {
type: Array,
required: true,
default: () => []
},
index: {
type: Number,
required: true
},
trigger: {
type: String,
required: true
}
},
data() {
return {
telemetryMetadataOptions: [],
operations: OPERATIONS,
inputCount: 0,
rowLabel: '',
operationFormat: '',
enumerations: [],
inputTypes: INPUT_TYPES
}
},
computed: {
setRowLabel: function () {
let operator = this.trigger === 'all' ? 'and ': 'or ';
return (this.index !== 0 ? operator : '') + 'when';
},
filteredOps: function () {
return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1);
},
setInputType: function () {
let type = '';
for (let i = 0; i < this.filteredOps.length; i++) {
if (this.criterion.operation === this.filteredOps[i].name) {
if (this.filteredOps[i].appliesTo.length) {
type = this.inputTypes[this.filteredOps[i].appliesTo[0]];
} else {
type = 'text'
}
break;
}
}
return type;
}
},
watch: {
telemetry: {
handler(newTelemetry, oldTelemetry) {
this.checkTelemetry();
},
deep: true
}
},
mounted() {
this.updateMetadataOptions();
},
methods: {
checkTelemetry() {
if(this.criterion.telemetry) {
if (this.criterion.telemetry === 'any' || this.criterion.telemetry === 'all') {
this.updateMetadataOptions();
} else {
if (!this.telemetry.find((telemetryObj) => this.openmct.objects.areIdsEqual(this.criterion.telemetry, telemetryObj.identifier))) {
//telemetry being used was removed. So reset this criterion.
this.criterion.telemetry = '';
this.criterion.metadata = '';
this.criterion.input = [];
this.criterion.operation = '';
this.persist();
}
}
}
},
updateOperationFormat() {
this.enumerations = [];
let foundMetadata = this.telemetryMetadataOptions.find((value) => {
return value.key === this.criterion.metadata;
});
if (foundMetadata) {
if (foundMetadata.enumerations !== undefined) {
this.operationFormat = 'enum';
this.enumerations = foundMetadata.enumerations;
} else if (foundMetadata.format === 'string' || foundMetadata.format === 'number') {
this.operationFormat = foundMetadata.format;
} else if (foundMetadata.hints.hasOwnProperty('range')) {
this.operationFormat = 'number';
} else if (foundMetadata.hints.hasOwnProperty('domain')) {
this.operationFormat = 'number';
} else if (foundMetadata.key === 'name') {
this.operationFormat = 'string';
} else {
this.operationFormat = 'number';
}
}
this.updateInputVisibilityAndValues();
},
updateMetadataOptions(ev) {
if (ev) {
this.clearDependentFields(ev.target);
this.persist();
}
if (this.criterion.telemetry) {
const telemetry = (this.criterion.telemetry === 'all' || this.criterion.telemetry === 'any') ? this.telemetry : [{
identifier: this.criterion.telemetry
}];
let telemetryPromises = telemetry.map((telemetryObject) => this.openmct.objects.get(telemetryObject.identifier));
Promise.all(telemetryPromises).then(telemetryObjects => {
this.telemetryMetadataOptions = [];
telemetryObjects.forEach(telemetryObject => {
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.addMetaDataOptions(telemetryMetadata.values());
});
this.updateOperations();
});
}
},
addMetaDataOptions(options) {
if (!this.telemetryMetadataOptions) {
this.telemetryMetadataOptions = options;
}
options.forEach((option) => {
const found = this.telemetryMetadataOptions.find((metadataOption) => {
return (metadataOption.key && (metadataOption.key === option.key)) && (metadataOption.name && (metadataOption.name === option.name))
});
if (!found) {
this.telemetryMetadataOptions.push(option);
}
});
},
updateOperations(ev) {
this.updateOperationFormat();
if (ev) {
this.clearDependentFields(ev.target);
this.persist();
}
},
updateInputVisibilityAndValues(ev) {
if (ev) {
this.clearDependentFields();
this.persist();
}
for (let i = 0; i < this.filteredOps.length; i++) {
if (this.criterion.operation === this.filteredOps[i].name) {
this.inputCount = this.filteredOps[i].inputCount;
}
}
if (!this.inputCount) {
this.criterion.input = [];
}
},
clearDependentFields(el) {
if (el === this.$refs.telemetrySelect) {
this.criterion.metadata = '';
} else if (el === this.$refs.metadataSelect) {
if (!this.filteredOps.find(operation => operation.name === this.criterion.operation)) {
this.criterion.operation = '';
this.criterion.input = this.enumerations.length ? [this.enumerations[0].value.toString()] : [];
this.inputCount = 0;
}
} else {
if (this.enumerations.length && !this.criterion.input.length) {
this.criterion.input = [this.enumerations[0].value.toString()];
}
this.inputCount = 0;
}
},
persist() {
this.$emit('persist', this.criterion);
}
}
};
</script>

View File

@ -0,0 +1,237 @@
/*****************************************************************************
* 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>
<section v-show="isEditing"
id="test-data"
class="c-cs__test-data"
:class="{ 'is-expanded': expanded }"
>
<div class="c-cs__header c-section__header">
<span
class="c-disclosure-triangle c-tree__item__view-control is-enabled"
:class="{ 'c-disclosure-triangle--expanded': expanded }"
@click="expanded = !expanded"
></span>
<div class="c-cs__header-label c-section__label">Test Data</div>
</div>
<div v-if="expanded"
class="c-cs__content"
>
<div class="c-cdef__controls"
:disabled="!telemetry.length"
>
<label class="c-toggle-switch">
<input
type="checkbox"
:checked="isApplied"
@change="applyTestData"
>
<span class="c-toggle-switch__slider"></span>
<span class="c-toggle-switch__label">Apply Test Data</span>
</label>
</div>
<div class="c-cs-tests">
<span v-for="(testInput, tIndex) in testInputs"
:key="tIndex"
class="c-test-datum c-cs-test"
>
<span class="c-cs-test__label">Set</span>
<span class="c-cs-test__controls">
<span class="c-cdef__control">
<select v-model="testInput.telemetry"
@change="updateMetadata(testInput)"
>
<option value="">- Select Telemetry -</option>
<option v-for="(telemetryOption, index) in telemetry"
:key="index"
:value="telemetryOption.identifier"
>
{{ telemetryOption.name }}
</option>
</select>
</span>
<span v-if="testInput.telemetry"
class="c-cdef__control"
>
<select v-model="testInput.metadata"
@change="updateTestData"
>
<option value="">- Select Field -</option>
<option v-for="(option, index) in telemetryMetadataOptions[getId(testInput.telemetry)]"
:key="index"
:value="option.key"
>
{{ option.name }}
</option>
</select>
</span>
<span v-if="testInput.metadata"
class="c-cdef__control__inputs"
>
<input v-model="testInput.value"
placeholder="Enter test input"
type="text"
class="c-cdef__control__input"
@change="updateTestData"
>
</span>
</span>
<div class="c-test-datum__buttons">
<button class="c-click-icon c-test-data__duplicate-button icon-duplicate"
title="Duplicate this test datum"
@click="addTestInput(testInput)"
></button>
<button class="c-click-icon c-test-data__delete-button icon-trash"
title="Delete this test datum"
@click="removeTestInput(tIndex)"
></button>
</div>
</span>
</div>
<button
v-show="isEditing"
id="addTestDatum"
class="c-button c-button--major icon-plus labeled"
@click="addTestInput"
>
<span class="c-cs-button__label">Add Test Datum</span>
</button>
</div>
</section>
</template>
<script>
export default {
inject: ['openmct'],
props: {
isEditing: Boolean,
telemetry: {
type: Array,
required: true,
default: () => []
},
testData: {
type: Object,
required: true,
default: () => {
return {
applied: false,
conditionTestInputs: []
}
}
}
},
data() {
return {
expanded: true,
isApplied: false,
testInputs: [],
telemetryMetadataOptions: {}
};
},
watch: {
isEditing(editing) {
if (!editing) {
this.resetApplied();
}
},
telemetry: {
handler() {
this.initializeMetadata();
},
deep: true
},
testData: {
handler() {
this.initialize();
},
deep: true
}
},
beforeDestroy() {
this.resetApplied();
},
mounted() {
this.initialize();
this.initializeMetadata();
},
methods: {
applyTestData() {
this.isApplied = !this.isApplied;
this.updateTestData();
},
initialize() {
if (this.testData && this.testData.conditionTestInputs) {
this.testInputs = this.testData.conditionTestInputs;
}
if (!this.testInputs.length) {
this.addTestInput();
}
},
initializeMetadata() {
this.telemetry.forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
});
},
addTestInput(testInput) {
this.testInputs.push(Object.assign({
telemetry: '',
metadata: '',
input: ''
}, testInput));
},
removeTestInput(index) {
this.testInputs.splice(index, 1);
this.updateTestData();
},
getId(identifier) {
if (identifier) {
return this.openmct.objects.makeKeyString(identifier);
}
return [];
},
updateMetadata(testInput) {
if (testInput.telemetry) {
const id = this.openmct.objects.makeKeyString(testInput.telemetry);
if(this.telemetryMetadataOptions[id]) {
return;
}
let telemetryMetadata = this.openmct.telemetry.getMetadata(testInput);
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
}
},
resetApplied() {
this.isApplied = false;
this.updateTestData();
},
updateTestData() {
this.$emit('updateTestData', {
applied: this.isApplied,
conditionTestInputs: this.testInputs
});
}
}
}
</script>

View File

@ -0,0 +1,116 @@
/*****************************************************************************
* 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

@ -0,0 +1,133 @@
/*****************************************************************************
* 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,185 @@
/*****************************************************************************
* 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>
<li class="c-tree__item-h">
<div
class="c-tree__item"
:class="{ 'is-alias': isAlias, 'is-navigated-object': navigated }"
@click="handleItemSelected(node.object, node)"
>
<view-control
v-model="expanded"
class="c-tree__item__view-control"
:enabled="hasChildren"
:propagate="false"
/>
<div class="c-tree__item__label c-object-label">
<div
class="c-tree__item__type-icon c-object-label__type-icon"
:class="typeClass"
></div>
<div class="c-tree__item__name c-object-label__name">{{ node.object.name }}</div>
</div>
</div>
<ul
v-if="expanded"
class="c-tree"
>
<li
v-if="isLoading && !loaded"
class="c-tree__item-h"
>
<div class="c-tree__item loading">
<span class="c-tree__item__label">Loading...</span>
</div>
</li>
<condition-set-dialog-tree-item
v-for="child in children"
:key="child.id"
:node="child"
:selected-item="selectedItem"
:handle-item-selected="handleItemSelected"
/>
</ul>
</li>
</template>
<script>
import viewControl from '@/ui/components/viewControl.vue';
export default {
name: 'ConditionSetDialogTreeItem',
inject: ['openmct'],
components: {
viewControl
},
props: {
node: {
type: Object,
required: true
},
selectedItem: {
type: Object,
default() {
return undefined;
}
},
handleItemSelected: {
type: Function,
default() {
return (item) => {};
}
}
},
data() {
return {
hasChildren: false,
isLoading: false,
loaded: false,
children: [],
expanded: false
}
},
computed: {
navigated() {
const itemId = this.selectedItem && this.selectedItem.itemId;
const isSelectedObject = itemId && this.openmct.objects.areIdsEqual(this.node.object.identifier, itemId);
if (isSelectedObject && this.node.objectPath && this.node.objectPath.length > 1) {
const isParent = this.openmct.objects.areIdsEqual(this.node.objectPath[1].identifier, this.selectedItem.parentId);
return isSelectedObject && isParent;
}
return isSelectedObject;
},
isAlias() {
let parent = this.node.objectPath[1];
if (!parent) {
return false;
}
let parentKeyString = this.openmct.objects.makeKeyString(parent.identifier);
return parentKeyString !== this.node.object.location;
},
typeClass() {
let type = this.openmct.types.get(this.node.object.type);
if (!type) {
return 'icon-object-unknown';
}
return type.definition.cssClass;
}
},
watch: {
expanded() {
if (!this.hasChildren) {
return;
}
if (!this.loaded && !this.isLoading) {
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addChild);
this.composition.on('remove', this.removeChild);
this.composition.load().then(this.finishLoading);
this.isLoading = true;
}
}
},
mounted() {
this.domainObject = this.node.object;
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
this.domainObject = newObject;
});
this.$once('hook:destroyed', removeListener);
if (this.openmct.composition.get(this.node.object)) {
this.hasChildren = true;
}
},
beforeDestroy() {
this.expanded = false;
},
destroyed() {
if (this.composition) {
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
delete this.composition;
}
},
methods: {
addChild(child) {
this.children.push({
id: this.openmct.objects.makeKeyString(child.identifier),
object: child,
objectPath: [child].concat(this.node.objectPath),
navigateToParent: this.navigateToPath
});
},
removeChild(identifier) {
let removeId = this.openmct.objects.makeKeyString(identifier);
this.children = this.children
.filter(c => c.id !== removeId);
},
finishLoading() {
this.isLoading = false;
this.loaded = true;
}
}
}
</script>

View File

@ -0,0 +1,172 @@
/*****************************************************************************
* 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="u-contents">
<div class="c-overlay__top-bar">
<div class="c-overlay__dialog-title">Select Condition Set</div>
</div>
<div class="c-overlay__contents-main c-selector c-tree-and-search">
<div class="c-tree-and-search__search">
<search ref="shell-search"
class="c-search"
:value="searchValue"
@input="searchTree"
@clear="searchTree"
/>
</div>
<!-- loading -->
<div v-if="isLoading"
class="c-tree-and-search__loading loading"
></div>
<!-- end loading -->
<div v-if="(allTreeItems.length === 0) || (searchValue && filteredTreeItems.length === 0)"
class="c-tree-and-search__no-results"
>
No results found
</div>
<!-- main tree -->
<ul v-if="!isLoading"
v-show="!searchValue"
class="c-tree-and-search__tree c-tree"
>
<condition-set-dialog-tree-item
v-for="treeItem in allTreeItems"
:key="treeItem.id"
:node="treeItem"
:selected-item="selectedItem"
:handle-item-selected="handleItemSelection"
/>
</ul>
<!-- end main tree -->
<!-- search tree -->
<ul v-if="searchValue"
class="c-tree-and-search__tree c-tree"
>
<condition-set-dialog-tree-item
v-for="treeItem in filteredTreeItems"
:key="treeItem.id"
:node="treeItem"
:selected-item="selectedItem"
:handle-item-selected="handleItemSelection"
/>
</ul>
<!-- end search tree -->
</div>
</div>
</template>
<script>
import search from '@/ui/components/search.vue';
import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue';
export default {
inject: ['openmct'],
name: 'ConditionSetSelectorDialog',
components: {
search,
ConditionSetDialogTreeItem
},
data() {
return {
expanded: false,
searchValue: '',
allTreeItems: [],
filteredTreeItems: [],
isLoading: false,
selectedItem: undefined
}
},
mounted() {
this.searchService = this.openmct.$injector.get('searchService');
this.getAllChildren();
},
methods: {
getAllChildren() {
this.isLoading = true;
this.openmct.objects.get('ROOT')
.then(root => {
return this.openmct.composition.get(root).load()
})
.then(children => {
this.isLoading = false;
this.allTreeItems = children.map(c => {
return {
id: this.openmct.objects.makeKeyString(c.identifier),
object: c,
objectPath: [c],
navigateToParent: '/browse'
};
});
});
},
getFilteredChildren() {
this.searchService.query(this.searchValue).then(children => {
this.filteredTreeItems = children.hits.map(child => {
let context = child.object.getCapability('context'),
object = child.object.useCapability('adapter'),
objectPath = [],
navigateToParent;
if (context) {
objectPath = context.getPath().slice(1)
.map(oldObject => oldObject.useCapability('adapter'))
.reverse();
navigateToParent = '/browse/' + objectPath.slice(1)
.map((parent) => this.openmct.objects.makeKeyString(parent.identifier))
.join('/');
}
return {
id: this.openmct.objects.makeKeyString(object.identifier),
object,
objectPath,
navigateToParent
}
});
});
},
searchTree(value) {
this.searchValue = value;
if (this.searchValue !== '') {
this.getFilteredChildren();
}
},
handleItemSelection(item, node) {
if (item && item.type === 'conditionSet') {
const parentId = (node.objectPath && node.objectPath.length > 1) ? node.objectPath[1].identifier : undefined;
this.selectedItem = {
itemId: item.identifier,
parentId
};
this.$emit('conditionSetSelected', item);
}
}
}
}
</script>

View File

@ -0,0 +1,335 @@
/*****************************************************************************
* 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">
<template v-if="!conditionSetDomainObject">
<div class="c-inspect-styles__header">
Object Style
</div>
<div class="c-inspect-styles__content">
<div v-if="staticStyle"
class="c-inspect-styles__style"
>
<style-editor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
@persist="updateStaticStyle"
/>
</div>
<button
id="addConditionSet"
class="c-button c-button--major c-toggle-styling-button labeled"
@click="addConditionSet"
>
<span class="c-cs-button__label">Use Conditional Styling...</span>
</button>
</div>
</template>
<template v-else>
<div class="c-inspect-styles__header">
Conditional Object Styles
</div>
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject"
class="c-object-label icon-conditional"
:href="navigateToPath"
@click="navigateOrPreview"
>
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
</a>
<template v-if="isEditing">
<button
id="changeConditionSet"
class="c-button labeled"
@click="addConditionSet"
>
<span class="c-button__label">Change...</span>
</button>
<button class="c-click-icon icon-x"
title="Remove conditional styles"
@click="removeConditionSet"
></button>
</template>
</div>
<div v-if="conditionsLoaded"
class="c-inspect-styles__conditions"
>
<div v-for="(conditionStyle, index) in conditionalStyles"
:key="index"
class="c-inspect-styles__condition"
>
<condition-error :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
/>
<condition-description :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
/>
<style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:is-editing="isEditing"
@persist="updateConditionalStyle"
/>
</div>
</div>
</template>
</div>
</template>
<script>
import StyleEditor from "./StyleEditor.vue";
import ConditionSetSelectorDialog from "./ConditionSetSelectorDialog.vue";
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import Vue from 'vue';
import PreviewAction from "@/ui/preview/PreviewAction.js";
export default {
name: 'ConditionalStylesView',
components: {
ConditionDescription,
ConditionError,
StyleEditor
},
inject: [
'openmct',
'domainObject'
],
props: {
itemId: {
type: String,
default: ''
},
initialStyles: {
type: Object,
default() {
return undefined;
}
},
canHide: {
type: Boolean,
default: false
}
},
data() {
return {
conditionalStyles: [],
staticStyle: undefined,
conditionSetDomainObject: undefined,
isEditing: this.openmct.editor.isEditing(),
conditions: undefined,
conditionsLoaded: false,
navigateToPath: ''
}
},
destroyed() {
this.openmct.editor.off('isEditing', this.setEditState);
},
mounted() {
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;
this.initializeStaticStyle(objectStyles);
if (objectStyles && objectStyles.conditionSetIdentifier) {
this.openmct.objects.get(objectStyles.conditionSetIdentifier).then(this.initialize);
this.conditionalStyles = objectStyles.styles;
}
} else {
this.initializeStaticStyle();
}
this.openmct.editor.on('isEditing', this.setEditState);
},
methods: {
initialize(conditionSetDomainObject) {
//If there are new conditions in the conditionSet we need to set those styles to default
this.conditionSetDomainObject = conditionSetDomainObject;
this.enableConditionSetNav();
this.initializeConditionalStyles();
},
setEditState(isEditing) {
this.isEditing = isEditing;
},
addConditionSet() {
let conditionSetDomainObject;
const handleItemSelection = (item) => {
if (item) {
conditionSetDomainObject = item;
}
};
const dismissDialog = (overlay, initialize) => {
overlay.dismiss();
if (initialize && conditionSetDomainObject) {
this.conditionSetDomainObject = conditionSetDomainObject;
this.conditionalStyles = [];
this.initializeConditionalStyles();
}
};
let vm = new Vue({
provide: {
openmct: this.openmct
},
components: {ConditionSetSelectorDialog},
data() {
return {
handleItemSelection
}
},
template: '<condition-set-selector-dialog @conditionSetSelected="handleItemSelection"></condition-set-selector-dialog>'
}).$mount();
let overlay = this.openmct.overlays.overlay({
element: vm.$el,
size: 'small',
buttons: [
{
label: 'OK',
emphasis: 'true',
callback: () => dismissDialog(overlay, true)
},
{
label: 'Cancel',
callback: () => dismissDialog(overlay, false)
}
],
onDestroy: () => vm.$destroy()
});
},
enableConditionSetNav() {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => {
this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.objectPath
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
}
);
},
navigateOrPreview(event) {
// If editing, display condition set in Preview overlay; otherwise nav to it while browsing
if (this.openmct.editor.isEditing()) {
event.preventDefault();
this.previewAction.invoke(this.objectPath);
}
},
removeConditionSet() {
this.conditionSetDomainObject = undefined;
this.conditionalStyles = [];
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (this.itemId) {
domainObjectStyles[this.itemId].conditionSetIdentifier = undefined;
delete domainObjectStyles[this.itemId].conditionSetIdentifier;
domainObjectStyles[this.itemId].styles = undefined;
delete domainObjectStyles[this.itemId].styles;
if (_.isEmpty(domainObjectStyles[this.itemId])) {
delete domainObjectStyles[this.itemId];
}
} else {
domainObjectStyles.conditionSetIdentifier = undefined;
delete domainObjectStyles.conditionSetIdentifier;
domainObjectStyles.styles = undefined;
delete domainObjectStyles.styles;
}
if (_.isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
this.persist(domainObjectStyles);
},
initializeConditionalStyles() {
if (!this.conditions) {
this.conditions = {};
}
let conditionalStyles = [];
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
this.conditions[conditionConfiguration.id] = conditionConfiguration;
let foundStyle = this.findStyleByConditionId(conditionConfiguration.id);
if (foundStyle) {
foundStyle.style = Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, foundStyle.style);
conditionalStyles.push(foundStyle);
} else {
conditionalStyles.splice(index, 0, {
conditionId: conditionConfiguration.id,
style: Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles)
});
}
});
//we're doing this so that we remove styles for any conditions that have been removed from the condition set
this.conditionalStyles = conditionalStyles;
this.conditionsLoaded = true;
this.persist(this.getDomainObjectConditionalStyle());
},
initializeStaticStyle(objectStyles) {
let staticStyle = objectStyles && objectStyles.staticStyle;
this.staticStyle = staticStyle || {
style: Object.assign({}, this.initialStyles)
};
},
findStyleByConditionId(id) {
return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id);
},
updateStaticStyle(staticStyle) {
this.staticStyle = staticStyle;
this.persist(this.getDomainObjectConditionalStyle());
},
updateConditionalStyle(conditionStyle) {
let found = this.findStyleByConditionId(conditionStyle.conditionId);
if (found) {
found.style = conditionStyle.style;
this.persist(this.getDomainObjectConditionalStyle());
}
},
getDomainObjectConditionalStyle() {
let objectStyle = {
styles: this.conditionalStyles,
staticStyle: this.staticStyle
};
if (this.conditionSetDomainObject) {
objectStyle.conditionSetIdentifier = this.conditionSetDomainObject.identifier;
}
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (this.itemId) {
domainObjectStyles[this.itemId] = objectStyle;
} else {
//we're deconstructing here to ensure that if an item within a domainObject already had a style we don't lose it
domainObjectStyles = {
...domainObjectStyles,
...objectStyle
}
}
return domainObjectStyles;
},
getCondition(id) {
return this.conditions ? this.conditions[id] : {};
},
persist(style) {
this.openmct.objects.mutate(this.domainObject, 'configuration.objectStyles', style);
}
}
}
</script>

View File

@ -0,0 +1,179 @@
/*****************************************************************************
* 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-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="c-style-thumb__text"
:class="{ 'hide-nice': !styleItem.style.color }"
>
ABC
</span>
</span>
<span class="c-toolbar">
<toolbar-color-picker v-if="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"
class="c-style__toolbar-button--background-color u-menu-to--center"
:options="backgroundColorOption"
@change="updateStyleValue"
/>
<toolbar-color-picker v-if="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"
class="c-style__toolbar-button--image-url"
:options="imageUrlOption"
@change="updateStyleValue"
/>
<toolbar-toggle-button v-if="styleItem.style.isStyleInvisible !== undefined"
class="c-style__toolbar-button--toggle-visible"
:options="isStyleInvisibleOption"
@change="updateStyleValue"
/>
</span>
</div>
</template>
<script>
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";
export default {
name: 'StyleEditor',
components: {
ToolbarButton,
ToolbarColorPicker,
ToolbarToggleButton
},
inject: [
'openmct'
],
props: {
isEditing: {
type: Boolean
},
styleItem: {
type: Object,
required: true
}
},
computed: {
borderColorOption() {
return {
icon: 'icon-line-horz',
title: STYLE_CONSTANTS.borderColorTitle,
value: this.styleItem.style.border.replace('1px solid ', ''),
property: 'border',
isEditing: this.isEditing
}
},
backgroundColorOption() {
return {
icon: 'icon-paint-bucket',
title: STYLE_CONSTANTS.backgroundColorTitle,
value: this.styleItem.style.backgroundColor,
property: 'backgroundColor',
isEditing: this.isEditing
}
},
colorOption() {
return {
icon: 'icon-font',
title: STYLE_CONSTANTS.textColorTitle,
value: this.styleItem.style.color,
property: 'color',
isEditing: this.isEditing
}
},
imageUrlOption() {
return {
icon: 'icon-image',
title: STYLE_CONSTANTS.imagePropertiesTitle,
dialog: {
name: "Image Properties",
sections: [
{
rows: [
{
key: "url",
control: "textfield",
name: "Image URL",
"cssClass": "l-input-lg"
}
]
}
]
},
property: 'imageUrl',
formKeys: ['url'],
value: {url: this.styleItem.style.imageUrl},
isEditing: this.isEditing
}
},
isStyleInvisibleOption() {
return {
value: this.styleItem.style.isStyleInvisible,
property: 'isStyleInvisible',
isEditing: this.isEditing,
options: [
{
value: '',
icon: 'icon-eye-disabled',
title: STYLE_CONSTANTS.visibilityHidden
},
{
value: STYLE_CONSTANTS.isStyleInvisible,
icon: 'icon-eye-open',
title: STYLE_CONSTANTS.visibilityVisible
}
]
}
}
},
methods: {
updateStyleValue(value, item) {
if (item.property === 'border') {
value = '1px solid ' + value;
}
if (value && (value.url !== undefined)) {
this.styleItem.style[item.property] = value.url;
} else {
this.styleItem.style[item.property] = value;
}
this.$emit('persist', this.styleItem);
}
}
}
</script>

View File

@ -0,0 +1,124 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/********************************************* INSPECTOR STYLES TAB */
.c-inspect-styles {
> * + * {
margin-top: $interiorMargin;
}
&__content,
&__conditions,
&__condition {
> * + * {
margin-top: $interiorMargin;
}
}
&__content {
display: flex;
flex-direction: column;
}
&__condition-set {
display: flex;
flex-direction: row;
align-items: center;
.c-object-label {
flex: 1 1 auto;
}
.c-button {
flex: 0 0 auto;
}
}
&__style,
&__condition {
padding: $interiorMargin;
}
&__condition {
@include discreteItem();
}
.c-style {
padding: 2px; // Allow a bit of room for thumb box-shadow
&__condition-desc {
@include ellipsize();
}
}
}
.c-inspect-styles__style {
.is-editing & {
border-bottom: 1px solid $colorInteriorBorder;
}
}
.l-shell:not(.is-editing) .c-inspect-styles {
.c-toolbar {
// Disabled-look toolbar when not editing
pointer-events: none;
cursor: inherit;
// Hide control buttons, like image URL
[class*='--image-url'] {
display: none;
}
// Make buttons look disabled by knocking back icon, not swatch element
.c-icon-button {
&:before {
opacity: $controlDisabledOpacity;
}
}
}
}
.c-toggle-styling-button {
display: none;
.is-editing & {
display: block;
align-self: flex-end;
}
}
.is-style-invisible {
display: none !important;
.is-editing & {
display: block !important;
opacity: 0.2;
}
&.c-style-thumb {
display: block !important;
background-color: transparent !important;
border-color: transparent !important;
@include bgCheckerboard($size: 10px, $imp: true);
opacity: 1;
}
}

View File

@ -0,0 +1,172 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import {OPERATIONS} from '../utils/operations';
import {computeCondition} from "@/plugins/condition/utils/evaluator";
export default class TelemetryCriterion extends EventEmitter {
/**
* Subscribes/Unsubscribes to telemetry and emits the result
* of operations performed on the telemetry data returned and a given input value.
* @constructor
* @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }
* @param openmct
*/
constructor(telemetryDomainObjectDefinition, openmct) {
super();
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;
this.telemetryDataCache = {};
}
updateTelemetry(telemetryObjects) {
this.telemetryObjects = Object.assign({}, telemetryObjects);
}
formatData(data, telemetryObjects) {
if (data) {
this.telemetryDataCache[data.id] = this.computeResult(data);
}
let keys = Object.keys(telemetryObjects);
keys.forEach((key) => {
let telemetryObject = telemetryObjects[key];
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (this.telemetryDataCache[id] === undefined) {
this.telemetryDataCache[id] = false;
}
});
const datum = {
result: computeCondition(this.telemetryDataCache, this.telemetry === 'all')
};
if (data) {
// TODO check back to see if we should format times here
this.timeAPI.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));
}
}
findOperation(operation) {
for (let i=0; i < OPERATIONS.length; i++) {
if (operation === OPERATIONS[i].name) {
return OPERATIONS[i].operation;
}
}
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;
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
if (!this.isValid()) {
return this.formatData({}, options.telemetryObjects);
}
const telemetryRequests = options.telemetryObjects
.map(telemetryObject => this.telemetryAPI.request(
telemetryObject,
options
));
return Promise.all(telemetryRequests)
.then(telemetryRequestsResults => {
telemetryRequestsResults.forEach((results, index) => {
const latestDatum = results.length ? results[results.length - 1] : {};
if (index === telemetryRequestsResults.length-1) {
//when the last result is computed, we return the result
return {
id: this.id,
data: this.formatData(latestDatum, options.telemetryObjects)
};
} else {
if (latestDatum) {
this.telemetryDataCache[latestDatum.id] = this.computeResult(latestDatum);
}
}
});
});
}
destroy() {
this.emitEvent('criterionRemoved');
delete this.telemetryObjects;
delete this.telemetryDataCache;
delete this.telemetryObjectIdAsString;
delete this.telemetryObject;
}
}

View File

@ -0,0 +1,149 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import {OPERATIONS} from '../utils/operations';
export default class TelemetryCriterion extends EventEmitter {
/**
* Subscribes/Unsubscribes to telemetry and emits the result
* of operations performed on the telemetry data returned and a given input value.
* @constructor
* @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }
* @param openmct
*/
constructor(telemetryDomainObjectDefinition, openmct) {
super();
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.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.emitEvent('criterionUpdated', this);
}
updateTelemetry(telemetryObjects) {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
}
formatData(data) {
const datum = {
result: this.computeResult(data)
};
if (data) {
// TODO check back to see if we should format times here
this.timeAPI.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({}));
}
}
findOperation(operation) {
for (let i=0, ii=OPERATIONS.length; i < ii; i++) {
if (operation === OPERATIONS[i].name) {
return OPERATIONS[i].operation;
}
}
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.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;
}
}

View File

@ -0,0 +1,111 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TelemetryCriterion from "./TelemetryCriterion";
let openmct = {},
mockListener,
testCriterionDefinition,
testTelemetryObject,
telemetryCriterion;
describe("The telemetry criterion", function () {
beforeEach (() => {
testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"},
type: "test-object",
name: "Test Object",
telemetry: {
valueMetadatas: [{
key: "value",
name: "Value",
hints: {
range: 2
}
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
}, {
key: "testSource",
source: "value",
name: "Test",
format: "enum"
}]
}
};
openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']);
openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key);
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter"]);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) {
return value;
}
});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
openmct.time = jasmine.createSpyObj('timeAPI',
['timeSystem', 'bounds', 'getAllTimeSystems']
);
openmct.time.timeSystem.and.returnValue({key: 'system'});
openmct.time.bounds.and.returnValue({start: 0, end: 1});
openmct.time.getAllTimeSystems.and.returnValue([{key: 'system'}]);
testCriterionDefinition = {
id: 'test-criterion-id',
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
operation: 'lessThan',
metadata: 'sin',
telemetryObject: testTelemetryObject
};
mockListener = jasmine.createSpy('listener');
telemetryCriterion = new TelemetryCriterion(
testCriterionDefinition,
openmct
);
telemetryCriterion.on('criterionResultUpdated', mockListener);
});
it("initializes with a telemetry objectId as string", 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({
value: 'Hello',
utc: 'Hi'
});
expect(telemetryCriterion.emitEvent).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,68 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ConditionSetViewProvider from './ConditionSetViewProvider.js';
import ConditionSetCompositionPolicy from "./ConditionSetCompositionPolicy";
import ConditionSetMetadataProvider from './ConditionSetMetadataProvider';
import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider';
import ConditionSetViewPolicy from './ConditionSetViewPolicy';
import uuid from "uuid";
export default function ConditionPlugin() {
return function install(openmct) {
openmct.types.addType('conditionSet', {
name: 'Condition Set',
key: 'conditionSet',
description: 'Monitor and evaluate telemetry values in real-time with a wide variety of criteria. Use to control the styling of many objects in Open MCT.',
creatable: true,
cssClass: 'icon-conditional',
initialize: function (domainObject) {
domainObject.configuration = {
conditionTestData: [],
conditionCollection: [{
isDefault: true,
id: uuid(),
configuration: {
name: 'Default',
output: 'Default',
trigger: 'all',
criteria: []
},
summary: 'Default condition'
}]
};
domainObject.composition = [];
domainObject.telemetry = {};
}
});
openmct.legacyExtension('policies', {
category: 'view',
implementation: ConditionSetViewPolicy
});
openmct.composition.addPolicy(new ConditionSetCompositionPolicy(openmct).allow);
openmct.telemetry.addProvider(new ConditionSetMetadataProvider(openmct));
openmct.telemetry.addProvider(new ConditionSetTelemetryProvider(openmct));
openmct.objectViews.addProvider(new ConditionSetViewProvider(openmct));
}
}

View File

@ -0,0 +1,97 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct } from "testTools";
import ConditionPlugin from "./plugin";
let openmct = createOpenMct();
openmct.install(new ConditionPlugin());
let conditionSetDefinition;
let mockConditionSetDomainObject;
let element;
let child;
describe('the plugin', function () {
beforeAll((done) => {
conditionSetDefinition = openmct.types.get('conditionSet').definition;
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
element = document.createElement('div');
child = document.createElement('div');
element.appendChild(child);
mockConditionSetDomainObject = {
identifier: {
key: 'testConditionSetKey',
namespace: ''
},
type: 'conditionSet'
};
conditionSetDefinition.initialize(mockConditionSetDomainObject);
openmct.on('start', done);
openmct.start(appHolder);
});
let mockConditionSetObject = {
name: 'Condition Set',
key: 'conditionSet',
creatable: true
};
it('defines a conditionSet object type with the correct key', () => {
expect(conditionSetDefinition.key).toEqual(mockConditionSetObject.key);
});
describe('the conditionSet object', () => {
it('is creatable', () => {
expect(conditionSetDefinition.creatable).toEqual(mockConditionSetObject.creatable);
});
it('initializes with an empty composition list', () => {
expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();
expect(mockConditionSetDomainObject.composition.length).toEqual(0);
});
it('provides a view', () => {
const testViewObject = {
id:"test-object",
type: "conditionSet",
configuration: {
conditionCollection: []
}
};
const applicableViews = openmct.objectViews.get(testViewObject);
let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
expect(conditionSetView).toBeDefined();
});
});
});

View File

@ -0,0 +1,54 @@
/*****************************************************************************
* 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 TRIGGER = {
ANY: 'any',
ALL: 'all',
NOT: 'not',
XOR: 'xor'
};
export const TRIGGER_LABEL = {
'any': 'when any criteria are met',
'all': 'when all criteria are met',
'not': 'when no criteria are met',
'xor': 'when only one criteria is met'
};
export const STYLE_CONSTANTS = {
isStyleInvisible: 'is-style-invisible',
borderColorTitle: 'Set border color',
textColorTitle: 'Set text color',
backgroundColorTitle: 'Set background color',
imagePropertiesTitle: 'Edit image properties',
visibilityHidden: 'Hidden',
visibilityVisible: 'Visible'
};
export const ERROR = {
'TELEMETRY_NOT_FOUND': {
errorText: 'Telemetry not found for criterion'
},
'CONDITION_NOT_FOUND': {
errorText: 'Condition not found'
}
};

View File

@ -0,0 +1,54 @@
/*****************************************************************************
* 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 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;
}
}
}
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;
}
}
}
return trueCount === limit;
};

View File

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

@ -0,0 +1,276 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import _ from 'lodash';
export const OPERATIONS = [
{
name: 'equalTo',
operation: function (input) {
return Number(input[0]) === Number(input[1]);
},
text: 'is equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' is ' + values.join(', ');
}
},
{
name: 'notEqualTo',
operation: function (input) {
return Number(input[0]) !== Number(input[1]);
},
text: 'is not equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' is not ' + values.join(', ');
}
},
{
name: 'greaterThan',
operation: function (input) {
return Number(input[0]) > Number(input[1]);
},
text: 'is greater than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' > ' + values.join(', ');
}
},
{
name: 'lessThan',
operation: function (input) {
return Number(input[0]) < Number(input[1]);
},
text: 'is less than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' < ' + values.join(', ');
}
},
{
name: 'greaterThanOrEq',
operation: function (input) {
return Number(input[0]) >= Number(input[1]);
},
text: 'is greater than or equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' >= ' + values.join(', ');
}
},
{
name: 'lessThanOrEq',
operation: function (input) {
return Number(input[0]) <= Number(input[1]);
},
text: 'is less than or equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' <= ' + values.join(', ');
}
},
{
name: 'between',
operation: function (input) {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
let larger = Math.max(...numberInputs.slice(1,3));
let smaller = Math.min(...numberInputs.slice(1,3));
return (numberInputs[0] > smaller) && (numberInputs[0] < larger);
},
text: 'is between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' is between ' + values[0] + ' and ' + values[1];
}
},
{
name: 'notBetween',
operation: function (input) {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
let larger = Math.max(...numberInputs.slice(1,3));
let smaller = Math.min(...numberInputs.slice(1,3));
return (numberInputs[0] < smaller) || (numberInputs[0] > larger);
},
text: 'is not between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' is not between ' + values[0] + ' and ' + values[1];
}
},
{
name: 'textContains',
operation: function (input) {
return input[0] && input[1] && input[0].includes(input[1]);
},
text: 'text contains',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' contains ' + values.join(', ');
}
},
{
name: 'textDoesNotContain',
operation: function (input) {
return input[0] && input[1] && !input[0].includes(input[1]);
},
text: 'text does not contain',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' does not contain ' + values.join(', ');
}
},
{
name: 'textStartsWith',
operation: function (input) {
return input[0].startsWith(input[1]);
},
text: 'text starts with',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' starts with ' + values.join(', ');
}
},
{
name: 'textEndsWith',
operation: function (input) {
return input[0].endsWith(input[1]);
},
text: 'text ends with',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' ends with ' + values.join(', ');
}
},
{
name: 'textIsExactly',
operation: function (input) {
return input[0] === input[1];
},
text: 'text is exactly',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' is exactly ' + values.join(', ');
}
},
{
name: 'isUndefined',
operation: function (input) {
return typeof input[0] === 'undefined';
},
text: 'is undefined',
appliesTo: ['string', 'number', 'enum'],
inputCount: 0,
getDescription: function () {
return ' is undefined';
}
},
{
name: 'isDefined',
operation: function (input) {
return typeof input[0] !== 'undefined';
},
text: 'is defined',
appliesTo: ['string', 'number', 'enum'],
inputCount: 0,
getDescription: function () {
return ' is defined';
}
},
{
name: 'enumValueIs',
operation: function (input) {
return input[0] === input[1];
},
text: 'is',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' is ' + values.join(', ');
}
},
{
name: 'enumValueIsNot',
operation: function (input) {
return input[0] !== input[1];
},
text: 'is not',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' is not ' + values.join(', ');
}
},
{
name: 'valueIs',
operation: function (input) {
if (input[1]) {
const values = input[1].split(',');
return values.find((value) => input[0].toString() === _.trim(value.toString()));
}
return false;
},
text: 'is one of',
appliesTo: ["string", "number"],
inputCount: 1,
getDescription: function (values) {
return ' is one of ' + values[0];
}
},
{
name: 'valueIsNot',
operation: function (input) {
if (input[1]) {
const values = input[1].split(',');
const found = values.find((value) => input[0].toString() === _.trim(value.toString()));
return !found;
}
return false;
},
text: 'is not one of',
appliesTo: ["string", "number"],
inputCount: 1,
getDescription: function (values) {
return ' is not one of ' + values[0];
}
}
];
export const INPUT_TYPES = {
'string': 'text',
'number': 'number'
};

View File

@ -0,0 +1,90 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { OPERATIONS } from "./operations";
let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIs');
let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIsNot');
let isBetween = OPERATIONS.find((operation) => operation.name === 'between');
let isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween');
describe('Is one of and is not one of operations', function () {
it('should evaluate isOneOf to true for number inputs', () => {
const inputs = [45, "5,6,45,8"];
expect(!!isOneOfOperation.operation(inputs)).toBeTrue();
});
it('should evaluate isOneOf to true for string inputs', () => {
const inputs = ["45", " 45, 645, 4,8 "];
expect(!!isOneOfOperation.operation(inputs)).toBeTrue();
});
it('should evaluate isNotOneOf to true for number inputs', () => {
const inputs = [45, "5,6,4,8"];
expect(!!isNotOneOfOperation.operation(inputs)).toBeTrue();
});
it('should evaluate isNotOneOf to true for string inputs', () => {
const inputs = ["45", " 5,645, 4,8 "];
expect(!!isNotOneOfOperation.operation(inputs)).toBeTrue();
});
it('should evaluate isOneOf to false for number inputs', () => {
const inputs = [4, "5, 6, 7, 8"];
expect(!!isOneOfOperation.operation(inputs)).toBeFalse();
});
it('should evaluate isOneOf to false for string inputs', () => {
const inputs = ["4", "5,645 ,7,8"];
expect(!!isOneOfOperation.operation(inputs)).toBeFalse();
});
it('should evaluate isNotOneOf to false for number inputs', () => {
const inputs = [4, "5,4, 7,8"];
expect(!!isNotOneOfOperation.operation(inputs)).toBeFalse();
});
it('should evaluate isNotOneOf to false for string inputs', () => {
const inputs = ["4", "5,46,4,8"];
expect(!!isNotOneOfOperation.operation(inputs)).toBeFalse();
});
it('should evaluate isBetween to true', () => {
const inputs = ["4", "3", "89"];
expect(!!isBetween.operation(inputs)).toBeTrue();
});
it('should evaluate isNotBetween to true', () => {
const inputs = ["45", "100", "89"];
expect(!!isNotBetween.operation(inputs)).toBeTrue();
});
it('should evaluate isBetween to false', () => {
const inputs = ["4", "100", "89"];
expect(!!isBetween.operation(inputs)).toBeFalse();
});
it('should evaluate isNotBetween to false', () => {
const inputs = ["45", "30", "50"];
expect(!!isNotBetween.operation(inputs)).toBeFalse();
});
});

View File

@ -0,0 +1,45 @@
/*****************************************************************************
* 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 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;
}
return styleProp;
};

View File

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

View File

@ -0,0 +1,55 @@
/*****************************************************************************
* 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>
<a class="c-condition-widget"
:href="internalDomainObject.url"
>
<div class="c-condition-widget__label">
{{ internalDomainObject.label }}
</div>
</a>
</template>
<script>
export default {
inject: ['openmct', 'domainObject'],
data: function () {
return {
internalDomainObject: this.domainObject
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
}
}
}
</script>

View File

@ -0,0 +1,58 @@
/*****************************************************************************
* 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-widget {
$shdwSize: 3px;
@include userSelectNone();
background-color: rgba($colorBodyFg, 0.1); // Give a little presence if the user hasn't defined a fill color
border-radius: $basicCr;
border: 1px solid transparent;
display: inline-block;
padding: $interiorMarginLg $interiorMarginLg * 2;
cursor: inherit !important;
&[href] {
cursor: pointer !important;
}
}
// Make Condition Widget expand when in a hidden frame Layout context
// For both static and Flexible Layouts
.c-so-view--no-frame > .c-so-view__object-view > .c-condition-widget {
@include abs();
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
// Add some margin when a Condition Widget is in a Flexible Layout
.c-fl .c-so-view--no-frame .c-condition-widget {
@include abs(1px);
}
// When the widget is in the main view, center it in the space
.l-shell__main-container > .c-condition-widget {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@ -0,0 +1,58 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ConditionWidgetViewProvider from './ConditionWidgetViewProvider.js';
export default function plugin() {
return function install(openmct) {
openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct));
openmct.types.addType('conditionWidget', {
name: "Condition Widget",
description: "A button that can be used on its own, or dynamically styled with a Condition Set.",
creatable: true,
cssClass: 'icon-condition-widget',
initialize(domainObject) {
domainObject.label = 'Condition Widget';
},
form: [
{
"key": "label",
"name": "Label",
"control": "textfield",
property: [
"label"
],
"required": true,
"cssClass": "l-input"
},
{
"key": "url",
"name": "URL",
"control": "textfield",
"required": false,
"cssClass": "l-input-lg"
}
]
});
};
}

View File

@ -347,78 +347,6 @@ define(['lodash'], function (_) {
};
}
function getFillMenu(selectedParent, selection) {
return {
control: "color-picker",
domainObject: selectedParent,
applicableSelectedItems: selection.filter(selectionPath => {
let type = selectionPath[0].context.layoutItem.type;
return type === 'text-view' ||
type === 'telemetry-view' ||
type === 'box-view';
}),
property: function (selectionPath) {
return getPath(selectionPath) + ".fill";
},
icon: "icon-paint-bucket",
title: "Set fill color"
};
}
function getStrokeMenu(selectedParent, selection) {
return {
control: "color-picker",
domainObject: selectedParent,
applicableSelectedItems: selection.filter(selectionPath => {
let type = selectionPath[0].context.layoutItem.type;
return type === 'text-view' ||
type === 'telemetry-view' ||
type === 'box-view' ||
type === 'image-view' ||
type === 'line-view';
}),
property: function (selectionPath) {
return getPath(selectionPath) + ".stroke";
},
icon: "icon-line-horz",
title: "Set border color"
};
}
function getTextColorMenu(selectedParent, selection) {
return {
control: "color-picker",
domainObject: selectedParent,
applicableSelectedItems: selection.filter(selectionPath => {
let type = selectionPath[0].context.layoutItem.type;
return type === 'text-view' || type === 'telemetry-view';
}),
property: function (selectionPath) {
return getPath(selectionPath) + ".color";
},
icon: "icon-font",
mandatory: true,
title: "Set text color",
preventNone: true
};
}
function getURLButton(selectedParent, selection) {
return {
control: "button",
domainObject: selectedParent,
applicableSelectedItems: selection.filter(selectionPath => {
return selectionPath[0].context.layoutItem.type === 'image-view';
}),
property: function (selectionPath) {
return getPath(selectionPath);
},
icon: "icon-image",
title: "Edit image properties",
dialog: DIALOG_FORM.image
};
}
function getTextButton(selectedParent, selection) {
return {
control: "button",
@ -429,7 +357,7 @@ define(['lodash'], function (_) {
property: function (selectionPath) {
return getPath(selectionPath);
},
icon: "icon-gear",
icon: "icon-font",
title: "Edit text properties",
dialog: DIALOG_FORM.text
};
@ -505,14 +433,14 @@ define(['lodash'], function (_) {
let toolbar = {
'add-menu': [],
'text': [],
'url': [],
'toggle-frame': [],
'display-mode': [],
'telemetry-value': [],
'style': [],
'text-style': [],
'position': [],
'text': [],
'url': [],
'remove': []
};
@ -520,6 +448,10 @@ define(['lodash'], function (_) {
let selectedParent = selectionPath[1].context.item;
let layoutItem = selectionPath[0].context.layoutItem;
if (!layoutItem) {
return;
}
if (layoutItem.type === 'subobject-view') {
if (toolbar['add-menu'].length === 0 && selectionPath[0].context.item.type === 'layout') {
toolbar['add-menu'] = [getAddButton(selectedObjects, selectionPath)];
@ -546,15 +478,8 @@ define(['lodash'], function (_) {
if (toolbar['telemetry-value'].length === 0) {
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
}
if (toolbar.style.length < 2) {
toolbar.style = [
getFillMenu(selectedParent, selectedObjects),
getStrokeMenu(selectedParent, selectedObjects)
];
}
if (toolbar['text-style'].length === 0) {
toolbar['text-style'] = [
getTextColorMenu(selectedParent, selectedObjects),
getTextSizeMenu(selectedParent, selectedObjects)
];
}
@ -571,15 +496,8 @@ define(['lodash'], function (_) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
}
} else if (layoutItem.type === 'text-view') {
if (toolbar.style.length < 2) {
toolbar.style = [
getFillMenu(selectedParent, selectedObjects),
getStrokeMenu(selectedParent, selectedObjects)
];
}
if (toolbar['text-style'].length === 0) {
toolbar['text-style'] = [
getTextColorMenu(selectedParent, selectedObjects),
getTextSizeMenu(selectedParent, selectedObjects)
];
}
@ -599,12 +517,6 @@ define(['lodash'], function (_) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
}
} else if (layoutItem.type === 'box-view') {
if (toolbar.style.length < 2) {
toolbar.style = [
getFillMenu(selectedParent, selectedObjects),
getStrokeMenu(selectedParent, selectedObjects)
];
}
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
@ -618,11 +530,6 @@ define(['lodash'], function (_) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
}
} else if (layoutItem.type === 'image-view') {
if (toolbar.style.length === 0) {
toolbar.style = [
getStrokeMenu(selectedParent, selectedObjects)
];
}
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
@ -632,18 +539,10 @@ define(['lodash'], function (_) {
getWidthInput(selectedParent, selectedObjects)
];
}
if (toolbar.url.length === 0) {
toolbar.url = [getURLButton(selectedParent, selectedObjects)];
}
if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
}
} else if (layoutItem.type === 'line-view') {
if (toolbar.style.length === 0) {
toolbar.style = [
getStrokeMenu(selectedParent, selectedObjects)
];
}
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),

View File

@ -23,20 +23,20 @@
<template>
<div
v-if="isEditing"
class="c-properties"
class="c-inspect-properties"
>
<div class="c-properties__header">
<div class="c-inspect-properties__header">
Alphanumeric Format
</div>
<ul class="c-properties__section">
<li class="c-properties__row">
<ul class="c-inspect-properties__section">
<li class="c-inspect-properties__row">
<div
class="c-properties__label"
class="c-inspect-properties__label"
title="Printf formatting for the selected telemetry"
>
<label for="telemetryPrintfFormat">Format</label>
</div>
<div class="c-properties__value">
<div class="c-inspect-properties__value">
<input
id="telemetryPrintfFormat"
type="text"
@ -85,7 +85,13 @@ export default {
return;
}
let format = selection[0][0].context.layoutItem.format;
let layoutItem = selection[0][0].context.layoutItem;
if (!layoutItem) {
return;
}
let format = layoutItem.format;
this.nonMixedFormat = selection.every(selectionPath => {
return selectionPath[0].context.layoutItem.format === format;
});

View File

@ -29,6 +29,7 @@
>
<div
class="c-box-view"
:class="[styleClass]"
:style="style"
></div>
</layout-frame>
@ -36,6 +37,7 @@
<script>
import LayoutFrame from './LayoutFrame.vue'
import conditionalStylesMixin from '../mixins/objectStyles-mixin';
export default {
makeDefinition() {
@ -52,6 +54,7 @@ export default {
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
props: {
item: {
type: Object,
@ -71,10 +74,13 @@ export default {
},
computed: {
style() {
return {
return Object.assign({
backgroundColor: this.item.fill,
border: '1px solid ' + this.item.stroke
};
}, this.itemStyle);
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
}
},
watch: {

View File

@ -29,6 +29,7 @@
>
<div
class="c-image-view"
:class="[styleClass]"
:style="style"
></div>
</layout-frame>
@ -36,6 +37,7 @@
<script>
import LayoutFrame from './LayoutFrame.vue'
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
export default {
makeDefinition(openmct, gridSize, element) {
@ -52,6 +54,7 @@ export default {
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
props: {
item: {
type: Object,
@ -72,9 +75,12 @@ export default {
computed: {
style() {
return {
backgroundImage: 'url(' + this.item.url + ')',
border: '1px solid ' + this.item.stroke
}
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)
};
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
}
},
watch: {

View File

@ -23,6 +23,7 @@
<template>
<div
class="l-layout__frame c-frame no-frame"
:class="[styleClass]"
:style="style"
>
<svg
@ -31,7 +32,7 @@
>
<line
v-bind="linePosition"
:stroke="item.stroke"
:stroke="stroke"
stroke-width="2"
/>
</svg>
@ -60,6 +61,8 @@
<script>
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
const START_HANDLE_QUADRANTS = {
1: 'c-frame-edit__handle--sw',
2: 'c-frame-edit__handle--se',
@ -85,6 +88,7 @@ export default {
};
},
inject: ['openmct'],
mixins: [conditionalStylesMixin],
props: {
item: {
type: Object,
@ -122,6 +126,13 @@ export default {
}
return {x, y, x2, y2};
},
stroke() {
if (this.itemStyle && this.itemStyle.border) {
return this.itemStyle.border.replace('1px solid ', '');
} else {
return this.item.stroke;
}
},
style() {
let {x, y, x2, y2} = this.position;
let width = Math.max(this.gridSize[0] * Math.abs(x - x2), 1);
@ -135,6 +146,9 @@ export default {
height: `${height}px`
};
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
},
startHandleClass() {
return START_HANDLE_QUADRANTS[this.vectorQuadrant];
},

View File

@ -45,7 +45,7 @@ import LayoutFrame from './LayoutFrame.vue'
const MINIMUM_FRAME_SIZE = [320, 180],
DEFAULT_DIMENSIONS = [10, 10],
DEFAULT_POSITION = [1, 1],
DEFAULT_HIDDEN_FRAME_TYPES = ['hyperlink', 'summary-widget'];
DEFAULT_HIDDEN_FRAME_TYPES = ['hyperlink', 'summary-widget', 'conditionWidget'];
function getDefaultDimensions(gridSize) {
return MINIMUM_FRAME_SIZE.map((min, index) => {

View File

@ -36,6 +36,8 @@
<div
v-if="showLabel"
class="c-telemetry-view__label"
:class="[styleClass]"
:style="objectStyle"
>
<div class="c-telemetry-view__label-text">
{{ domainObject.name }}
@ -46,7 +48,8 @@
v-if="showValue"
:title="fieldName"
class="c-telemetry-view__value"
:class="[telemetryClass]"
:class="[telemetryClass, !telemetryClass && styleClass]"
:style="!telemetryClass && objectStyle"
>
<div class="c-telemetry-view__value-text">
{{ telemetryValue }}
@ -59,6 +62,7 @@
<script>
import LayoutFrame from './LayoutFrame.vue'
import printj from 'printj'
import StyleRuleManager from "../../condition/StyleRuleManager";
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5],
DEFAULT_POSITION = [1, 1],
@ -109,7 +113,8 @@ export default {
datum: undefined,
formats: undefined,
domainObject: undefined,
currentObjectPath: undefined
currentObjectPath: undefined,
objectStyle: ''
}
},
computed: {
@ -129,6 +134,9 @@ export default {
fontSize: this.item.size
}
},
styleClass() {
return this.objectStyle && this.objectStyle.isStyleInvisible;
},
fieldName() {
return this.valueMetadata && this.valueMetadata.name;
},
@ -182,6 +190,15 @@ 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: {
@ -224,6 +241,7 @@ 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);
@ -248,6 +266,30 @@ 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

@ -29,6 +29,7 @@
>
<div
class="c-text-view"
:class="[styleClass]"
:style="style"
>
{{ item.text }}
@ -38,6 +39,7 @@
<script>
import LayoutFrame from './LayoutFrame.vue'
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
export default {
makeDefinition(openmct, gridSize, element) {
@ -57,6 +59,7 @@ export default {
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
props: {
item: {
type: Object,
@ -76,12 +79,15 @@ export default {
},
computed: {
style() {
return {
return Object.assign({
backgroundColor: this.item.fill,
borderColor: this.item.stroke,
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

@ -0,0 +1,81 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
export default {
inject: ['openmct'],
data() {
return {
itemStyle: this.itemStyle
}
},
mounted() {
this.domainObject = this.$parent.domainObject;
this.itemId = this.item.id;
this.objectStyle = this.getObjectStyleForItem(this.domainObject.configuration.objectStyles);
this.initObjectStyles();
},
destroyed() {
if (this.stopListeningObjectStyles) {
this.stopListeningObjectStyles();
}
},
methods: {
getObjectStyleForItem(objectStyle) {
if (objectStyle) {
return objectStyle[this.itemId] ? Object.assign({}, objectStyle[this.itemId]) : undefined;
} else {
return undefined;
}
},
initObjectStyles() {
if (!this.styleRuleManager) {
this.styleRuleManager = new StyleRuleManager(this.objectStyle, this.openmct, this.updateStyle.bind(this));
} else {
this.styleRuleManager.updateObjectStyleConfig(this.objectStyle);
}
if (this.stopListeningObjectStyles) {
this.stopListeningObjectStyles();
}
this.stopListeningObjectStyles = 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
let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle);
if (this.objectStyle !== newItemObjectStyle) {
this.objectStyle = newItemObjectStyle;
this.styleRuleManager.updateObjectStyleConfig(this.objectStyle);
}
});
},
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];
}
});
}
}
};

View File

@ -1,17 +1,17 @@
<template>
<div class="c-properties__section c-filter-settings">
<div class="c-inspect-properties__section c-filter-settings">
<li
v-for="(filter, index) in filterField.filters"
:key="index"
class="c-properties__row c-filter-settings__setting"
class="c-inspect-properties__row c-filter-settings__setting"
>
<div
class="c-properties__label label"
class="c-inspect-properties__label label"
:disabled="useGlobal"
>
{{ filterField.name }} =
</div>
<div class="c-properties__value value">
<div class="c-inspect-properties__value value">
<!-- EDITING -->
<!-- String input, editing -->
<template v-if="!filter.possibleValues && isEditing">

View File

@ -26,17 +26,17 @@
</div>
<div v-if="expanded">
<ul class="c-properties">
<ul class="c-inspect-properties">
<div
v-if="!isEditing && persistedFilters.useGlobal"
class="c-properties__label span-all"
class="c-inspect-properties__label span-all"
>
Uses global filter
</div>
<div
v-if="isEditing"
class="c-properties__label span-all"
class="c-inspect-properties__label span-all"
>
<toggle-switch
:id="keyString"

View File

@ -23,7 +23,7 @@
</div>
<ul
v-if="expanded"
class="c-properties"
class="c-inspect-properties"
>
<filter-field
v-for="metadatum in globalMetadata"

View File

@ -7,7 +7,7 @@
}
.c-filter-tree {
// Filters UI uses a tree-based structure
.c-properties {
.c-inspect-properties {
// Add extra margin to account for filter-indicator
margin-left: 38px;
}

View File

@ -313,8 +313,4 @@
margin: 0;
}
}
.c-object-view {
display: contents;
}
}

View File

@ -26,6 +26,7 @@ export default function ImageryViewProvider(openmct) {
return {
show: function (element) {
component = new Vue({
el: element,
components: {
ImageryViewLayout
},
@ -33,7 +34,6 @@ export default function ImageryViewProvider(openmct) {
openmct,
domainObject
},
el: element,
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
});
},

View File

@ -1,90 +1,68 @@
<template>
<multipane class="c-imagery-layout"
type="vertical"
>
<pane :style="{'min-height': `300px`}">
<div class="main-image-wrapper c-imagery has-local-controls">
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc">
<span class="holder flex-elem grows c-imagery__lc__sliders">
<input v-model="filters.brightness"
class="icon-brightness"
type="range"
min="0"
max="500"
>
<input v-model="filters.contrast"
class="icon-contrast"
type="range"
min="0"
max="500"
>
</span>
<span class="holder flex-elem t-reset-btn-holder c-imagery__lc__reset-btn">
<a class="s-icon-button icon-reset t-btn-reset"
@click="filters={brightness: 100, contrast: 100}"
></a>
</span>
</div>
<div class="main-image s-image-main"
:class="{'paused unnsynced': paused(),'stale':false }"
:style="{'background-image': `url(${getImageUrl()})`,
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
>
</div>
<div class="l-image-controller flex-elem l-flex-row">
<div class="l-datetime-w flex-elem grows">
<a class="c-button show-thumbs sm hidden icon-thumbs-strip"></a>
<span class="l-time">{{ getTime() }}</span>
</div>
<div class="h-local-controls flex-elem">
<a class="c-button icon-pause pause-play"
:class="{'is-paused': paused()}"
@click="paused(!paused())"
></a>
</div>
</div>
</div>
</pane>
<pane class="c-inspector__elements"
handle="before"
:style="{'min-height': `100px`}"
>
<div class="c-elements-pool">
<div ref="thumbsWrapper"
class="thumbs-layout"
@scroll="handleScroll"
>
<div v-for="(imageData, index) in imageHistory"
:key="index"
class="l-image-thumb-item"
:class="{selected: imageData.selected}"
@click="setSelectedImage(imageData)"
<div class="c-imagery">
<div class="c-imagery__main-image-wrapper has-local-controls">
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc">
<span class="holder flex-elem grows c-imagery__lc__sliders">
<input v-model="filters.brightness"
class="icon-brightness"
type="range"
min="0"
max="500"
>
<img class="l-thumb"
:src="getImageUrl(imageData)"
>
<div class="l-time">{{ getTime(imageData) }}</div>
</div>
<input v-model="filters.contrast"
class="icon-contrast"
type="range"
min="0"
max="500"
>
</span>
<span class="holder flex-elem t-reset-btn-holder c-imagery__lc__reset-btn">
<a class="s-icon-button icon-reset t-btn-reset"
@click="filters={brightness: 100, contrast: 100}"
></a>
</span>
</div>
<div class="main-image s-image-main c-imagery__main-image"
:class="{'paused unnsynced': paused(),'stale':false }"
:style="{'background-image': `url(${getImageUrl()})`,
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
>
</div>
<div class="c-imagery__control-bar">
<div class="c-imagery__timestamp">{{ getTime() }}</div>
<div class="h-local-controls flex-elem">
<a class="c-button icon-pause pause-play"
:class="{'is-paused': paused()}"
@click="paused(!paused())"
></a>
</div>
</div>
</pane>
</multipane>
</div>
<div ref="thumbsWrapper"
class="c-imagery__thumbs-wrapper"
:class="{'is-paused': paused()}"
@scroll="handleScroll"
>
<div v-for="(imageData, index) in imageHistory"
:key="index"
class="c-imagery__thumb c-thumb"
:class="{selected: imageData.selected}"
@click="setSelectedImage(imageData)"
>
<img class="c-thumb__image"
:src="getImageUrl(imageData)"
>
<div class="c-thumb__timestamp">{{ getTime(imageData) }}</div>
</div>
</div>
</div>
</template>
<script>
import multipane from '@/ui/layout/multipane.vue';
import pane from '@/ui/layout/pane.vue';
import _ from 'lodash';
export default {
inject: ['openmct', 'domainObject'],
components: {
multipane,
pane
},
data() {
return {
autoScroll: true,
@ -109,7 +87,7 @@ export default {
this.subscribe(this.domainObject);
},
updated() {
this.scrollToBottom();
this.scrollToRight();
},
beforeDestroy() {
this.stopListening();
@ -152,6 +130,10 @@ export default {
if (arguments.length > 0 && state !== this.isPaused) {
this.unselectAllImages();
this.isPaused = state;
if (state === true) {
// If we are pausing, select the latest image in imageHistory
this.setSelectedImage(this.imageHistory[this.imageHistory.length - 1]);
}
if (this.nextDatum) {
this.updateValues(this.nextDatum);
@ -180,24 +162,31 @@ export default {
this.updateValues(values[values.length - 1]);
});
},
scrollToBottom() {
scrollToRight() {
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
return;
}
const scrollHeight = this.$refs.thumbsWrapper.scrollHeight || 0;
if (!scrollHeight) {
const scrollWidth = this.$refs.thumbsWrapper.scrollWidth || 0;
if (!scrollWidth) {
return;
}
setTimeout(() => this.$refs.thumbsWrapper.scrollTop = scrollHeight, 0);
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
},
setSelectedImage(image) {
this.imageUrl = this.getImageUrl(image);
this.time = this.getTime(image);
this.paused(true);
this.unselectAllImages();
image.selected = true;
// If we are paused and the current image IS selected, unpause
// Otherwise, set current image and pause
if (this.isPaused && image.selected) {
this.paused(false);
this.unselectAllImages();
} else {
this.imageUrl = this.getImageUrl(image);
this.time = this.getTime(image);
this.paused(true);
this.unselectAllImages();
image.selected = true;
}
},
stopListening() {
if (this.unsubscribe) {

View File

@ -1,16 +1,20 @@
.c-imagery-layout {
.c-imagery {
display: flex;
flex-direction: column;
overflow: auto;
overflow: hidden;
height: 100%;
.main-image-wrapper {
display: flex;
flex-direction: column;
height: 100%;
padding-bottom: 5px;
> * + * {
margin-top: $interiorMargin;
}
.main-image {
&__main-image-wrapper {
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
&__main-image {
background-position: center;
background-repeat: no-repeat;
background-size: contain;
@ -21,12 +25,137 @@
}
}
.l-image-controller {
&__control-bar {
padding: 5px 0 0 0;
display: flex;
align-items: center;
}
.thumbs-layout {
margin-top: 5px;
overflow: auto;
&__timestamp {
flex: 1 1 auto;
}
&__thumbs-wrapper {
flex: 0 0 auto;
display: flex;
flex-direction: row;
height: 135px;
overflow-x: auto;
overflow-y: hidden;
&.is-paused {
background: rgba($colorPausedBg, 0.4);
}
.c-thumb:last-child {
// Hilite the lastest thumb
background: $colorBodyFg;
color: $colorBodyBg;
}
}
}
/*************************************** THUMBS */
.c-thumb {
display: flex;
flex-direction: column;
padding: 4px;
width: $imageThumbsD;
&:hover {
background: $colorThumbHoverBg;
}
&.selected {
background: $colorPausedBg !important;
color: $colorPausedFg !important;
}
&__image {
background-color: rgba($colorBodyFg, 0.2);
width: 100%;
}
&__timestamp {
flex: 0 0 auto;
padding: 2px 3px;
}
}
.l-layout,
.c-fl {
.c-imagery__thumbs-wrapper {
// When Imagery is in a layout, hide the thumbs area
display: none;
}
}
.s-image-main {
background-color: $colorPlotBg;
border: 1px solid transparent;
}
/*************************************** IMAGERY LOCAL CONTROLS*/
.c-imagery {
.h-local-controls--overlay-content {
position: absolute;
right: $interiorMargin; top: $interiorMargin;
z-index: 2;
background: $colorLocalControlOvrBg;
border-radius: $basicCr;
max-width: 200px;
min-width: 100px;
width: 35%;
align-items: center;
padding: $interiorMargin $interiorMarginLg;
input[type="range"] {
display: block;
width: 100%;
&:not(:first-child) {
margin-top: $interiorMarginLg;
}
&:before {
margin-right: $interiorMarginSm;
}
}
}
&__lc {
&__reset-btn {
$bc: $scrollbarTrackColorBg;
&:before,
&:after {
border-right: 1px solid $bc;
content:'';
display: block;
width: 5px;
height: 4px;
}
&:before {
border-top: 1px solid $bc;
margin-bottom: 2px;
}
&:after {
border-bottom: 1px solid $bc;
margin-top: 2px;
}
}
}
}
/*************************************** BUTTONS */
.c-button.pause-play {
// Pause icon set by default in markup
&.is-paused {
background: $colorPausedBg !important;
color: $colorPausedFg;
&:before {
content: $glyph-icon-play;
}
}
}

View File

@ -0,0 +1,270 @@
<template>
<div class="c-snapshot c-ne__embed">
<div v-if="embed.snapshot"
class="c-ne__embed__snap-thumb"
@click="openSnapshot()"
>
<img :src="embed.snapshot.src">
</div>
<div class="c-ne__embed__info">
<div class="c-ne__embed__name">
<a class="c-ne__embed__link"
:class="embed.cssClass"
@click="changeLocation"
>{{ embed.name }}</a>
<a class="c-ne__embed__context-available icon-arrow-down"
@click="toggleActionMenu"
></a>
</div>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(embed)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
<div v-if="embed.snapshot"
class="c-ne__embed__time"
>
{{ formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss') }}
</div>
</div>
</div>
</template>
<script>
import Moment from 'moment';
import PreviewAction from '../../../ui/preview/PreviewAction';
import Painterro from 'painterro';
import SnapshotTemplate from './snapshot-template.html';
import { togglePopupMenu } from '../utils/popup-menu';
import Vue from 'vue';
export default {
inject: ['openmct'],
components: {
},
props: {
embed: {
type: Object,
default() {
return {};
}
},
removeActionString: {
type: String,
default() {
return 'Remove Embed';
}
}
},
data() {
return {
actions: [this.removeEmbedAction()],
agentService: this.openmct.$injector.get('agentService'),
popupService: this.openmct.$injector.get('popupService')
}
},
watch: {
},
beforeMount() {
this.populateActionMenu();
},
methods: {
annotateSnapshot() {
const self = this;
let save = false;
let painterroInstance = {};
const annotateVue = new Vue({
template: '<div id="snap-annotation"></div>'
});
let annotateOverlay = self.openmct.overlays.overlay({
element: annotateVue.$mount().$el,
size: 'large',
dismissable: false,
buttons: [
{
label: 'Cancel',
callback: function () {
save = false;
painterroInstance.save();
annotateOverlay.dismiss();
}
},
{
label: 'Save',
callback: function () {
save = true;
painterroInstance.save();
annotateOverlay.dismiss();
}
}
],
onDestroy: function () {
annotateVue.$destroy(true);
}
});
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();
this.openmct.time.bounds({
start: this.embed.bounds.start,
end: this.embed.bounds.end
});
const link = this.embed.historicLink;
if (!link) {
return;
}
window.location.href = link;
const message = 'Time bounds changed to fixed timespan mode';
this.openmct.notifications.alert(message);
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);
},
openSnapshot() {
const self = this;
const snapshot = new Vue({
data: () => {
return {
embed: self.embed
};
},
methods: {
formatTime: self.formatTime,
annotateSnapshot: self.annotateSnapshot
},
template: SnapshotTemplate
});
const snapshotOverlay = this.openmct.overlays.overlay({
element: snapshot.$mount().$el,
onDestroy: () => { snapshot.$destroy(true) },
size: 'large',
dismissable: true,
buttons: [
{
label: 'Done',
emphasis: true,
callback: () => {
snapshotOverlay.dismiss();
}
}
]
});
},
populateActionMenu() {
const self = this;
const actions = [new PreviewAction(self.openmct)];
actions.forEach((action) => {
self.actions.push({
cssClass: action.cssClass,
name: action.name,
perform: () => {
action.invoke(JSON.parse(self.embed.objectPath));
}
});
});
},
removeEmbed(id) {
this.$emit('removeEmbed', id);
},
removeEmbedAction() {
const self = this;
return {
name: self.removeActionString,
cssClass: 'icon-trash',
perform: function (embed) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: `This action will permanently ${self.removeActionString.toLowerCase()}. Do you wish to continue?`,
buttons: [{
label: "No",
callback: function () {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: function () {
dialog.dismiss();
self.removeEmbed(embed.id);
}
}]
});
}
};
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
updateEmbed(embed) {
this.$emit('updateEmbed', embed);
}
}
}
</script>

View File

@ -0,0 +1,316 @@
<template>
<div class="c-notebook__entry c-ne has-local-controls"
@dragover="dragover"
@drop.capture="dropCapture"
@drop.prevent="dropOnEntry(entry.id, $event)"
>
<div class="c-ne__time-and-content">
<div class="c-ne__time">
<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"
class="c-ne__text"
:class="{'c-input-inline' : !readOnly }"
:contenteditable="!readOnly"
:style="!entry.text.length ? defaultEntryStyle : ''"
@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"
:key="embed.id"
:embed="embed"
:entry="entry"
@removeEmbed="removeEmbed"
@updateEmbed="updateEmbed"
/>
</div>
</div>
</div>
<div v-if="!readOnly"
class="c-ne__local-controls--hidden"
>
<button class="c-icon-button c-icon-button--major icon-trash"
title="Delete this entry"
@click="deleteEntry"
>
</button>
</div>
<div v-if="readOnly"
class="c-ne__section-and-page"
>
<a class="c-click-link"
@click="navigateToSection()"
>
{{ result.section.name }}
</a>
<span class="icon-arrow-right"></span>
<a class="c-click-link"
@click="navigateToPage()"
>
{{ result.page.name }}
</a>
</div>
</div>
</template>
<script>
import NotebookEmbed from './notebook-embed.vue';
import { createNewEmbed, getEntryPosById, getNotebookEntries } from '../utils/notebook-entries';
import Moment from 'moment';
export default {
inject: ['openmct', 'snapshotContainer'],
components: {
NotebookEmbed
},
props: {
domainObject: {
type: Object,
default() {
return {};
}
},
entry: {
type: Object,
default() {
return {};
}
},
result: {
type: Object,
default() {
return {};
}
},
selectedPage: {
type: Object,
default() {
return {};
}
},
selectedSection: {
type: Object,
default() {
return {};
}
},
readOnly: {
type: Boolean,
default() {
return true;
}
}
},
data() {
return {
currentEntryValue: '',
defaultEntryStyle: {
fontStyle: 'italic',
color: '#6e6e6e'
},
defaultText: 'add description'
}
},
watch: {
entry() {
},
readOnly(readOnly) {
},
selectedSection(selectedSection) {
},
selectedPage(selectedSection) {
}
},
mounted() {
this.updateEntries = this.updateEntries.bind(this);
},
beforeDestory() {
},
methods: {
deleteEntry() {
const self = this;
if (!self.domainObject || !self.selectedSection || !self.selectedPage || !self.entry.id) {
return;
}
const entryPosById = this.entryPosById(this.entry.id);
if (entryPosById === -1) {
return;
}
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will permanently delete this entry. Do you wish to continue?',
buttons: [
{
label: "Ok",
emphasis: true,
callback: () => {
const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage);
entries.splice(entryPosById, 1);
this.updateEntries(entries);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: () => {
dialog.dismiss();
}
}
]
});
},
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();
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const snapshotId = $event.dataTransfer.getData('snapshot/id');
if (snapshotId.length) {
this.moveSnapshot(snapshotId);
return;
}
const data = $event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
const entryPos = this.entryPosById(entryId);
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
}
const newEmbed = createNewEmbed(snapshotMeta);
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const currentEntryEmbeds = entries[entryPos].embeds;
currentEntryEmbeds.push(newEmbed);
this.updateEntries(entries);
},
entryPosById(entryId) {
return getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
},
findPositionInArray(array, id) {
let position = -1;
array.some((item, index) => {
const found = item.id === id;
if (found) {
position = index;
}
return found;
});
return position;
},
formatTime(unixTime, timeFormat) {
return Moment(unixTime).format(timeFormat);
},
moveSnapshot(snapshotId) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.entry.embeds.push(snapshot);
this.updateEntry(this.entry);
this.snapshotContainer.removeSnapshot(snapshotId);
},
navigateToPage() {
this.$emit('changeSectionPage', {
sectionId: this.result.section.id,
pageId: this.result.page.id
});
},
navigateToSection() {
this.$emit('changeSectionPage', {
sectionId: this.result.section.id,
pageId: null
});
},
removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
this.entry.embeds.splice(embedPosition, 1);
this.updateEntry(this.entry);
},
selectTextInsideElement(element) {
const range = document.createRange();
range.selectNodeContents(element);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
textBlur($event, entryId) {
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const target = $event.target;
if (!target) {
return;
}
const entryPos = this.entryPosById(entryId);
const value = target.textContent.trim();
if (this.currentEntryValue !== value) {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries[entryPos].text = value;
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);
}
}
}
</script>

View File

@ -0,0 +1,114 @@
<template>
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu icon-notebook"
title="Switch view type"
@click="setNotebookTypes"
@click.stop="toggleMenu"
>
<span class="c-button__label"></span>
</button>
<div
v-show="showMenu"
class="c-menu"
>
<ul>
<li
v-for="(type, index) in notebookTypes"
:key="index"
:class="type.cssClass"
:title="type.name"
@click="snapshot(type)"
>
{{ type.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
import Snapshot from '../snapshot';
import { clearDefaultNotebook, getDefaultNotebook } from '../utils/notebook-storage';
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
export default {
inject: ['openmct'],
props: {
domainObject: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
notebookSnapshot: null,
notebookTypes: [],
showMenu: false
}
},
mounted() {
this.notebookSnapshot = new Snapshot(this.openmct);
document.addEventListener('click', this.hideMenu);
},
destroyed() {
document.removeEventListener('click', this.hideMenu);
},
methods: {
async setNotebookTypes() {
const notebookTypes = [];
let defaultPath = '';
const defaultNotebook = getDefaultNotebook();
if (defaultNotebook) {
const domainObject = await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier)
.then(d => d);
if (!domainObject.location) {
clearDefaultNotebook();
} else {
defaultPath = `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
}
}
if (defaultPath.length !== 0) {
notebookTypes.push({
cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`,
type: NOTEBOOK_DEFAULT
});
}
notebookTypes.push({
cssClass: 'icon-notebook',
name: 'Save to Notebook Snapshots',
type: NOTEBOOK_SNAPSHOT
});
this.notebookTypes = notebookTypes;
},
toggleMenu() {
this.showMenu = !this.showMenu;
},
hideMenu() {
this.showMenu = false;
},
snapshot(notebook) {
let element = document.getElementsByClassName("l-shell__main-container")[0];
const bounds = this.openmct.time.bounds();
const objectPath = this.openmct.router.path;
const snapshotMeta = {
bounds,
link: window.location.href,
objectPath,
openmct: this.openmct
};
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
}
}
}
</script>

View File

@ -0,0 +1,152 @@
<template>
<div class="c-snapshots-h">
<div class="l-browse-bar">
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w icon-notebook">
<div class="l-browse-bar__object-name">
Notebook Snapshots
<span v-if="snapshots.length"
class="l-browse-bar__object-details"
>&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
</span>
</div>
<a class="l-browse-bar__context-actions c-disclosure-button"
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform()"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="l-browse-bar__end">
<button class="c-click-icon c-click-icon--major icon-x"
@click="close"
></button>
</div>
</div><!-- closes l-browse-bar -->
<div class="c-snapshots">
<span v-for="snapshot in snapshots"
:key="snapshot.id"
draggable="true"
@dragstart="startEmbedDrag(snapshot, $event)"
>
<NotebookEmbed ref="notebookEmbed"
:key="snapshot.id"
:embed="snapshot"
:remove-action-string="'Delete Snapshot'"
@updateEmbed="updateSnapshot"
@removeEmbed="removeSnapshot"
/>
</span>
<div v-if="!snapshots.length > 0"
class="hint"
>
There are no Notebook Snapshots currently.
</div>
</div>
</div>
</template>
<script>
import NotebookEmbed from './notebook-embed.vue';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
import { togglePopupMenu } from '../utils/popup-menu';
export default {
inject: ['openmct', 'snapshotContainer'],
components: {
NotebookEmbed
},
props: {
toggleSnapshot: {
type: Function,
default() {
return () => {};
}
}
},
data() {
return {
actions: [this.removeAllSnapshotAction()],
snapshots: []
}
},
mounted() {
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.snapshots = this.snapshotContainer.getSnapshots();
},
beforeDestory() {
},
methods: {
close() {
this.toggleSnapshot();
},
getNotebookSnapshotMaxCount() {
return NOTEBOOK_SNAPSHOT_MAX_COUNT;
},
removeAllSnapshotAction() {
const self = this;
return {
name: 'Delete All Snapshots',
cssClass: 'icon-trash',
perform: function (embed) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete all notebook snapshots. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.removeAllSnapshots();
dialog.dismiss();
}
}
]
});
}
};
},
removeAllSnapshots() {
this.snapshotContainer.removeAllSnapshots();
},
removeSnapshot(id) {
this.snapshotContainer.removeSnapshot(id);
},
snapshotsUpdated() {
this.snapshots = this.snapshotContainer.getSnapshots();
},
startEmbedDrag(snapshot, event) {
event.dataTransfer.setData('text/plain', snapshot.id);
event.dataTransfer.setData('snapshot/id', snapshot.id);
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
updateSnapshot(snapshot) {
this.snapshotContainer.updateSnapshot(snapshot);
}
}
}
</script>

View File

@ -0,0 +1,97 @@
<template>
<div class="c-indicator c-indicator--clickable icon-notebook"
:class="[
{ 's-status-off': snapshotCount === 0 },
{ 's-status-on': snapshotCount > 0 },
{ 's-status-caution': snapshotCount === snapshotMaxCount },
{ 'has-new-snapshot': flashIndicator }
]"
>
<span class="label c-indicator__label">
{{ indicatorTitle }}
<button @click="toggleSnapshot">
{{ expanded ? 'Hide' : 'Show' }}
</button>
</span>
<span class="c-indicator__count">{{ snapshotCount }}</span>
</div>
</template>
<script>
import SnapshotContainerComponent from './notebook-snapshot-container.vue';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import Vue from 'vue';
export default {
inject: ['openmct','snapshotContainer'],
data() {
return {
expanded: false,
indicatorTitle: '',
snapshotCount: 0,
snapshotMaxCount: NOTEBOOK_SNAPSHOT_MAX_COUNT,
flashIndicator: false
}
},
mounted() {
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.updateSnapshotIndicatorTitle();
},
methods: {
notifyNewSnapshot() {
this.flashIndicator = true;
setTimeout(this.removeNotify, 15000);
},
removeNotify() {
this.flashIndicator = false;
},
snapshotsUpdated() {
if (this.snapshotContainer.getSnapshots().length > this.snapshotCount) {
this.notifyNewSnapshot();
}
this.updateSnapshotIndicatorTitle();
},
toggleSnapshot() {
this.expanded = !this.expanded;
const drawerElement = document.querySelector('.l-shell__drawer');
drawerElement.classList.toggle('is-expanded');
this.updateSnapshotContainer();
},
updateSnapshotContainer() {
const { openmct, snapshotContainer } = this;
const toggleSnapshot = this.toggleSnapshot.bind(this);
const drawerElement = document.querySelector('.l-shell__drawer');
drawerElement.innerHTML = '<div></div>';
const divElement = document.querySelector('.l-shell__drawer div');
this.component = new Vue({
provide: {
openmct,
snapshotContainer
},
el: divElement,
components: {
SnapshotContainerComponent
},
data() {
return {
toggleSnapshot
};
},
template: '<SnapshotContainerComponent :toggleSnapshot="toggleSnapshot"></SnapshotContainerComponent>'
}).$mount();
},
updateSnapshotIndicatorTitle() {
const snapshotCount = this.snapshotContainer.getSnapshots().length;
this.snapshotCount = snapshotCount;
const snapshotTitleSuffix = snapshotCount === 1
? 'Snapshot'
: 'Snapshots';
this.indicatorTitle = `${snapshotCount} ${snapshotTitleSuffix}`;
}
}
}
</script>

View File

@ -0,0 +1,528 @@
<template>
<div class="c-notebook">
<div class="c-notebook__head">
<Search class="c-notebook__search"
:value="search"
@input="throttledSearchItem"
@clear="throttledSearchItem"
/>
</div>
<SearchResults v-if="search.length"
ref="searchResults"
:results="getSearchResults()"
@changeSectionPage="changeSelectedSection"
/>
<div v-if="!search.length"
class="c-notebook__body"
>
<Sidebar ref="sidebar"
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:default-page-id="defaultPageId"
:default-section-id="defaultSectionId"
:domain-object="internalDomainObject"
:page-title="internalDomainObject.configuration.pageTitle"
:pages="pages"
:section-title="internalDomainObject.configuration.sectionTitle"
:sections="sections"
:sidebar-covers-entries="sidebarCoversEntries"
@updatePage="updatePage"
@updateSection="updateSection"
@toggleNav="toggleNav"
/>
<div class="c-notebook__page-view">
<div class="c-notebook__page-view__header">
<button class="c-notebook__toggle-nav-button c-icon-button c-icon-button--major icon-menu-hamburger"
@click="toggleNav"
></button>
<div class="c-notebook__page-view__path c-path">
<span class="c-notebook__path__section c-path__item">
{{ getSelectedSection() ? getSelectedSection().name : '' }}
</span>
<span class="c-notebook__path__page c-path__item">
{{ getSelectedPage() ? getSelectedPage().name : '' }}
</span>
</div>
<div class="c-notebook__page-view__controls">
<select v-model="showTime"
class="c-notebook__controls__time"
>
<option value="0"
selected="selected"
>
Show all
</option>
<option value="1">Last hour</option>
<option value="8">Last 8 hours</option>
<option value="24">Last 24 hours</option>
</select>
<select v-model="defaultSort"
class="c-notebook__controls__time"
>
<option value="newest"
:selected="defaultSort === 'newest'"
>Newest first</option>
<option value="oldest"
:selected="defaultSort === 'oldest'"
>Oldest first</option>
</select>
</div>
</div>
<div class="c-notebook__drag-area icon-plus"
@click="newEntry()"
@dragover="dragOver"
@drop.capture="dropCapture"
@drop="dropOnEntry($event)"
>
<span class="c-notebook__drag-area__label">
To start a new entry, click here or drag and drop any object
</span>
</div>
<div v-if="selectedSection && selectedPage"
class="c-notebook__entries"
>
<NotebookEntry v-for="entry in filteredAndSortedEntries"
ref="notebookEntry"
:key="entry.id"
:entry="entry"
:domain-object="internalDomainObject"
:selected-page="getSelectedPage()"
:selected-section="getSelectedSection()"
:read-only="false"
@updateEntries="updateEntries"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import NotebookEntry from './notebook-entry.vue';
import Search from '@/ui/components/search.vue';
import SearchResults from './search-results.vue';
import Sidebar from './sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
import { throttle } from 'lodash';
const DEFAULT_CLASS = 'is-notebook-default';
export default {
inject: ['openmct', 'domainObject', 'snapshotContainer'],
components: {
NotebookEntry,
Search,
SearchResults,
Sidebar
},
data() {
return {
defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '',
defaultSectionId: getDefaultNotebook() ? getDefaultNotebook().section.id : '',
defaultSort: this.domainObject.configuration.defaultSort,
internalDomainObject: this.domainObject,
search: '',
showTime: 0,
showNav: false,
sidebarCoversEntries: false
}
},
computed: {
filteredAndSortedEntries() {
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
return pageEntries.sort(this.sortEntries);
},
pages() {
return this.getPages() || [];
},
sections() {
return this.internalDomainObject.configuration.sections || [];
},
selectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
}
return pages.find(page => page.isSelected);
},
selectedSection() {
if (!this.sections.length) {
return null;
}
return this.sections.find(section => section.isSelected);
}
},
watch: {
},
beforeMount() {
this.throttledSearchItem = throttle(this.searchItem, 500);
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.formatSidebar();
window.addEventListener('orientationchange', this.formatSidebar);
this.navigateToSectionPage();
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
addDefaultClass() {
const classList = this.internalDomainObject.classList || [];
if (classList.includes(DEFAULT_CLASS)) {
return;
}
classList.push(DEFAULT_CLASS);
this.mutateObject('classList', classList);
},
changeSelectedSection({ sectionId, pageId }) {
const sections = this.sections.map(s => {
s.isSelected = false;
if (s.id === sectionId) {
s.isSelected = true;
}
s.pages.forEach((p, i) => {
p.isSelected = false;
if (pageId && pageId === p.id) {
p.isSelected = true;
}
if (!pageId && i === 0) {
p.isSelected = true;
}
});
return s;
});
this.updateSection({ sections });
this.throttledSearchItem('');
},
dragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
},
dropCapture(event) {
const isEditing = this.openmct.editor.isEditing();
if (isEditing) {
this.openmct.editor.cancel();
}
},
dropOnEntry(event) {
event.preventDefault();
event.stopImmediatePropagation();
const snapshotId = event.dataTransfer.getData('snapshot/id');
if (snapshotId.length) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.newEntry(snapshot);
this.snapshotContainer.removeSnapshot(snapshotId);
return;
}
const data = event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
};
const embed = createNewEmbed(snapshotMeta);
this.newEntry(embed);
},
formatSidebar() {
/*
Determine if the sidebar should slide over content, or compress it
Slide over checks:
- phone (all orientations)
- tablet portrait
- in a layout frame (within .c-so-view)
*/
const classList = document.querySelector('body').classList;
const isPhone = Array.from(classList).includes('phone');
const isTablet = Array.from(classList).includes('tablet');
const isPortrait = window.screen.orientation.type.includes('portrait');
const isInLayout = !!this.$el.closest('.c-so-view');
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
this.sidebarCoversEntries = sidebarCoversEntries;
},
getDefaultNotebookObject() {
const oldNotebookStorage = getDefaultNotebook();
if (!oldNotebookStorage) {
return null;
}
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier).then(d => d);
},
getPage(section, id) {
return section.pages.find(p => p.id === id);
},
getSection(id) {
return this.sections.find(s => s.id === id);
},
getSearchResults() {
if (!this.search.length) {
return [];
}
const output = [];
const entries = this.internalDomainObject.configuration.entries;
const sectionKeys = Object.keys(entries);
sectionKeys.forEach(sectionKey => {
const pages = entries[sectionKey];
const pageKeys = Object.keys(pages);
pageKeys.forEach(pageKey => {
const pageEntries = entries[sectionKey][pageKey];
pageEntries.forEach(entry => {
if (entry.text && entry.text.toLowerCase().includes(this.search.toLowerCase())) {
const section = this.getSection(sectionKey);
output.push({
section,
page: this.getPage(section, pageKey),
entry
});
}
});
});
});
return output;
},
getPages() {
const selectedSection = this.getSelectedSection();
if (!selectedSection || !selectedSection.pages.length) {
return [];
}
return selectedSection.pages;
},
getSelectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
}
const selectedPage = pages.find(page => page.isSelected);
if (selectedPage) {
return selectedPage;
}
if (!selectedPage && !pages.length) {
return null;
}
pages[0].isSelected = true;
return pages[0];
},
getSelectedSection() {
if (!this.sections.length) {
return null;
}
return this.sections.find(section => section.isSelected);
},
mutateObject(key, value) {
this.openmct.objects.mutate(this.internalDomainObject, key, value);
},
navigateToSectionPage() {
const { pageId, sectionId } = this.openmct.router.getParams();
if(!pageId || !sectionId) {
return;
}
const sections = this.sections.map(s => {
s.isSelected = false;
if (s.id === sectionId) {
s.isSelected = true;
s.pages.forEach(p => p.isSelected = (p.id === pageId));
}
return s;
});
this.updateSection({ sections });
},
newEntry(embed = null) {
const selectedSection = this.getSelectedSection();
const selectedPage = this.getSelectedPage();
this.search = '';
this.updateDefaultNotebook(selectedSection, selectedPage);
const notebookStorage = getDefaultNotebook();
const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed);
this.$nextTick(() => {
const element = this.$el.querySelector(`#${id}`);
element.focus();
});
return id;
},
orientationChange() {
this.formatSidebar();
},
removeDefaultClass(domainObject) {
if (!domainObject) {
return;
}
const classList = domainObject.classList || [];
const index = classList.indexOf(DEFAULT_CLASS);
if (!classList.length || index < 0) {
return;
}
classList.splice(index, 1);
this.openmct.objects.mutate(domainObject, 'classList', classList);
},
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;
},
async updateDefaultNotebook(selectedSection, selectedPage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.internalDomainObject, selectedSection, selectedPage);
this.addDefaultClass();
this.defaultSectionId = selectedSection.id;
this.defaultPageId = selectedPage.id;
},
updateDefaultNotebookPage(pages, id) {
if (!id) {
return;
}
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
return;
}
const defaultNotebookPage = notebookStorage.page;
const page = pages.find(p => p.id === id);
if (!page && defaultNotebookPage.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null
this.removeDefaultClass(this.internalDomainObject);
clearDefaultNotebook();
return;
}
if (id !== defaultNotebookPage.id) {
return;
}
setDefaultNotebookPage(page);
},
updateDefaultNotebookSection(sections, id) {
if (!id) {
return;
}
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
return;
}
const defaultNotebookSection = notebookStorage.section;
const section = sections.find(s => s.id === id);
if (!section && defaultNotebookSection.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null
this.removeDefaultClass(this.internalDomainObject);
clearDefaultNotebook();
return;
}
if (section.id !== defaultNotebookSection.id) {
return;
}
setDefaultNotebookSection(section);
},
updateEntries(entries) {
const configuration = this.internalDomainObject.configuration;
const notebookEntries = configuration.entries || {};
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
this.mutateObject('configuration.entries', notebookEntries);
},
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
},
updatePage({ pages = [], id = null}) {
const selectedSection = this.getSelectedSection();
if (!selectedSection) {
return;
}
selectedSection.pages = pages;
const sections = this.sections.map(section => {
if (section.id === selectedSection.id) {
section = selectedSection;
}
return section;
});
this.updateSection({ sections });
this.updateDefaultNotebookPage(pages, id);
},
updateParams(sections) {
const selectedSection = sections.find(s => s.isSelected);
if (!selectedSection) {
return;
}
const selectedPage = selectedSection.pages.find(p => p.isSelected);
if (!selectedPage) {
return;
}
const sectionId = selectedSection.id;
const pageId = selectedPage.id;
if (!sectionId || !pageId) {
return;
}
this.openmct.router.updateParams({
sectionId,
pageId
});
},
updateSection({ sections, id = null }) {
this.mutateObject('configuration.sections', sections);
this.updateParams(sections);
this.updateDefaultNotebookSection(sections, id);
}
}
}
</script>

View File

@ -0,0 +1,132 @@
<template>
<ul class="c-list">
<li v-for="page in pages"
:key="page.id"
class="c-list__item-h"
>
<Page ref="pageComponent"
:default-page-id="defaultPageId"
:page="page"
:page-title="pageTitle"
@deletePage="deletePage"
@renamePage="updatePage"
@selectPage="selectPage"
/>
</li>
</ul>
</template>
<script>
import { deleteNotebookEntries } from '../utils/notebook-entries';
import { getDefaultNotebook } from '../utils/notebook-storage';
import Page from './page-component.vue';
export default {
inject: ['openmct'],
components: {
Page
},
props: {
defaultPageId: {
type: String,
default() {
return '';
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
pages: {
type: Array,
required: true,
default() {
return [];
}
},
sections: {
type: Array,
required: true,
default() {
return [];
}
},
pageTitle: {
type: String,
default() {
return '';
}
},
sidebarCoversEntries: {
type: Boolean,
default() {
return false;
}
}
},
data() {
return {
}
},
watch: {
},
mounted() {
},
destroyed() {
},
methods: {
deletePage(id) {
const selectedSection = this.sections.find(s => s.isSelected);
const page = this.pages.filter(p => p.id !== id);
deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page);
const selectedPage = this.pages.find(p => p.isSelected);
const defaultNotebook = getDefaultNotebook();
const defaultpage = defaultNotebook && defaultNotebook.page;
const isPageSelected = selectedPage && selectedPage.id === id;
const isPageDefault = defaultpage && defaultpage.id === id;
const pages = this.pages.filter(s => s.id !== id);
if (isPageSelected && defaultpage) {
pages.forEach(s => {
s.isSelected = false;
if (defaultpage && defaultpage.id === s.id) {
s.isSelected = true;
}
});
}
if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) {
pages[0].isSelected = true;
}
this.$emit('updatePage', { pages, id });
},
selectPage(id) {
const pages = this.pages.map(page => {
const isSelected = page.id === id;
page.isSelected = isSelected;
return page;
});
this.$emit('updatePage', { pages, id });
// Add test here for whether or not to toggle the nav
if (this.sidebarCoversEntries) {
this.$emit('toggleNav');
}
},
updatePage(newPage) {
const id = newPage.id;
const pages = this.pages.map(page =>
page.id === id
? newPage
: page);
this.$emit('updatePage', { pages, id });
}
}
}
</script>

View File

@ -0,0 +1,146 @@
<template>
<div class="c-list__item js-list__item"
:class="[{ 'is-selected': page.isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
:data-id="page.id"
@click="selectPage"
>
<span class="c-list__item__name js-list__item__name"
:data-id="page.id"
@keydown.enter="updateName"
@blur="updateName"
>{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down"
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(page.id)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import { togglePopupMenu } from '../utils/popup-menu';
export default {
inject: ['openmct'],
props: {
defaultPageId: {
type: String,
default() {
return '';
}
},
page: {
type: Object,
required: true
},
pageTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
actions: [this.deletePage()]
}
},
watch: {
page(newPage) {
this.toggleContentEditable(newPage);
}
},
mounted() {
this.toggleContentEditable();
},
destroyed() {
},
methods: {
deletePage() {
const self = this;
return {
name: `Delete ${this.pageTitle}`,
cssClass: 'icon-trash',
perform: function (id) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete this page and all of its entries. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.$emit('deletePage', id);
dialog.dismiss();
}
}
]
});
}
};
},
selectPage(event) {
const target = event.target;
const page = target.closest('.js-list__item');
const input = page.querySelector('.js-list__item__name');
if (page.className.indexOf('is-selected') > -1) {
input.contentEditable = true;
input.classList.add('c-input-inline');
return;
}
const id = target.dataset.id;
if (!id) {
return;
}
this.$emit('selectPage', id);
},
toggleActionMenu(event) {
event.preventDefault();
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(page = this.page) {
const pageTitle = this.$el.querySelector('span');
pageTitle.contentEditable = page.isSelected;
},
updateName(event) {
const target = event.target;
const name = target.textContent.toString();
target.contentEditable = false;
target.classList.remove('c-input-inline');
if (this.page.name === name) {
return;
}
if (name === '') {
return;
}
this.$emit('renamePage', Object.assign(this.page, { name }));
}
}
}
</script>

View File

@ -0,0 +1,50 @@
<template>
<div class="c-notebook__search-results">
<div class="c-notebook__search-results__header">Search Results</div>
<div class="c-notebook__entries">
<NotebookEntry v-for="(result, index) in results"
:key="index"
:result="result"
:entry="result.entry"
:read-only="true"
:selected-page="null"
:selected-section="null"
@changeSectionPage="changeSectionPage"
/>
</div>
</div>
</template>
<script>
import NotebookEntry from './notebook-entry.vue';
export default {
inject: ['openmct', 'domainObject'],
components: {
NotebookEntry
},
props:{
results: {
type: Array,
default() {
return [];
}
}
},
data() {
return {}
},
watch: {
results(newResults) {}
},
destroyed() {
},
mounted() {
},
methods: {
changeSectionPage(data) {
this.$emit('changeSectionPage', data);
}
}
}
</script>

View File

@ -0,0 +1,113 @@
<template>
<ul class="c-list">
<li v-for="section in sections"
:key="section.id"
class="c-list__item-h"
>
<sectionComponent ref="sectionComponent"
:default-section-id="defaultSectionId"
:section="section"
:section-title="sectionTitle"
@deleteSection="deleteSection"
@renameSection="updateSection"
@selectSection="selectSection"
/>
</li>
</ul>
</template>
<script>
import { deleteNotebookEntries } from '../utils/notebook-entries';
import { getDefaultNotebook } from '../utils/notebook-storage';
import sectionComponent from './section-component.vue';
export default {
inject: ['openmct'],
components: {
sectionComponent
},
props: {
defaultSectionId: {
type: String,
default() {
return '';
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
sections: {
type: Array,
required: true,
default() {
return [];
}
},
sectionTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
}
},
watch: {
},
mounted() {
},
destroyed() {
},
methods: {
deleteSection(id) {
const section = this.sections.find(s => s.id === id);
deleteNotebookEntries(this.openmct, this.domainObject, section);
const selectedSection = this.sections.find(s => s.isSelected);
const defaultNotebook = getDefaultNotebook();
const defaultSection = defaultNotebook && defaultNotebook.section;
const isSectionSelected = selectedSection && selectedSection.id === id;
const isSectionDefault = defaultSection && defaultSection.id === id;
const sections = this.sections.filter(s => s.id !== id);
if (isSectionSelected && defaultSection) {
sections.forEach(s => {
s.isSelected = false;
if (defaultSection && defaultSection.id === s.id) {
s.isSelected = true;
}
});
}
if (sections.length && isSectionSelected && (!defaultSection || isSectionDefault)) {
sections[0].isSelected = true;
}
this.$emit('updateSection', { sections, id });
},
selectSection(id, newSections) {
const currentSections = newSections || this.sections;
const sections = currentSections.map(section => {
const isSelected = section.id === id;
section.isSelected = isSelected;
return section;
});
this.$emit('updateSection', { sections, id });
},
updateSection(newSection) {
const id = newSection.id;
const sections = this.sections.map(section =>
section.id === id
? newSection
: section);
this.$emit('updateSection', { sections, id });
}
}
}
</script>

View File

@ -0,0 +1,149 @@
<template>
<div class="c-list__item js-list__item"
:class="[{ 'is-selected': section.isSelected, 'is-notebook-default' : (defaultSectionId === section.id) }]"
:data-id="section.id"
@click="selectSection"
>
<span class="c-list__item__name js-list__item__name"
:data-id="section.id"
@keydown.enter="updateName"
@blur="updateName"
>{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down"
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(section.id)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<style lang="scss">
</style>
<script>
import { togglePopupMenu } from '../utils/popup-menu';
export default {
inject: ['openmct'],
props: {
defaultSectionId: {
type: String,
default() {
return '';
}
},
section: {
type: Object,
required: true
},
sectionTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
actions: [this.deleteSectionAction()]
}
},
watch: {
section(newSection) {
this.toggleContentEditable(newSection);
}
},
mounted() {
this.toggleContentEditable();
},
destroyed() {
},
methods: {
deleteSectionAction() {
const self = this;
return {
name: `Delete ${this.sectionTitle}`,
cssClass: 'icon-trash',
perform: function (id) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete this section and all of its pages and entries. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.$emit('deleteSection', id);
dialog.dismiss();
}
}
]
});
}
};
},
selectSection(event) {
const target = event.target;
const section = target.closest('.js-list__item');
const input = section.querySelector('.js-list__item__name');
if (section.className.indexOf('is-selected') > -1) {
input.contentEditable = true;
input.classList.add('c-input-inline');
return;
}
const id = target.dataset.id;
if (!id) {
return;
}
this.$emit('selectSection', id);
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(section = this.section) {
const sectionTitle = this.$el.querySelector('span');
sectionTitle.contentEditable = section.isSelected;
},
updateName(event) {
const target = event.target;
target.contentEditable = false;
target.classList.remove('c-input-inline');
const name = target.textContent.trim();
if (this.section.name === name) {
return;
}
if (name === '') {
return;
}
this.$emit('renameSection', Object.assign(this.section, { name }));
}
}
}
</script>

View File

@ -0,0 +1,119 @@
.c-sidebar {
@include userSelectNone();
background: $sideBarBg;
display: flex;
justify-content: stretch;
max-width: 300px;
&.c-drawer--push.is-expanded {
margin-right: $interiorMargin;
width: 50%;
}
&.c-drawer--overlays.is-expanded {
width: 95%;
}
> * {
// Hardcoded for two columns
background: $sideBarBg;
display: flex;
flex: 1 1 50%;
flex-direction: column;
+ * {
margin-left: $interiorMarginSm;
}
> * + * {
// Add margin-top to first and second level children
margin-top: $interiorMargin;
}
}
&__pane {
> * + * { margin-top: $interiorMargin; }
}
&__header-w {
// Wraps header, used for page pane with collapse button
display: flex;
flex: 0 0 auto;
background: $sideBarHeaderBg;
align-items: center;
.c-icon-button {
font-size: 0.8em;
color: $colorBodyFg;
}
}
&__header {
color: $sideBarHeaderFg;
display: flex;
flex: 1 1 auto;
padding: $interiorMargin;
text-transform: uppercase;
> * {
@include ellipsize();
}
}
&__contents-and-controls {
// Encloses pane buttons and contents elements
display: flex;
flex-direction: column;
flex: 1 1 auto;
> * {
margin: auto $interiorMargin $interiorMargin $interiorMargin;
&:first-child {
border-bottom: 1px solid $colorInteriorBorder;
flex: 0 0 auto;
}
+ * {
margin-top: $interiorMargin;
}
}
}
&__contents {
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
padding: auto $interiorMargin;
}
.c-list-button {
.c-button {
font-size: 0.8em;
}
}
.c-list__item {
@include hover() {
[class*="__menu-indicator"] {
opacity: 0.7;
transition: $transIn;
}
}
> * + * {
margin-left: $interiorMargin;
}
&__name {
flex: 0 1 auto;
}
&__menu-indicator {
flex: 0 0 auto;
font-size: 0.8em;
opacity: 0;
transition: $transOut;
}
}
}

View File

@ -0,0 +1,189 @@
<template>
<div class="c-sidebar c-drawer c-drawer--align-left">
<div class="c-sidebar__pane">
<div class="c-sidebar__header-w">
<div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
</div>
</div>
<div class="c-sidebar__contents-and-controls">
<button class="c-list-button"
@click="addSection"
>
<span class="c-button c-list-button__button icon-plus"></span>
<span class="c-list-button__label">Add {{ sectionTitle }}</span>
</button>
<SectionCollection class="c-sidebar__contents"
:default-section-id="defaultSectionId"
:domain-object="domainObject"
:sections="sections"
:section-title="sectionTitle"
@updateSection="updateSection"
/>
</div>
</div>
<div class="c-sidebar__pane">
<div class="c-sidebar__header-w">
<div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ pageTitle }}</span>
</div>
<button class="c-click-icon c-click-icon--major icon-x-in-circle"
@click="toggleNav"
></button>
</div>
<div class="c-sidebar__contents-and-controls">
<button class="c-list-button"
@click="addPage"
>
<span class="c-button c-list-button__button icon-plus"></span>
<span class="c-list-button__label">Add {{ pageTitle }}</span>
</button>
<PageCollection ref="pageCollection"
class="c-sidebar__contents"
:default-page-id="defaultPageId"
:domain-object="domainObject"
:pages="pages"
:sections="sections"
:sidebar-covers-entries="sidebarCoversEntries"
:page-title="pageTitle"
@toggleNav="toggleNav"
@updatePage="updatePage"
/>
</div>
</div>
</div>
</template>
<script>
import SectionCollection from './section-collection.vue';
import PageCollection from './page-collection.vue';
import uuid from 'uuid';
export default {
inject: ['openmct'],
components: {
SectionCollection,
PageCollection
},
props: {
defaultPageId: {
type: String,
default() {
return '';
}
},
defaultSectionId: {
type: String,
default() {
return '';
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
pages: {
type: Array,
required: true,
default() {
return [];
}
},
pageTitle: {
type: String,
default() {
return '';
}
},
sections: {
type: Array,
required: true,
default() {
return [];
}
},
sectionTitle: {
type: String,
default() {
return '';
}
},
sidebarCoversEntries: {
type: Boolean,
default() {
return false;
}
}
},
data() {
return {
}
},
watch: {
pages(newpages) {
if (!newpages.length) {
this.addPage();
}
},
sections(newSections) {
if (!newSections.length) {
this.addSection();
}
}
},
mounted() {
if (!this.sections.length) {
this.addSection();
}
},
destroyed() {
},
methods: {
addPage() {
const pageTitle = this.pageTitle;
const id = uuid();
const page = {
id,
isDefault : false,
isSelected: true,
name : `Unnamed ${pageTitle}`,
pageTitle
};
this.pages.forEach(p => p.isSelected = false);
const pages = this.pages.concat(page);
this.updatePage({ pages, id });
},
addSection() {
const sectionTitle = this.sectionTitle;
const id = uuid();
const section = {
id,
isDefault : false,
isSelected: true,
name : `Unnamed ${sectionTitle}`,
pages : [],
sectionTitle
};
this.sections.forEach(s => s.isSelected = false);
const sections = this.sections.concat(section);
this.updateSection({ sections, id });
},
toggleNav() {
this.$emit('toggleNav');
},
updatePage({ pages, id }) {
this.$emit('updatePage', { pages, id });
},
updateSection({ sections, id }) {
this.$emit('updateSection', { sections, id });
}
}
}
</script>

View File

@ -3,10 +3,10 @@
<div class="c-notebook-snapshot__header l-browse-bar">
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w">
<span class="l-browse-bar__object-name"
<span class="c-object-label l-browse-bar__object-name"
v-bind:class="embed.cssClass"
>
{{embed.name}}
<span class="c-object-label__name">{{ embed.name }}</span>
</span>
</div>
</div>
@ -21,9 +21,8 @@
</div>
</div>
<div class="c-notebook-snapshot__image">
<div class="image-main s-image-main"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
></div>
<div class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
>
</div>
</div>

View File

@ -0,0 +1,3 @@
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
export const NOTEBOOK_DEFAULT = 'DEFAULT';
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';

View File

@ -1,99 +1,134 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Notebook from './components/notebook.vue';
import NotebookSnapshotIndicator from './components/notebook-snapshot-indicator.vue';
import SnapshotContainer from './snapshot-container';
import Vue from 'vue';
define([
"./src/controllers/NotebookController"
], function (
NotebookController
) {
var installed = false;
let installed = false;
function NotebookPlugin() {
return function install(openmct) {
if (installed) {
return;
}
export default function NotebookPlugin() {
return function install(openmct) {
if (installed) {
return;
}
installed = true;
installed = true;
openmct.legacyRegistry.register('notebook', {
name: 'Notebook Plugin',
extensions: {
types: [
const notebookType = {
name: 'Notebook',
description: 'Create and save timestamped notes with embedded object snapshots.',
creatable: true,
cssClass: 'icon-notebook',
initialize: domainObject => {
domainObject.configuration = {
defaultSort: 'oldest',
entries: {},
pageTitle: 'Page',
sections: [],
sectionTitle: 'Section',
type: 'General'
};
},
form: [
{
key: 'defaultSort',
name: 'Entry Sorting',
control: 'select',
options: [
{
key: 'notebook',
name: 'Notebook',
cssClass: 'icon-notebook',
description: 'Create and save timestamped notes with embedded object snapshots.',
features: 'creation',
model: {
entries: [],
entryTypes: [],
defaultSort: 'oldest'
},
properties: [
{
key: 'defaultSort',
name: 'Default Sort',
control: 'select',
options: [
{
name: 'Newest First',
value: "newest"
},
{
name: 'Oldest First',
value: "oldest"
}
],
cssClass: 'l-inline'
}
]
name: 'Newest First',
value: "newest"
},
{
name: 'Oldest First',
value: "oldest"
}
],
cssClass: 'l-inline',
property: [
"configuration",
"defaultSort"
]
},
{
key: 'type',
name: 'Note book Type',
control: 'textfield',
cssClass: 'l-inline',
property: [
"configuration",
"type"
]
},
{
key: 'sectionTitle',
name: 'Section Title',
control: 'textfield',
cssClass: 'l-inline',
property: [
"configuration",
"sectionTitle"
]
},
{
key: 'pageTitle',
name: 'Page Title',
control: 'textfield',
cssClass: 'l-inline',
property: [
"configuration",
"pageTitle"
]
}
});
]
};
openmct.types.addType('notebook', notebookType);
openmct.legacyRegistry.enable('notebook');
openmct.objectViews.addProvider({
key: 'notebook-vue',
name: 'Notebook View',
cssClass: 'icon-notebook',
canView: function (domainObject) {
return domainObject.type === 'notebook';
},
view: function (domainObject) {
var controller = new NotebookController (openmct, domainObject);
return {
show: controller.show,
destroy: controller.destroy
};
}
});
const snapshotContainer = new SnapshotContainer(openmct);
const notebookSnapshotIndicator = new Vue ({
provide: {
openmct,
snapshotContainer
},
components: {
NotebookSnapshotIndicator
},
template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
});
const indicator = {
element: notebookSnapshotIndicator.$mount().$el
};
}
openmct.indicators.add(indicator);
return NotebookPlugin;
});
openmct.objectViews.addProvider({
key: 'notebook-vue',
name: 'Notebook View',
cssClass: 'icon-notebook',
canView: function (domainObject) {
return domainObject.type === 'notebook';
},
view: function (domainObject) {
let component;
return {
show(container) {
component = new Vue({
el: container,
components: {
Notebook
},
provide: {
openmct,
domainObject,
snapshotContainer
},
template: '<Notebook></Notebook>'
});
},
destroy() {
component.$destroy();
}
};
}
});
};
}

View File

@ -1,33 +0,0 @@
<div class="c-ne__embed">
<div class="c-ne__embed__snap-thumb"
v-if="embed.snapshot"
v-on:click="openSnapshot(domainObject, entry, embed)">
<img v-bind:src="embed.snapshot.src">
</div>
<div class="c-ne__embed__info">
<div class="c-ne__embed__name">
<a class="c-ne__embed__link"
:href="objectLink"
:class="embed.cssClass">{{embed.name}}</a>
<a class="c-ne__embed__context-available icon-arrow-down"
@click="toggleActionMenu"></a>
</div>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(embed, entry)">
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
<div class="c-ne__embed__time" v-if="embed.snapshot">
{{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div>
</div>
</div>

View File

@ -1,34 +0,0 @@
<li class="c-notebook__entry c-ne has-local-controls"
@drop.prevent="dropOnEntry(entry.id, $event)">
<div class="c-ne__time-and-content">
<div class="c-ne__time">
<span>{{formatTime(entry.createdOn, 'YYYY-MM-DD')}}</span>
<span>{{formatTime(entry.createdOn, 'HH:mm:ss')}}</span>
</div>
<div class="c-ne__content">
<div class="c-ne__text c-input-inline"
contenteditable="true"
ref="contenteditable"
@blur="textBlur($event, entry.id)"
@focus="textFocus($event, entry.id)"
v-html="entry.text">
</div>
<div class="c-ne__embeds">
<notebook-embed
v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
:objectPath="embed.objectPath"
:entry="entry">
</notebook-embed>
</div>
</div>
</div>
<div class="c-ne__local-controls--hidden">
<button class="c-icon-button c-icon-button--major icon-trash"
title="Delete this entry"
@click="deleteEntry">
</button>
</div>
</li>

View File

@ -1,35 +0,0 @@
<div class="c-notebook">
<div class="c-notebook__head">
<search class="c-notebook__search"
:value="entrySearch"
@input="search"
@clear="search">
</search>
<div class="c-notebook__controls ">
<select class="c-notebook__controls__time" v-model="showTime">
<option value="0" selected="selected">Show all</option>
<option value="1">Last hour</option>
<option value="8">Last 8 hours</option>
<option value="24">Last 24 hours</option>
</select>
<select class="c-notebook__controls__time" v-model="sortEntries">
<option value="newest" :selected="sortEntries === 'newest'">Newest first</option>
<option value="oldest" :selected="sortEntries === 'oldest'">Oldest first</option>
</select>
</div>
</div>
<div class="c-notebook__drag-area icon-plus"
@click="newEntry($event)"
@drop="newEntry($event)">
<span class="c-notebook__drag-area__label">To start a new entry, click here or drag and drop any object</span>
</div>
<div class="c-notebook__entries">
<ul>
<notebook-entry
v-for="(entry,index) in filteredAndSortedEntries"
:key="entry.key"
:entry="entry">
</notebook-entry>
</ul>
</div>
</div>

View File

@ -1,50 +0,0 @@
<div class="abs overlay l-large-view">
<div class="abs blocker" v-on:click="close"></div>
<div class="abs outer-holder">
<a
class="close icon-x-in-circle"
v-on:click="close">
</a>
<div class="abs inner-holder l-flex-col">
<div class="t-contents flex-elem holder grows">
<div class="t-snapshot abs l-view-header">
<div class="abs object-browse-bar l-flex-row">
<div class="left flex-elem l-flex-row grows">
<div class="object-header flex-elem l-flex-row grows">
<div class="type-icon flex-elem embed-icon holder" v-bind:class="embed.cssClass"></div>
<div class="title-label flex-elem holder flex-can-shrink">{{embed.name}}</div>
</div>
</div>
</div>
</div>
<div class="btn-bar right l-flex-row flex-elem flex-justify-end flex-fixed">
<div class="flex-elem holder flex-can-shrink s-snapshot-datetime">
SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div>
<a class="s-button icon-pencil" title="Annotate">
<span class="title-label">Annotate</span>
</a>
</div>
<div class="abs object-holder t-image-holder s-image-holder">
<div
class="image-main s-image-main"
v-bind:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }">
</div>
</div>
</div>
<div
class="bottom-bar flex-elem holder"
v-on:click="close">
<a class="t-done s-button major">Done</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,82 @@
import EventEmitter from 'EventEmitter';
import { EVENT_SNAPSHOTS_UPDATED } from './notebook-constants';
const NOTEBOOK_SNAPSHOT_STORAGE = 'notebook-snapshot-storage';
export const NOTEBOOK_SNAPSHOT_MAX_COUNT = 5;
export default class SnapshotContainer extends EventEmitter {
constructor(openmct) {
super();
if (!SnapshotContainer.instance) {
SnapshotContainer.instance = this;
}
this.openmct = openmct;
return SnapshotContainer.instance;
}
addSnapshot(embedObject) {
const snapshots = this.getSnapshots();
if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) {
snapshots.pop();
}
snapshots.unshift(embedObject);
return this.saveSnapshots(snapshots);
}
getSnapshot(id) {
const snapshots = this.getSnapshots();
return snapshots.find(s => s.id === id);
}
getSnapshots() {
const snapshots = window.localStorage.getItem(NOTEBOOK_SNAPSHOT_STORAGE) || '[]';
return JSON.parse(snapshots);
}
removeSnapshot(id) {
if (!id) {
return;
}
const snapshots = this.getSnapshots();
const filteredsnapshots = snapshots.filter(snapshot => snapshot.id !== id);
return this.saveSnapshots(filteredsnapshots);
}
removeAllSnapshots() {
return this.saveSnapshots([]);
}
saveSnapshots(snapshots) {
try {
window.localStorage.setItem(NOTEBOOK_SNAPSHOT_STORAGE, JSON.stringify(snapshots));
this.emit(EVENT_SNAPSHOTS_UPDATED, true);
return true;
} catch (e) {
const message = 'Insufficient memory in localstorage to store snapshot, please delete some snapshots and try again!';
this.openmct.notifications.error(message);
return false;
}
}
updateSnapshot(snapshot) {
const snapshots = this.getSnapshots();
const updatedSnapshots = snapshots.map(s => {
return s.id === snapshot.id
? snapshot
: s;
});
return this.saveSnapshots(updatedSnapshots);
}
}

View File

@ -0,0 +1,74 @@
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
import { getDefaultNotebook } from './utils/notebook-storage';
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import SnapshotContainer from './snapshot-container';
export default class Snapshot {
constructor(openmct) {
this.openmct = openmct;
this.snapshotContainer = new SnapshotContainer(openmct);
this.exportImageService = openmct.$injector.get('exportImageService');
this.dialogService = openmct.$injector.get('dialogService');
this.capture = this.capture.bind(this);
this._saveSnapShot = this._saveSnapShot.bind(this);
}
capture(snapshotMeta, notebookType, domElement) {
this.exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot')
.then(function (blob) {
const reader = new window.FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
this._saveSnapShot(notebookType, reader.result, snapshotMeta);
}.bind(this);
}.bind(this));
}
/**
* @private
*/
_saveSnapShot(notebookType, imageUrl, snapshotMeta) {
const snapshot = imageUrl ? { src: imageUrl } : '';
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
this._saveToDefaultNoteBook(embed);
return;
}
this._saveToNotebookSnapshots(embed);
}
/**
* @private
*/
_saveToDefaultNoteBook(embed) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
const defaultPath = `${domainObject.name} > ${notebookStorage.section.name} > ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this._showNotification(msg);
});
}
/**
* @private
*/
_saveToNotebookSnapshots(embed) {
const saved = this.snapshotContainer.addSnapshot(embed);
if (!saved) {
return;
}
const msg = 'Saved to Notebook Snapshots - click to view.';
this._showNotification(msg);
}
_showNotification(msg) {
this.openmct.notifications.info(msg);
}
}

View File

@ -1,302 +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.
*****************************************************************************/
define([
'moment',
'zepto',
'../../res/templates/snapshotTemplate.html',
'vue',
'painterro'
],
function (
Moment,
$,
SnapshotTemplate,
Vue,
Painterro
) {
function EmbedController(openmct, domainObject) {
this.openmct = openmct;
this.domainObject = domainObject;
this.popupService = openmct.$injector.get('popupService');
this.agentService = openmct.$injector.get('agentService');
this.exposedData = this.exposedData.bind(this);
this.exposedMethods = this.exposedMethods.bind(this);
this.toggleActionMenu = this.toggleActionMenu.bind(this);
}
EmbedController.prototype.openSnapshot = function (domainObject, entry, embed) {
function annotateSnapshot(openmct) {
return function () {
var save = false,
painterroInstance = {},
annotateVue = new Vue({
template: '<div id="snap-annotation"></div>'
}),
self = this;
let annotateOverlay = openmct.overlays.overlay({
element: annotateVue.$mount().$el,
size: 'large',
dismissable: false,
buttons: [
{
label: 'Cancel',
callback: function () {
save = false;
painterroInstance.save();
annotateOverlay.dismiss();
}
},
{
label: 'Save',
callback: function () {
save = true;
painterroInstance.save();
annotateOverlay.dismiss();
}
}
],
onDestroy: function () {
annotateVue.$destroy(true);
}
});
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) {
var entryPos = self.findInArray(domainObject.entries, entry.id),
embedPos = self.findInArray(entry.embeds, embed.id);
if (entryPos !== -1 && embedPos !== -1) {
var url = image.asBlob(),
reader = new window.FileReader();
reader.readAsDataURL(url);
reader.onloadend = function () {
var snapshot = reader.result,
snapshotObject = {
src: snapshot,
type: url.type,
size: url.size,
modified: Date.now()
},
dirString = 'entries[' + entryPos + '].embeds[' + embedPos + '].snapshot';
openmct.objects.mutate(domainObject, dirString, snapshotObject);
};
}
} else {
console.log('You cancelled the annotation!!!');
}
done(true);
}
}).show(embed.snapshot.src);
};
}
var self = this,
snapshot = new Vue({
data: function () {
return {
embed: self.embed
};
},
methods: {
formatTime: self.formatTime,
annotateSnapshot: annotateSnapshot(self.openmct),
findInArray: self.findInArray
},
template: SnapshotTemplate
});
var snapshotOverlay = this.openmct.overlays.overlay({
element: snapshot.$mount().$el,
onDestroy: () => {snapshot.$destroy(true)},
size: 'large',
dismissable: true,
buttons: [
{
label: 'Done',
emphasis: true,
callback: function () {
snapshotOverlay.dismiss();
}
}
]
});
};
EmbedController.prototype.formatTime = function (unixTime, timeFormat) {
return Moment(unixTime).format(timeFormat);
};
EmbedController.prototype.findInArray = function (array, id) {
var foundId = -1;
array.forEach(function (element, index) {
if (element.id === id) {
foundId = index;
return;
}
});
return foundId;
};
EmbedController.prototype.populateActionMenu = function (openmct, actions) {
return function () {
var self = this;
openmct.objects.get(self.embed.type).then(function (domainObject) {
actions.forEach((action) => {
self.actions.push({
cssClass: action.cssClass,
name: action.name,
perform: () => {
action.invoke([domainObject].concat(openmct.router.path));
}
});
});
});
};
};
EmbedController.prototype.removeEmbedAction = function () {
var self = this;
return {
name: 'Remove Embed',
cssClass: 'icon-trash',
perform: function (embed, entry) {
var entryPosition = self.findInArray(self.domainObject.entries, entry.id),
embedPosition = self.findInArray(entry.embeds, embed.id);
var dialog = self.openmct.overlays.dialog({
iconClass: "alert",
message: 'This Action will permanently delete this embed. Do you wish to continue?',
buttons: [{
label: "No",
callback: function () {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: function () {
entry.embeds.splice(embedPosition, 1);
var dirString = 'entries[' + entryPosition + '].embeds';
self.openmct.objects.mutate(self.domainObject, dirString, entry.embeds);
dialog.dismiss();
}
}]
});
}
};
};
EmbedController.prototype.toggleActionMenu = function (event) {
event.preventDefault();
var body = $(document.body),
container = $(event.target.parentElement.parentElement),
initiatingEvent = this.agentService.isMobile() ?
'touchstart' : 'mousedown',
menu = container.find('.menu-element'),
dismissExistingMenu;
// Remove the context menu
function dismiss() {
container.find('.hide-menu').append(menu);
body.off(initiatingEvent, menuClickHandler);
dismissExistingMenu = undefined;
}
function menuClickHandler(e) {
window.setTimeout(() => {
dismiss();
}, 100);
}
// Dismiss any menu which was already showing
if (dismissExistingMenu) {
dismissExistingMenu();
}
// ...and record the presence of this menu.
dismissExistingMenu = dismiss;
this.popupService.display(menu, [event.pageX,event.pageY], {
marginX: 0,
marginY: -50
});
body.on(initiatingEvent, menuClickHandler);
};
EmbedController.prototype.exposedData = function () {
return {
actions: [this.removeEmbedAction()],
showActionMenu: false
};
};
EmbedController.prototype.exposedMethods = function () {
var self = this;
return {
openSnapshot: self.openSnapshot,
formatTime: self.formatTime,
toggleActionMenu: self.toggleActionMenu,
findInArray: self.findInArray
};
};
return EmbedController;
});

View File

@ -1,151 +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.
*****************************************************************************/
define([
'moment'
],
function (
Moment
) {
function EntryController(openmct, domainObject) {
this.openmct = openmct;
this.domainObject = domainObject;
this.currentEntryValue = '';
this.exposedMethods = this.exposedMethods.bind(this);
this.exposedData = this.exposedData.bind(this);
}
EntryController.prototype.entryPosById = function (entryId) {
var foundId = -1;
this.domainObject.entries.forEach(function (element, index) {
if (element.id === entryId) {
foundId = index;
return;
}
});
return foundId;
};
EntryController.prototype.textFocus = function ($event) {
if ($event.target) {
this.currentEntryValue = $event.target.innerText;
} else {
$event.target.innerText = '';
}
};
EntryController.prototype.textBlur = function ($event, entryId) {
if ($event.target) {
var entryPos = this.entryPosById(entryId);
if (this.currentEntryValue !== $event.target.innerText) {
this.openmct.objects.mutate(this.domainObject, 'entries[' + entryPos + '].text', $event.target.innerText);
}
}
};
EntryController.prototype.formatTime = function (unixTime, timeFormat) {
return Moment(unixTime).format(timeFormat);
};
EntryController.prototype.deleteEntry = function () {
var entryPos = this.entryPosById(this.entry.id),
domainObject = this.domainObject,
openmct = this.openmct;
if (entryPos !== -1) {
var dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will permanently delete this entry. Do you wish to continue?',
buttons: [
{
label: "Ok",
emphasis: true,
callback: function () {
domainObject.entries.splice(entryPos, 1);
openmct.objects.mutate(domainObject, 'entries', domainObject.entries);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: function () {
dialog.dismiss();
}
}
]
});
}
};
EntryController.prototype.dropOnEntry = function (entryid, event) {
var data = event.dataTransfer.getData('openmct/domain-object-path');
if (data) {
var objectPath = JSON.parse(data),
domainObject = objectPath[0],
domainObjectKey = domainObject.identifier.key,
domainObjectType = this.openmct.types.get(domainObject.type),
cssClass = domainObjectType && domainObjectType.definition ?
domainObjectType.definition.cssClass : 'icon-object-unknown',
entryPos = this.entryPosById(entryid),
currentEntryEmbeds = this.domainObject.entries[entryPos].embeds,
newEmbed = {
id: '' + Date.now(),
domainObject: domainObject,
objectPath: objectPath,
type: domainObjectKey,
cssClass: cssClass,
name: domainObject.name,
snapshot: ''
};
currentEntryEmbeds.push(newEmbed);
this.openmct.objects.mutate(this.domainObject, 'entries[' + entryPos + '].embeds', currentEntryEmbeds);
}
};
EntryController.prototype.exposedData = function () {
return {
openmct: this.openmct,
domainObject: this.domainObject,
currentEntryValue: this.currentEntryValue
};
};
EntryController.prototype.exposedMethods = function () {
return {
entryPosById: this.entryPosById,
textFocus: this.textFocus,
textBlur: this.textBlur,
formatTime: this.formatTime,
deleteEntry: this.deleteEntry,
dropOnEntry: this.dropOnEntry
};
};
return EntryController;
});

View File

@ -1,237 +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.
*****************************************************************************/
define([
'vue',
'./EntryController',
'./EmbedController',
'../../res/templates/notebook.html',
'../../res/templates/entry.html',
'../../res/templates/embed.html',
'../../../../ui/components/search.vue',
'../../../../ui/preview/PreviewAction',
'../../../../ui/mixins/object-link'
],
function (
Vue,
EntryController,
EmbedController,
NotebookTemplate,
EntryTemplate,
EmbedTemplate,
search,
PreviewAction,
objectLinkMixin
) {
function NotebookController(openmct, domainObject) {
this.openmct = openmct;
this.domainObject = domainObject;
this.entrySearch = '';
this.previewAction = new PreviewAction.default(openmct);
this.show = this.show.bind(this);
this.destroy = this.destroy.bind(this);
this.newEntry = this.newEntry.bind(this);
this.entryPosById = this.entryPosById.bind(this);
}
NotebookController.prototype.initializeVue = function (container) {
var self = this,
entryController = new EntryController(this.openmct, this.domainObject),
embedController = new EmbedController(this.openmct, this.domainObject);
this.container = container;
var notebookEmbed = {
inject:['openmct', 'domainObject'],
mixins:[objectLinkMixin.default],
props:['embed', 'entry'],
template: EmbedTemplate,
data: embedController.exposedData,
methods: embedController.exposedMethods(),
beforeMount: embedController.populateActionMenu(self.openmct, [self.previewAction])
};
var entryComponent = {
props:['entry'],
template: EntryTemplate,
components: {
'notebook-embed': notebookEmbed
},
data: entryController.exposedData,
methods: entryController.exposedMethods(),
mounted: self.focusOnEntry
};
var NotebookVue = Vue.extend({
provide: {openmct: self.openmct, domainObject: self.domainObject},
components: {
'notebook-entry': entryComponent,
'search': search.default
},
data: function () {
return {
entrySearch: self.entrySearch,
showTime: '0',
sortEntries: self.domainObject.defaultSort,
entries: self.domainObject.entries,
currentEntryValue: ''
};
},
computed: {
filteredAndSortedEntries() {
return this.sort(this.filterBySearch(this.entries, this.entrySearch), this.sortEntries);
}
},
methods: {
search(value) {
this.entrySearch = value;
},
newEntry: self.newEntry,
filterBySearch: self.filterBySearch,
sort: self.sort
},
template: NotebookTemplate
});
this.NotebookVue = new NotebookVue();
container.appendChild(this.NotebookVue.$mount().$el);
};
NotebookController.prototype.newEntry = function (event) {
this.NotebookVue.search('');
var date = Date.now(),
embed;
if (event.dataTransfer && event.dataTransfer.getData('openmct/domain-object-path')) {
var selectedObject = JSON.parse(event.dataTransfer.getData('openmct/domain-object-path'))[0],
selectedObjectId = selectedObject.identifier.key,
cssClass = this.openmct.types.get(selectedObject.type);
embed = {
type: selectedObjectId,
id: '' + date,
cssClass: cssClass,
name: selectedObject.name,
snapshot: ''
};
}
var entries = this.domainObject.entries,
lastEntryIndex = this.NotebookVue.sortEntries === 'newest' ? 0 : entries.length - 1,
lastEntry = entries[lastEntryIndex];
if (lastEntry === undefined || lastEntry.text || lastEntry.embeds.length) {
var createdEntry = {'id': 'entry-' + date, 'createdOn': date, 'embeds':[]};
if (embed) {
createdEntry.embeds.push(embed);
}
entries.push(createdEntry);
this.openmct.objects.mutate(this.domainObject, 'entries', entries);
} else {
lastEntry.createdOn = date;
if(embed) {
lastEntry.embeds.push(embed);
}
this.openmct.objects.mutate(this.domainObject, 'entries[entries.length-1]', lastEntry);
this.focusOnEntry.bind(this.NotebookVue.$children[lastEntryIndex+1])();
}
};
NotebookController.prototype.entryPosById = function (entryId) {
var foundId = -1;
this.domainObject.entries.forEach(function (element, index) {
if (element.id === entryId) {
foundId = index;
return;
}
});
return foundId;
};
NotebookController.prototype.focusOnEntry = function () {
if (!this.entry.text) {
this.$refs.contenteditable.focus();
}
};
NotebookController.prototype.filterBySearch = function (entryArray, filterString) {
if (filterString) {
var lowerCaseFilterString = filterString.toLowerCase();
return entryArray.filter(function (entry) {
if (entry.text) {
return entry.text.toLowerCase().includes(lowerCaseFilterString);
} else {
return false;
}
});
} else {
return entryArray;
}
};
NotebookController.prototype.sort = function (array, sortDirection) {
let oldest = (a,b) => {
if (a.createdOn < b.createdOn) {
return -1;
} else if (a.createdOn > b.createdOn) {
return 1;
} else {
return 0;
}
},
newest = (a,b) => {
if (a.createdOn < b.createdOn) {
return 1;
} else if (a.createdOn > b.createdOn) {
return -1;
} else {
return 0;
}
};
if (sortDirection === 'newest') {
return array.sort(newest);
} else {
return array.sort(oldest);
}
};
NotebookController.prototype.show = function (container) {
this.initializeVue(container);
};
NotebookController.prototype.destroy = function (container) {
this.NotebookVue.$destroy(true);
};
return NotebookController;
});

View File

@ -0,0 +1,195 @@
import objectLink from '../../../ui/mixins/object-link';
const TIME_BOUNDS = {
START_BOUND: 'tc.startBound',
END_BOUND: 'tc.endBound',
START_DELTA: 'tc.startDelta',
END_DELTA: 'tc.endDelta'
}
export const getHistoricLinkInFixedMode = (openmct, bounds, historicLink) => {
if (historicLink.includes('tc.mode=fixed')) {
return historicLink;
}
openmct.time.getAllClocks().forEach(clock => {
if (historicLink.includes(`tc.mode=${clock.key}`)) {
historicLink.replace(`tc.mode=${clock.key}`, 'tc.mode=fixed');
return;
}
});
const params = historicLink.split('&').map(param => {
if (param.includes(TIME_BOUNDS.START_BOUND)
|| param.includes(TIME_BOUNDS.START_DELTA)) {
param = `${TIME_BOUNDS.START_BOUND}=${bounds.start}`;
}
if (param.includes(TIME_BOUNDS.END_BOUND)
|| param.includes(TIME_BOUNDS.END_DELTA)) {
param = `${TIME_BOUNDS.END_BOUND}=${bounds.end}`;
}
return param;
});
return params.join('&');
}
export const getNotebookDefaultEntries = (notebookStorage, domainObject) => {
if (!notebookStorage || !domainObject) {
return null;
}
const defaultSection = notebookStorage.section;
const defaultPage = notebookStorage.page;
if (!defaultSection || !defaultPage) {
return null;
}
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
let section = entries[defaultSection.id];
if (!section) {
section = {};
entries[defaultSection.id] = section;
}
let page = entries[defaultSection.id][defaultPage.id];
if (!page) {
page = [];
entries[defaultSection.id][defaultPage.id] = [];
}
return entries[defaultSection.id][defaultPage.id];
}
export const createNewEmbed = (snapshotMeta, snapshot = '') => {
const {
bounds,
link,
objectPath,
openmct
} = snapshotMeta;
const domainObject = objectPath[0];
const domainObjectType = openmct.types.get(domainObject.type);
const cssClass = domainObjectType && domainObjectType.definition
? domainObjectType.definition.cssClass
: 'icon-object-unknown';
const date = Date.now();
const historicLink = link
? getHistoricLinkInFixedMode(openmct, bounds, link)
: objectLink.computed.objectLink.call({ objectPath, openmct });
const name = domainObject.name;
const type = domainObject.identifier.key;
return {
bounds,
createdOn: date,
cssClass,
domainObject,
historicLink,
id: 'embed-' + date,
name,
snapshot,
type,
objectPath: JSON.stringify(objectPath)
};
}
export const addNotebookEntry = (openmct, domainObject, notebookStorage, embed = null) => {
if (!openmct || !domainObject || !notebookStorage) {
return;
}
const date = Date.now();
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
if (!entries) {
return;
}
const embeds = embed
? [embed]
: [];
const defaultEntries = getNotebookDefaultEntries(notebookStorage, domainObject);
const id = `entry-${date}`;
defaultEntries.push({
id,
createdOn: date,
text: '',
embeds
});
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
return id;
}
export const getNotebookEntries = (domainObject, selectedSection, selectedPage) => {
if (!domainObject || !selectedSection || !selectedPage) {
return null;
}
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
let section = entries[selectedSection.id];
if (!section) {
return null;
}
let page = entries[selectedSection.id][selectedPage.id];
if (!page) {
return null;
}
return entries[selectedSection.id][selectedPage.id];
}
export const getEntryPosById = (entryId, domainObject, selectedSection, selectedPage) => {
if (!domainObject || !selectedSection || !selectedPage) {
return;
}
const entries = getNotebookEntries(domainObject, selectedSection, selectedPage);
let foundId = -1;
entries.forEach((element, index) => {
if (element.id === entryId) {
foundId = index;
return;
}
});
return foundId;
}
export const deleteNotebookEntries = (openmct, domainObject, selectedSection, selectedPage) => {
if (!domainObject || !selectedSection) {
return;
}
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
// Delete entire section
if (!selectedPage) {
delete entries[selectedSection.id];
return;
}
let section = entries[selectedSection.id];
if (!section) {
return;
}
delete entries[selectedSection.id][selectedPage.id];
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
}

View File

@ -0,0 +1,40 @@
const NOTEBOOK_LOCAL_STORAGE = 'notebook-storage';
export function clearDefaultNotebook() {
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, null);
}
export function getDefaultNotebook() {
const notebookStorage = window.localStorage.getItem(NOTEBOOK_LOCAL_STORAGE);
return JSON.parse(notebookStorage);
}
export function setDefaultNotebook(domainObject, section, page) {
const notebookMeta = {
name: domainObject.name,
identifier: domainObject.identifier
};
const notebookStorage = {
notebookMeta,
section,
page
}
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
}
export function setDefaultNotebookSection(section) {
const notebookStorage = getDefaultNotebook();
notebookStorage.section = section;
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
}
export function setDefaultNotebookPage(page) {
const notebookStorage = getDefaultNotebook();
notebookStorage.page = page;
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
}

View File

@ -0,0 +1,45 @@
import $ from 'zepto';
export const togglePopupMenu = (event, openmct) => {
event.preventDefault();
const body = $(document.body);
const container = $(event.target.parentElement.parentElement);
const classList = document.querySelector('body').classList;
const isPhone = Array.from(classList).includes('phone');
const isTablet = Array.from(classList).includes('tablet');
const initiatingEvent = isPhone || isTablet
? 'touchstart'
: 'mousedown';
const menu = container.find('.menu-element');
let dismissExistingMenu;
function dismiss() {
container.find('.hide-menu').append(menu);
body.off(initiatingEvent, menuClickHandler);
dismissExistingMenu = undefined;
}
function menuClickHandler(e) {
window.setTimeout(() => {
dismiss();
}, 100);
}
// Dismiss any menu which was already showing
if (dismissExistingMenu) {
dismissExistingMenu();
}
// ...and record the presence of this menu.
dismissExistingMenu = dismiss;
const popupService = openmct.$injector.get('popupService');
popupService.display(menu, [event.pageX,event.pageY], {
marginX: 0,
marginY: -50
});
body.on(initiatingEvent, menuClickHandler);
}

View File

@ -19,8 +19,8 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<span ng-controller="PlotController as controller"
class="abs holder holder-plot has-control-bar">
<div ng-controller="PlotController as controller"
class="c-plot holder holder-plot has-control-bar">
<div class="c-control-bar" ng-show="!controller.hideExportButtons">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
@ -50,4 +50,4 @@
the-x-axis="xAxis">
</mct-plot>
</div>
</span>
</div>

View File

@ -19,8 +19,8 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<span ng-controller="StackedPlotController as stackedPlot"
class="abs holder holder-plot has-control-bar t-plot-stacked">
<div ng-controller="StackedPlotController as stackedPlot"
class="c-plot c-plot--stacked holder holder-plot has-control-bar">
<div class="l-control-bar" ng-show="!stackedPlot.hideExportButtons">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
@ -57,4 +57,4 @@
<mct-overlay-plot domain-object="telemetryObject"></mct-overlay-plot>
</div>
</div>
</span>
</div>

View File

@ -143,7 +143,7 @@ define([
let strategy;
if (this.model.interpolate !== 'none') {
strategy = 'minMax';
strategy = 'minmax';
}
options = _.extend({}, { size: 1000, strategy, filters: this.filters }, options || {});
@ -377,8 +377,10 @@ define([
* @public
*/
updateFiltersAndRefresh: function (updatedFilters) {
if (this.filters) {
this.filters = updatedFilters;
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
this.filters = deepCopiedFilters;
this.reset();
if (this.unsubscribe) {
this.unsubscribe();
@ -386,7 +388,7 @@ define([
}
this.fetch();
} else {
this.filters = updatedFilters;
this.filters = deepCopiedFilters;
}
}
});

View File

@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@ -47,6 +47,8 @@ define([
'./goToOriginalAction/plugin',
'./clearData/plugin',
'./webPage/plugin',
'./condition/plugin',
'./conditionWidget/plugin',
'./themes/espresso',
'./themes/maelstrom',
'./themes/snow'
@ -77,6 +79,8 @@ define([
GoToOriginalAction,
ClearData,
WebPagePlugin,
ConditionPlugin,
ConditionWidgetPlugin,
Espresso,
Maelstrom,
Snow
@ -171,7 +175,7 @@ define([
plugins.SummaryWidget = SummaryWidget;
plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin;
plugins.Notebook = Notebook;
plugins.Notebook = Notebook.default;
plugins.DisplayLayout = DisplayLayoutPlugin.default;
plugins.FolderView = FolderView;
plugins.Tabs = Tabs;
@ -185,6 +189,8 @@ define([
plugins.Espresso = Espresso.default;
plugins.Maelstrom = Maelstrom.default;
plugins.Snow = Snow.default;
plugins.Condition = ConditionPlugin.default;
plugins.ConditionWidget = ConditionWidgetPlugin.default;
return plugins;
});

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