Compare commits

..

1 Commits

Author SHA1 Message Date
Joel McKinnon
a1978b7c52 added description computed property 2020-03-06 16:44:17 -08:00
180 changed files with 3587 additions and 9367 deletions

View File

@@ -1,5 +1,5 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
Open MCT, Copyright (c) 2014-2018, 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-2020, United States Government
* Open MCT, Copyright (c) 2014-2018, 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

@@ -0,0 +1,80 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>
/
</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>0/0</span>
</div>
</div>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div><div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage
generated by <a href="http://istanbul-js.org/" target="_blank">istanbul</a> at Wed Dec 11 2019 13:15:10 GMT-0800 (Pacific Standard Time)
</div>
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="sorter.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

View File

@@ -0,0 +1,158 @@
var addSorting = (function () {
"use strict";
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() { return document.querySelector('.coverage-summary'); }
// returns the thead element of the summary table
function getTableHeader() { return getTable().querySelector('thead tr'); }
// returns the tbody element of the summary table
function getTableBody() { return getTable().querySelector('tbody'); }
// returns the th element for nth column
function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; }
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML = colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function (a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function (a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function () {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i =0 ; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function () {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData(cols);
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

View File

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

View File

@@ -31,7 +31,6 @@ define([
period: 10,
offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0
};
@@ -53,8 +52,7 @@ define([
'period',
'offset',
'dataRateInHz',
'phase',
'randomness'
'phase'
];
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, data.randomness),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase)
}
});
nextStep += step;
@@ -99,7 +99,6 @@
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;
@@ -111,8 +110,8 @@
name: request.name,
utc: nextStep,
yesterday: nextStep - 60*60*24*1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
sin: sin(nextStep, period, amplitude, offset, phase),
cos: cos(nextStep, period, amplitude, offset, phase)
});
}
self.postMessage({
@@ -121,14 +120,14 @@
});
}
function cos(timestamp, period, amplitude, offset, phase, randomness) {
function cos(timestamp, period, amplitude, offset, phase) {
return amplitude *
Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + offset;
}
function sin(timestamp, period, amplitude, offset, phase, randomness) {
function sin(timestamp, period, amplitude, offset, phase) {
return amplitude *
Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + offset;
}
function sendError(error, message) {

View File

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

View File

@@ -43,7 +43,7 @@
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
);
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.Snow());
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.Generator());

View File

@@ -33,7 +33,7 @@ define(
var CONNECTED = {
text: "Connected",
glyphClass: "ok",
statusClass: "s-status-on",
statusClass: "s-status-ok",
description: "Connected to the domain object database."
},
DISCONNECTED = {

View File

@@ -32,7 +32,7 @@ define(
var CONNECTED = {
text: "Connected",
glyphClass: "ok",
statusClass: "s-status-on",
statusClass: "s-status-ok",
description: "Connected to the domain object database."
},
DISCONNECTED = {

View File

@@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* Open MCT, Copyright (c) 2014-2019, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@@ -265,7 +265,6 @@ define([
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 ? this.metadata.values.map(applyReasonableDefaults) : [];
this.valueMetadatas = this.metadata.values.map(applyReasonableDefaults);
}
/**

View File

@@ -81,24 +81,6 @@ 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

@@ -1,6 +1,6 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@@ -62,7 +62,6 @@ export default {
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.bounds = this.openmct.time.bounds();
this.limitEvaluator = this.openmct
.telemetry
@@ -77,7 +76,6 @@ export default {
);
this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.openmct.time.on('bounds', this.updateBounds);
this.timestampKey = this.openmct.time.timeSystem().key;
@@ -91,80 +89,43 @@ export default {
.telemetry
.subscribe(this.domainObject, this.updateValues);
this.requestHistory();
this.openmct
.telemetry
.request(this.domainObject, {strategy: 'latest'})
.then((array) => this.updateValues(array[array.length - 1]));
},
destroyed() {
this.stopWatchingMutation();
this.unsubscribe();
this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.openmct.time.off('bounds', this.updateBounds);
this.openmct.off('timeSystem', this.updateTimeSystem);
},
methods: {
updateValues(datum) {
let newTimestamp = this.formats[this.timestampKey].parse(datum),
update = false, limit;
this.timestamp = this.formats[this.timestampKey].format(datum);
this.value = this.formats[this.valueKey].format(datum);
if(this.inBounds(newTimestamp)) {
// if timestamp is set, need tocheck, else update
if(this.timestamp !== '---') {
let existingTimestamp = this.formats[this.timestampKey].parse(this.timestamp);
// if existing is in bounds, need to check, if not update
if(this.inBounds(existingTimestamp)) {
// race condition check
if(newTimestamp >= existingTimestamp) {
update = true;
}
} else {
update = true;
}
} else {
update = true;
}
if(update) {
this.timestamp = this.formats[this.timestampKey].format(datum);
this.value = this.formats[this.valueKey].format(datum);
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
if (limit) {
this.valueClass = limit.cssClass;
} else {
this.valueClass = '';
}
}
var limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
if (limit) {
this.valueClass = limit.cssClass;
} else {
this.valueClass = '';
}
},
requestHistory() {
this.openmct
.telemetry
.request(this.domainObject, {
start: this.bounds.start,
end: this.bounds.end,
strategy: 'latest'
})
.then((array) => this.updateValues(array[array.length - 1]));
},
updateName(name) {
this.name = name;
},
updateBounds(bounds, isTick) {
this.bounds = bounds;
if(!isTick) {
this.requestHistory();
}
},
inBounds(timestamp) {
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
},
updateTimeSystem(timeSystem) {
this.value = '---';
this.timestamp = '---';
this.valueClass = '';
this.timestampKey = timeSystem.key;
this.openmct
.telemetry
.request(this.domainObject, {strategy: 'latest'})
.then((array) => this.updateValues(array[array.length - 1]));
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);

View File

@@ -21,24 +21,22 @@
*****************************************************************************/
<template>
<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>
<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>
</template>
<script>

View File

@@ -30,7 +30,7 @@ define(
// DISCONNECTED: HTTP failed; maybe misconfigured, disconnected.
// PENDING: Still trying to connect, and haven't failed yet.
var CONNECTED = {
statusClass: "s-status-on"
statusClass: "s-status-ok"
},
PENDING = {
statusClass: "s-status-warning-lo"

View File

@@ -122,7 +122,7 @@ define(
it("indicates success if connection is nominal", function () {
jasmine.clock().tick(pluginOptions.interval + 1);
ajaxOptions.success();
expect(indicatorElement.classList.contains('s-status-on')).toBe(true);
expect(indicatorElement.classList.contains('s-status-ok')).toBe(true);
});
it("indicates an error when the server cannot be reached", function () {

View File

@@ -23,14 +23,16 @@
import EventEmitter from 'EventEmitter';
import uuid from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { evaluateResults } from './utils/evaluator';
import { getLatestTimestamp } from './utils/time';
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
import { TRIGGER } from "./utils/constants";
import {computeCondition} from "./utils/evaluator";
/*
* conditionConfiguration = {
* id: uuid,
* trigger: 'any'/'all'/'not','xor',
* identifier: {
* key: '',
* namespace: ''
* },
* trigger: 'any'/'all',
* criteria: [
* {
* telemetry: '',
@@ -46,67 +48,44 @@ 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 conditionConfiguration: {identifier: {domainObject.identifier},trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
* @param openmct
*/
constructor(conditionConfiguration, openmct, conditionManager) {
constructor(conditionConfiguration, openmct) {
super();
this.openmct = openmct;
this.conditionManager = conditionManager;
this.id = conditionConfiguration.id;
this.id = this.openmct.objects.makeKeyString(conditionConfiguration.identifier);
this.criteria = [];
this.criteriaResults = {};
this.result = undefined;
this.timeSystems = this.openmct.time.getAllTimeSystems();
if (conditionConfiguration.configuration.criteria) {
this.createCriteria(conditionConfiguration.configuration.criteria);
}
this.trigger = conditionConfiguration.configuration.trigger;
this.openmct.objects.get(this.id).then(obj => this.observeForChanges(obj));
}
getResult(datum) {
if (!datum || !datum.id) {
console.log('no data received');
return;
}
this.criteria.forEach(criterion => {
if (this.isAnyOrAllTelemetry(criterion)) {
criterion.getResult(datum, this.conditionManager.telemetryObjects);
} else {
criterion.getResult(datum);
}
});
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
observeForChanges(conditionDO) {
this.stopObservingForChanges = this.openmct.objects.observe(conditionDO, '*', this.update.bind(this));
}
isAnyOrAllTelemetry(criterion) {
return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
}
isTelemetryUsed(id) {
return this.criteria.some(criterion => {
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
});
}
update(conditionConfiguration) {
this.updateTrigger(conditionConfiguration.configuration.trigger);
this.updateCriteria(conditionConfiguration.configuration.criteria);
update(newDomainObject) {
this.updateTrigger(newDomainObject.configuration.trigger);
this.updateCriteria(newDomainObject.configuration.criteria);
}
updateTrigger(trigger) {
if (this.trigger !== trigger) {
this.trigger = trigger;
this.handleConditionUpdated();
}
}
generateCriterion(criterionConfiguration) {
return {
id: criterionConfiguration.id || uuid(),
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 || ''
@@ -124,24 +103,14 @@ export default class ConditionClass extends EventEmitter {
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);
}
let 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 = [];
}
@@ -170,11 +139,22 @@ export default class ConditionClass extends EventEmitter {
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
newCriterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
let criterion = found.item;
criterion.unsubscribe();
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
this.criteria.splice(found.index, 1, newCriterion);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id];
}
}
}
removeCriterion(id) {
if (this.destroyCriterion(id)) {
this.handleConditionUpdated();
}
}
@@ -182,12 +162,15 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(id);
if (found) {
let criterion = found.item;
criterion.off('criterionUpdated', (obj) => {
this.handleCriterionUpdated(obj);
});
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;
@@ -197,38 +180,38 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(criterion.id);
if (found) {
this.criteria[found.index] = criterion.data;
this.subscribe();
// TODO nothing is listening to this
this.emitEvent('conditionUpdated', {
trigger: this.trigger,
criteria: this.criteria
});
}
}
requestLADConditionResult() {
let latestTimestamp;
let criteriaResults = {};
const criteriaRequests = this.criteria
.map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects}));
handleCriterionResult(eventData) {
const id = eventData.id;
return Promise.all(criteriaRequests)
.then(results => {
results.forEach(resultObj => {
const { id, data, data: { result } } = resultObj;
if (this.findCriterion(id)) {
criteriaResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
return {
id: this.id,
data: Object.assign(
{},
latestTimestamp,
{ result: evaluateResults(Object.values(criteriaResults), this.trigger) }
)
};
});
if (this.findCriterion(id)) {
this.criteriaResults[id] = eventData.data.result;
}
this.handleConditionUpdated(eventData.data);
}
subscribe() {
// TODO it looks like on any single criterion update subscriptions fire for all criteria
this.criteria.forEach((criterion) => {
criterion.subscribe();
})
}
handleConditionUpdated(datum) {
// trigger an updated event so that consumers can react accordingly
this.evaluate();
this.emitEvent('conditionResultUpdated',
Object.assign({}, datum, { result: this.result })
);
}
getCriteria() {
@@ -244,7 +227,21 @@ export default class ConditionClass extends EventEmitter {
return success;
}
evaluate() {
this.result = computeCondition(this.criteriaResults, this.trigger === TRIGGER.ALL);
}
emitEvent(eventName, data) {
this.emit(eventName, {
id: this.id,
data: data
});
}
destroy() {
if (typeof this.stopObservingForChanges === 'function') {
this.stopObservingForChanges();
}
this.destroyCriteria();
}
}

View File

@@ -21,107 +21,107 @@
*****************************************************************************/
import Condition from "./Condition";
import { getLatestTimestamp } from './utils/time';
import uuid from "uuid";
import EventEmitter from 'EventEmitter';
export default class ConditionManager extends EventEmitter {
constructor(conditionSetDomainObject, openmct) {
constructor(domainObject, openmct) {
super();
this.openmct = openmct;
this.conditionSetDomainObject = conditionSetDomainObject;
this.timeSystems = this.openmct.time.getAllTimeSystems();
this.composition = this.openmct.composition.get(conditionSetDomainObject);
this.composition.on('add', this.subscribeToTelemetry, this);
this.composition.on('remove', this.unsubscribeFromTelemetry, this);
this.compositionLoad = this.composition.load();
this.subscriptions = {};
this.telemetryObjects = {};
this.testData = {conditionTestData: [], applied: false};
this.domainObject = domainObject;
this.timeAPI = this.openmct.time;
this.latestTimestamp = {};
this.instantiate = this.openmct.$injector.get('instantiate');
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.telemetryReceived.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];
this.updateConditionTelemetry();
}
initialize() {
this.conditionClassCollection = [];
if (this.conditionSetDomainObject.configuration.conditionCollection.length) {
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
this.initCondition(conditionConfiguration, index);
this.conditionResults = {};
this.observeForChanges(this.domainObject);
this.conditionCollection = [];
if (this.domainObject.configuration.conditionCollection.length) {
this.domainObject.configuration.conditionCollection.forEach((conditionConfigurationId, index) => {
this.openmct.objects.get(conditionConfigurationId).then((conditionConfiguration) => {
this.initCondition(conditionConfiguration, index)
});
});
} else {
this.addCondition(true);
}
}
updateConditionTelemetry() {
this.conditionClassCollection.forEach((condition) => condition.updateTelemetry());
observeForChanges(domainObject) {
//TODO: Observe only the conditionCollection property instead of the whole domainObject
this.stopObservingForChanges = this.openmct.objects.observe(domainObject, '*', this.handleConditionCollectionUpdated.bind(this));
}
updateCondition(conditionConfiguration, index) {
let condition = this.conditionClassCollection[index];
condition.update(conditionConfiguration);
this.conditionSetDomainObject.configuration.conditionCollection[index] = conditionConfiguration;
this.persistConditions();
handleConditionCollectionUpdated(newDomainObject) {
let oldConditionIdentifiers = this.domainObject.configuration.conditionCollection.map((conditionConfigurationId) => {
return this.openmct.objects.makeKeyString(conditionConfigurationId);
});
let newConditionIdentifiers = newDomainObject.configuration.conditionCollection.map((conditionConfigurationId) => {
return this.openmct.objects.makeKeyString(conditionConfigurationId);
});
this.domainObject = newDomainObject;
//check for removed conditions
oldConditionIdentifiers.forEach((identifier, index) => {
if (newConditionIdentifiers.indexOf(identifier) < 0) {
this.removeCondition(identifier);
}
});
let newConditionCount = this.domainObject.configuration.conditionCollection.length - this.conditionCollection.length;
for (let i = 0; i < newConditionCount; i++) {
let conditionConfigurationId = this.domainObject.configuration.conditionCollection[i];
this.openmct.objects.get(conditionConfigurationId).then((conditionConfiguration) => {
this.initCondition(conditionConfiguration, i);
});
}
}
initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this);
let condition = new Condition(conditionConfiguration, this.openmct);
condition.on('conditionResultUpdated', this.handleConditionResult.bind(this));
if (index !== undefined) {
this.conditionClassCollection.splice(index + 1, 0, condition);
this.conditionCollection.splice(index + 1, 0, condition);
} else {
this.conditionClassCollection.unshift(condition);
this.conditionCollection.unshift(condition);
}
//There are no criteria for a default condition and hence no subscriptions.
//Hence the conditionResult must be manually triggered for it.
if (conditionConfiguration.isDefault) {
this.handleConditionResult();
}
}
createCondition(conditionConfiguration) {
createConditionDomainObject(isDefault, conditionConfiguration) {
let conditionObj;
if (conditionConfiguration) {
conditionObj = {
...conditionConfiguration,
id: uuid(),
configuration: {
...conditionConfiguration.configuration,
name: `Copy of ${conditionConfiguration.configuration.name}`
name: `Copy of ${conditionConfiguration.name}`,
identifier: {
...this.domainObject.identifier,
key: uuid()
}
};
} else {
conditionObj = {
id: uuid(),
isDefault: isDefault,
type: 'condition',
identifier: {
...this.domainObject.identifier,
key: uuid()
},
configuration: {
name: 'Unnamed Condition',
name: isDefault ? 'Default' : 'Unnamed Condition',
output: 'false',
trigger: 'all',
criteria: [{
id: uuid(),
criteria: isDefault ? [] : [{
telemetry: '',
operation: '',
input: [],
@@ -131,205 +131,134 @@ export default class ConditionManager extends EventEmitter {
summary: ''
};
}
let conditionDomainObjectKeyString = this.openmct.objects.makeKeyString(conditionObj.identifier);
let newDomainObject = this.instantiate(conditionObj, conditionDomainObjectKeyString);
return conditionObj;
return newDomainObject.useCapability('adapter');
}
addCondition() {
this.createAndSaveCondition();
addCondition(isDefault, index) {
this.createAndSaveConditionDomainObject(!!isDefault, index);
}
cloneCondition(conditionConfiguration, index) {
let clonedConfig = JSON.parse(JSON.stringify(conditionConfiguration));
clonedConfig.configuration.criteria.forEach((criterion) => criterion.id = uuid());
this.createAndSaveCondition(index, clonedConfig);
cloneCondition(conditionConfigurationId, index) {
this.openmct.objects.get(conditionConfigurationId).then((conditionConfiguration) => {
this.createAndSaveConditionDomainObject(false, index, conditionConfiguration);
});
}
createAndSaveCondition(index, conditionConfiguration) {
const newCondition = this.createCondition(conditionConfiguration);
createAndSaveConditionDomainObject(isDefault, index, conditionConfiguration) {
let newConditionDomainObject = this.createConditionDomainObject(isDefault, conditionConfiguration);
//persist the condition domain object so that we can do an openmct.objects.get on it and only persist the identifier in the conditionCollection of conditionSet
this.openmct.objects.mutate(newConditionDomainObject, 'created', new Date());
if (index !== undefined) {
this.conditionSetDomainObject.configuration.conditionCollection.splice(index + 1, 0, newCondition);
this.domainObject.configuration.conditionCollection.splice(index + 1, 0, newConditionDomainObject.identifier);
} else {
this.conditionSetDomainObject.configuration.conditionCollection.unshift(newCondition);
this.domainObject.configuration.conditionCollection.unshift(newConditionDomainObject.identifier);
}
this.initCondition(newCondition, index);
this.persistConditions();
this.persist();
}
removeCondition(index) {
let condition = this.conditionClassCollection[index];
condition.destroy();
this.conditionClassCollection.splice(index, 1);
this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1);
this.persistConditions();
removeCondition(identifier) {
let found = this.findConditionById(identifier);
if (found) {
let index = found.index;
let condition = this.conditionCollection[index];
let conditionIdAsString = condition.id;
condition.destroyCriteria();
condition.off('conditionResultUpdated', this.handleConditionResult.bind(this));
this.conditionCollection.splice(index, 1);
this.domainObject.configuration.conditionCollection.splice(index, 1);
if (this.conditionResults[conditionIdAsString] !== undefined) {
delete this.conditionResults[conditionIdAsString];
}
this.persist();
this.handleConditionResult();
}
}
findConditionById(id) {
return this.conditionClassCollection.find(conditionClass => conditionClass.id === id);
findConditionById(identifier) {
let found;
for (let i=0, ii=this.conditionCollection.length; i < ii; i++) {
if (this.conditionCollection[i].id === this.openmct.objects.makeKeyString(identifier)) {
found = {
item: this.conditionCollection[i],
index: i
};
break;
}
}
return found;
}
//this.$set(this.conditionCollection, reorderEvent.newIndex, oldConditions[reorderEvent.oldIndex]);
reorderConditions(reorderPlan) {
let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection);
let oldConditions = Array.from(this.domainObject.configuration.conditionCollection);
let newCollection = [];
reorderPlan.forEach((reorderEvent) => {
let item = oldConditions[reorderEvent.oldIndex];
newCollection.push(item);
this.conditionSetDomainObject.configuration.conditionCollection = newCollection;
this.domainObject.configuration.conditionCollection = newCollection;
});
this.persistConditions();
this.persist();
}
getCurrentCondition() {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length-1];
handleConditionResult(resultObj) {
let conditionCollection = this.domainObject.configuration.conditionCollection;
let currentConditionIdentifier = conditionCollection[conditionCollection.length-1];
if (resultObj) {
let idAsString = this.openmct.objects.makeKeyString(resultObj.id);
if (this.findConditionById(idAsString)) {
this.conditionResults[idAsString] = resultObj.data.result;
}
this.updateTimestamp(resultObj.data);
}
for (let i = 0; i < conditionCollection.length - 1; i++) {
const condition = this.findConditionById(conditionCollection[i].id)
if (condition.result) {
let conditionIdAsString = this.openmct.objects.makeKeyString(conditionCollection[i]);
if (this.conditionResults[conditionIdAsString]) {
//first condition to be true wins
currentCondition = conditionCollection[i];
currentConditionIdentifier = conditionCollection[i];
break;
}
}
return currentCondition;
}
getCurrentConditionLAD(conditionResults) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length-1];
for (let i = 0; i < conditionCollection.length - 1; i++) {
if (conditionResults[conditionCollection[i].id]) {
//first condition to be true wins
currentCondition = conditionCollection[i];
break;
}
}
return currentCondition;
}
requestLADConditionSetOutput() {
if (!this.conditionClassCollection.length) {
return Promise.resolve([]);
}
return this.compositionLoad.then(() => {
let latestTimestamp;
let conditionResults = {};
const conditionRequests = this.conditionClassCollection
.map(condition => condition.requestLADConditionResult());
return Promise.all(conditionRequests)
.then((results) => {
results.forEach(resultObj => {
const { id, data, data: { result } } = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
const currentCondition = this.getCurrentConditionLAD(conditionResults);
return Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
latestTimestamp
);
});
});
}
isTelemetryUsed(id) {
for(const condition of this.conditionClassCollection) {
if (condition.isTelemetryUsed(id)) {
return true;
}
}
return false;
}
telemetryReceived(id, datum) {
if (!this.isTelemetryUsed(id)) {
return;
}
const normalizedDatum = this.createNormalizedDatum(datum, id);
const timeSystemKey = this.openmct.time.timeSystem().key;
let timestamp = {};
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
this.conditionClassCollection.forEach(condition => {
condition.getResult(normalizedDatum);
});
const currentCondition = this.getCurrentCondition();
this.emit('conditionSetResultUpdated',
Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
timestamp
this.openmct.objects.get(currentConditionIdentifier).then((obj) => {
this.emit('conditionSetResultUpdated',
Object.assign(
{
output: obj.configuration.output,
id: this.domainObject.identifier,
conditionId: currentConditionIdentifier
},
this.latestTimestamp
)
)
)
});
}
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;
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];
}
}
return data;
});
}
createNormalizedDatum(telemetryDatum, id) {
const normalizedDatum = Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((datum, metadatum) => {
const testValue = this.getTestData(metadatum);
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
return datum;
}, {});
normalizedDatum.id = id;
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);
persist() {
this.openmct.objects.mutate(this.domainObject, 'configuration.conditionCollection', this.domainObject.configuration.conditionCollection);
}
destroy() {
this.composition.off('add', this.subscribeToTelemetry, this);
this.composition.off('remove', this.unsubscribeFromTelemetry, this);
Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe());
delete this.subscriptions;
if(this.stopObservingForChanges) {
if (typeof this.stopObservingForChanges === 'function') {
this.stopObservingForChanges();
}
this.conditionClassCollection.forEach((condition) => {
this.conditionCollection.forEach((condition) => {
condition.off('conditionResultUpdated', this.handleConditionResult);
condition.destroy();
})
}

View File

@@ -27,13 +27,6 @@ describe('ConditionManager', () => {
let conditionMgr;
let mockListener;
let openmct = {};
let mockCondition = {
isDefault: true,
id: '1234-5678',
configuration: {
criteria: []
}
};
let conditionSetDomainObject = {
identifier: {
namespace: "",
@@ -42,14 +35,17 @@ describe('ConditionManager', () => {
type: "conditionSet",
location: "mine",
configuration: {
conditionCollection: [
mockCondition
]
conditionCollection: []
}
};
let mockConditionDomainObject = {
isDefault: true,
type: 'condition',
identifier: {
namespace: '',
key: '1234-5678'
}
};
let mockComposition;
let loader;
let mockTimeSystems;
function mockAngularComponents() {
let mockInjector = jasmine.createSpyObj('$injector', ['get']);
@@ -59,7 +55,7 @@ describe('ConditionManager', () => {
let mockDomainObject = {
useCapability: function () {
return mockCondition;
return mockConditionDomainObject;
}
};
mockInstantiate.and.callFake(function () {
@@ -74,60 +70,29 @@ describe('ConditionManager', () => {
openmct.$injector = mockInjector;
}
beforeEach(function () {
beforeAll(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);
resolve(mockConditionDomainObject);
}));
openmct.objects.makeKeyString.and.returnValue(conditionSetDomainObject.identifier.key);
openmct.objects.observe.and.returnValue(function () {});
openmct.objects.mutate.and.returnValue(function () {});
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
mockListener = jasmine.createSpy('mockListener');
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.on('telemetryReceived', 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);
expect(conditionMgr.domainObject.configuration.conditionCollection.length).toEqual(1);
let defaultConditionIdentifier = conditionMgr.domainObject.configuration.conditionCollection[0];
expect(defaultConditionIdentifier).toEqual(mockConditionDomainObject.identifier);
});
});

View File

@@ -28,6 +28,7 @@ describe('ConditionSetCompositionPolicy', () => {
let testTelemetryObject;
let openmct = {};
let parentDomainObject;
let composition;
beforeAll(function () {
testTelemetryObject = {
@@ -56,6 +57,7 @@ describe('ConditionSetCompositionPolicy', () => {
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject']);
policy = new ConditionSetCompositionPolicy(openmct);
parentDomainObject = {};
composition = {};
});
it('returns true for object types that are not conditionSets', function () {

View File

@@ -1,25 +1,3 @@
/*****************************************************************************
* 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;
@@ -43,21 +21,12 @@ export default class ConditionSetMetadataProvider {
}
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,
format: 'string',
hints: {
range: 1
}

View File

@@ -1,86 +1,29 @@
/*****************************************************************************
* 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';
supportsRequest(domainObject, options) {
return false;
}
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);
let conditionManager = new ConditionManager(domainObject, this.openmct);
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];
return function unsubscribe() {
conditionManager.off('conditionSetResultUpdated');
conditionManager.destroy();
}
}
}

View File

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

@@ -30,7 +30,7 @@ export default class ConditionSetViewProvider {
this.openmct = openmct;
this.name = 'Conditions View';
this.key = 'conditionSet.view';
this.cssClass = 'icon-conditional';
this.cssClass = 'icon-conditional'; // TODO: replace with class for new icon
}
canView(domainObject) {

View File

@@ -25,22 +25,15 @@ import {TRIGGER} from "./utils/constants";
import TelemetryCriterion from "./criterion/TelemetryCriterion";
let openmct = {},
mockListener,
testConditionDefinition,
testTelemetryObject,
conditionObj,
conditionManager,
mockTelemetryReceived,
mockTimeSystems;
conditionObj;
describe("The condition", function () {
beforeEach (() => {
conditionManager = jasmine.createSpyObj('conditionManager',
['on']
);
mockTelemetryReceived = jasmine.createSpy('listener');
conditionManager.on('telemetryReceived', mockTelemetryReceived);
mockListener = jasmine.createSpy('listener');
testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"},
type: "test-object",
@@ -61,9 +54,6 @@ describe("The condition", function () {
}]
}
};
conditionManager.telemetryObjects = {
"test-object": testTelemetryObject
};
openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']);
openmct.objects.get.and.returnValue(new Promise(function (resolve, reject) {
resolve(testTelemetryObject);
@@ -73,23 +63,13 @@ describe("The condition", function () {
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values);
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
testConditionDefinition = {
id: '123-456',
configuration: {
name: 'mock condition',
output: 'mock output',
trigger: TRIGGER.ANY,
criteria: [
{
id: '1234-5678-9999-0000',
operation: 'equalTo',
input: ['0'],
input: false,
metadata: 'value',
telemetry: testTelemetryObject.identifier
}
@@ -99,12 +79,14 @@ describe("The condition", function () {
conditionObj = new Condition(
testConditionDefinition,
openmct,
conditionManager
openmct
);
conditionObj.on('conditionUpdated', mockListener);
});
it("generates criteria with the correct properties", function () {
it("generates criteria with an id", function () {
const testCriterion = testConditionDefinition.configuration.criteria[0];
let criterion = conditionObj.generateCriterion(testCriterion);
expect(criterion.id).toBeDefined();

View File

@@ -23,24 +23,19 @@
import EventEmitter from 'EventEmitter';
export default class StyleRuleManager extends EventEmitter {
constructor(styleConfiguration, openmct, callback) {
constructor(conditionalStyleConfiguration, openmct) {
super();
this.openmct = openmct;
this.callback = callback;
if (styleConfiguration) {
this.initialize(styleConfiguration);
if (styleConfiguration.conditionSetIdentifier) {
this.subscribeToConditionSet();
} else {
this.applyStaticStyle();
}
if (conditionalStyleConfiguration && conditionalStyleConfiguration.conditionSetIdentifier) {
this.initialize(conditionalStyleConfiguration);
this.subscribeToConditionSet();
}
}
initialize(styleConfiguration) {
this.conditionSetIdentifier = styleConfiguration.conditionSetIdentifier;
this.staticStyle = styleConfiguration.staticStyle;
this.updateConditionStylesMap(styleConfiguration.styles || []);
initialize(conditionalStyleConfiguration) {
this.conditionSetIdentifier = conditionalStyleConfiguration.conditionSetIdentifier;
this.defaultStyle = conditionalStyleConfiguration.defaultStyle;
this.updateConditionStylesMap(conditionalStyleConfiguration.styles || []);
}
subscribeToConditionSet() {
@@ -48,24 +43,17 @@ export default class StyleRuleManager extends EventEmitter {
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 || {});
updateConditionalStyleConfig(conditionalStyleConfiguration) {
if (!conditionalStyleConfiguration || !conditionalStyleConfiguration.conditionSetIdentifier) {
this.destroy();
} else {
let isNewConditionSet = !this.conditionSetIdentifier ||
!this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier);
this.initialize(styleConfiguration);
this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, conditionalStyleConfiguration.conditionSetIdentifier);
this.initialize(conditionalStyleConfiguration);
//Only resubscribe if the conditionSet has changed.
if (isNewConditionSet) {
this.subscribeToConditionSet();
@@ -76,52 +64,38 @@ export default class StyleRuleManager extends EventEmitter {
updateConditionStylesMap(conditionStyles) {
let conditionStyleMap = {};
conditionStyles.forEach((conditionStyle) => {
if (conditionStyle.conditionId) {
conditionStyleMap[conditionStyle.conditionId] = conditionStyle.style;
} else {
conditionStyleMap.static = conditionStyle.style;
}
const identifier = this.openmct.objects.makeKeyString(conditionStyle.conditionIdentifier);
conditionStyleMap[identifier] = conditionStyle.style;
});
this.conditionalStyleMap = conditionStyleMap;
}
handleConditionSetResultUpdated(resultData) {
let foundStyle = this.conditionalStyleMap[resultData.conditionId];
let identifier = this.openmct.objects.makeKeyString(resultData.conditionId);
let foundStyle = this.conditionalStyleMap[identifier];
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] = '__no_value';
});
if (this.currentStyle !== this.defaultStyle) {
this.currentStyle = this.defaultStyle;
}
}
this.updateDomainObjectStyle();
}
updateDomainObjectStyle() {
this.emit('conditionalStyleUpdated', this.currentStyle)
}
destroy() {
this.applyStaticStyle();
this.currentStyle = this.defaultStyle;
this.updateDomainObjectStyle();
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
delete this.stopProvidingTelemetry;
this.conditionSetIdentifier = undefined;
}

View File

@@ -1,186 +1,177 @@
/*****************************************************************************
* 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.
*****************************************************************************/
* 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-condition-h"
:class="{ 'is-drag-target': draggingOver }"
@dragover.prevent
@drop.prevent="dropCondition($event, conditionIndex)"
@dragenter="dragEnter($event, conditionIndex)"
@dragleave="dragLeave($event, conditionIndex)"
>
<div class="c-condition-h__drop-target"></div>
<div v-if="isEditing"
class="c-condition c-condition--edit"
<div v-if="isEditing">
<div v-if="domainObject"
class="c-c-editui__conditions c-c-container__container c-c__drag-wrapper"
:class="['widget-condition', { 'widget-condition--current': currentConditionIdentifier && (currentConditionIdentifier.key === conditionIdentifier.key) }]"
>
<!-- 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"
<div class="title-bar">
<span class="c-c__menu-hamburger"
:class="{ 'is-enabled': !domainObject.isDefault }"
:draggable="!domainObject.isDefault"
@dragstart="dragStart"
@dragend="dragEnd"
@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
class="is-enabled flex-elem"
:class="['c-c__disclosure-triangle', { 'c-c__disclosure-triangle--expanded': expanded }]"
@click="expanded = !expanded"
></span>
<span class="c-condition__name">{{ condition.configuration.name }}</span>
<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 class="condition-summary">
<span class="condition-name">{{ domainObject.configuration.name }}</span>
<!-- TODO: description should be derived from criteria -->
<span class="condition-description">{{ getDescription }}</span>
</div>
<span v-if="!domainObject.isDefault"
class="is-enabled c-c__duplicate"
@click="cloneCondition"
></span>
<span v-if="!domainObject.isDefault"
class="is-enabled c-c__trash"
@click="removeCondition"
></span>
</div>
<div v-if="expanded"
class="c-condition__definition c-cdef"
class="condition-config-edit widget-condition-content c-sw-editui__conditions-wrapper holder widget-conditions-wrapper flex-elem expanded"
>
<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"
@change="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"
@change="persist"
>
</span>
</span>
<div v-if="!condition.isDefault"
class="c-cdef__match-and-criteria"
<div id="conditionArea"
class="c-c-editui__condition widget-conditions"
>
<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="criterion.id"
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 class="c-c-condition">
<div class="c-c-condition__ui l-compact-form l-widget-condition has-local-controls">
<div>
<ul class="t-widget-condition-config">
<li>
<label>Condition Name</label>
<span class="controls">
<input v-model="domainObject.configuration.name"
class="t-condition-input__name"
type="text"
@blur="persist"
>
</span>
</li>
<li>
<label>Output</label>
<span class="controls">
<select v-model="selectedOutputSelection"
@change="setOutputValue"
>
<option value="">- Select Output -</option>
<option v-for="option in outputOptions"
:key="option"
:value="option"
>
{{ initCap(option) }}
</option>
</select>
<input v-if="selectedOutputSelection === outputOptions[2]"
v-model="domainObject.configuration.output"
class="t-condition-name-input"
type="text"
@blur="persist"
>
</span>
</li>
</ul>
<div v-if="!domainObject.isDefault"
class="widget-condition-content expanded"
>
<ul class="t-widget-condition-config">
<li class="has-local-controls t-condition">
<label>Match when</label>
<span class="controls">
<select v-model="domainObject.configuration.trigger"
@change="persist"
>
<option value="all">all criteria are met</option>
<option value="any">any criteria are met</option>
</select>
</span>
</li>
</ul>
<ul v-if="telemetry.length"
class="t-widget-condition-config"
>
<li v-for="(criterion, index) in domainObject.configuration.criteria"
:key="index"
class="has-local-controls t-condition"
>
<Criterion :telemetry="telemetry"
:criterion="criterion"
:index="index"
:trigger="domainObject.configuration.trigger"
:is-default="domainObject.configuration.criteria.length === 1"
@persist="persist"
/>
<div class="c-c__criterion-controls">
<span class="is-enabled c-c__duplicate"
@click="cloneCriterion(index)"
></span>
<span v-if="!(domainObject.configuration.criteria.length === 1)"
class="is-enabled c-c__trash"
@click="removeCriterion(index)"
></span>
</div>
</li>
</ul>
<div class="holder c-c-button-wrapper align-left">
<span class="c-c-label-spacer"></span>
<button
class="c-c-button c-c-button--minor add-criteria-button"
@click="addCriteria"
>
<span class="c-c-button__label">Add Criteria</span>
</button>
</div>
</div>
</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"
</div>
<div v-else>
<div v-if="domainObject"
id="conditionArea"
class="c-cs-ui__conditions"
:class="['widget-condition', { 'widget-condition--current': currentConditionIdentifier && (currentConditionIdentifier.key === conditionIdentifier.key) }]"
>
<!-- Browse view -->
<div class="c-condition__header">
<span class="c-condition__name">
{{ condition.configuration.name }}
<div class="title-bar">
<span class="condition-name">
{{ domainObject.configuration.name }}
</span>
<span class="c-condition__output">
Output: {{ condition.configuration.output }}
<span class="condition-output">
Output: {{ domainObject.configuration.output }}
</span>
</div>
<div class="c-condition__summary">
<condition-description :show-label="false"
:condition="condition"
/>
<div class="condition-config">
<span class="condition-description">
{{ getDescription }}
</span>
</div>
</div>
</div>
@@ -188,18 +179,19 @@
<script>
import Criterion from './Criterion.vue';
import ConditionDescription from "./ConditionDescription.vue";
import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
import uuid from 'uuid';
import { OPERATIONS } from '../utils/operations';
export default {
inject: ['openmct'],
components: {
Criterion,
ConditionDescription
Criterion
},
props: {
condition: {
conditionIdentifier: {
type: Object,
required: true
},
currentConditionIdentifier: {
type: Object,
required: true
},
@@ -215,64 +207,73 @@ export default {
type: Array,
required: true,
default: () => []
},
isDragging: {
type: Boolean,
default: false
},
moveIndex: {
type: Number,
default: 0
}
},
data() {
return {
domainObject: this.domainObject,
currentCriteria: this.currentCriteria,
expanded: true,
trigger: 'all',
selectedOutputSelection: '',
outputOptions: ['false', 'true', 'string'],
criterionIndex: 0,
draggingOver: false,
isDefault: this.condition.isDefault
criterionIndex: 0
};
},
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;
getDescription: function () {
let config = this.domainObject.configuration;
if (!config.criteria.length) {
return 'When all else fails';
} else {
let description = '';
if (config.criteria.length === 1) {
if (config.criteria[0].operation && config.criteria[0].input.length) {
description += `When ${config.criteria[0].telemetry.name} value ${this.findDescription(config.criteria[0].operation, config.criteria[0].input)}`
}
} else {
let conjunction = '';
config.criteria.forEach((criterion, index) => {
if (criterion.operation && criterion.input.length) {
if (index !== config.criteria.length - 1 && (criterion.operation && criterion.input.length)) {
conjunction = config.trigger === 'all' ? 'and ' : 'or ';
} else {
conjunction = '';
}
description += `${criterion.telemetry.name} value ${this.findDescription(criterion.operation, criterion.input)} ${conjunction}`
}
});
}
return description;
}
return false;
}
},
destroyed() {
this.destroy();
},
mounted() {
this.setOutputSelection();
this.openmct.objects.get(this.conditionIdentifier).then((domainObject => {
this.domainObject = domainObject;
this.initialize();
}));
},
methods: {
findDescription(operation, values) {
for (let i=0, ii= OPERATIONS.length; i < ii; i++) {
if (operation === OPERATIONS[i].name) {
console.log('OPERATIONS[i].getDescription()', OPERATIONS[i].getDescription(values));
return OPERATIONS[i].getDescription(values);
}
}
return null;
},
initialize() {
this.setOutputSelection();
},
setOutputSelection() {
let conditionOutput = this.condition.configuration.output;
let conditionOutput = this.domainObject.configuration.output;
if (conditionOutput) {
if (conditionOutput !== 'false' && conditionOutput !== 'true') {
this.selectedOutputSelection = 'string';
@@ -283,88 +284,59 @@ export default {
},
setOutputValue() {
if (this.selectedOutputSelection === 'string') {
this.condition.configuration.output = '';
this.domainObject.configuration.output = '';
} else {
this.condition.configuration.output = this.selectedOutputSelection;
this.domainObject.configuration.output = this.selectedOutputSelection;
}
this.persist();
},
addCriteria() {
const criteriaObject = {
id: uuid(),
telemetry: '',
operation: '',
input: '',
metadata: ''
};
this.condition.configuration.criteria.push(criteriaObject);
this.domainObject.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('.c-condition-h'), 0, 0);
e.dataTransfer.setDragImage(e.target.closest('.c-c-container__container'), 0, 0);
this.$emit('setMoveIndex', this.conditionIndex);
},
dragEnd(event) {
this.dragStarted = false;
event.dataTransfer.clearData();
this.$emit('dragComplete');
},
dropCondition(event, targetIndex) {
if (!this.isDragging) { return }
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.isValidTarget(targetIndex)) {
this.dragElement = undefined;
this.draggingOver = false;
this.$emit('dropCondition', targetIndex);
}
},
dragEnter(event, targetIndex) {
if (!this.isDragging) { return }
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.isValidTarget(targetIndex)) {
this.dragElement = event.target.parentElement;
this.draggingOver = true;
}
},
dragLeave(event) {
if (event.target.parentElement === this.dragElement) {
this.draggingOver = false;
this.dragElement = undefined;
}
},
isValidTarget(targetIndex) {
return this.moveIndex !== targetIndex;
dragStop(e) {
e.dataTransfer.clearData();
},
destroy() {
},
removeCondition(ev) {
this.$emit('removeCondition', this.conditionIndex);
this.$emit('removeCondition', this.conditionIdentifier);
},
cloneCondition(ev) {
this.$emit('cloneCondition', {
condition: this.condition,
index: this.conditionIndex
identifier: this.conditionIdentifier,
index: Number(ev.target.closest('.widget-condition').getAttribute('data-condition-index'))
});
},
removeCriterion(index) {
this.condition.configuration.criteria.splice(index, 1);
this.persist();
this.domainObject.configuration.criteria.splice(index, 1);
this.persist()
},
cloneCriterion(index) {
const clonedCriterion = JSON.parse(JSON.stringify(this.condition.configuration.criteria[index]));
clonedCriterion.id = uuid();
this.condition.configuration.criteria.splice(index + 1, 0, clonedCriterion);
this.persist();
const clonedCriterion = {...this.domainObject.configuration.criteria[index]};
this.domainObject.configuration.criteria.splice(index + 1, 0, clonedCriterion);
this.persist()
},
hasTelemetry(identifier) {
// TODO: check parent domainObject.composition.hasTelemetry
return this.currentCriteria && identifier;
},
persist() {
this.$emit('updateCondition', {
condition: this.condition,
index: this.conditionIndex
});
this.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration);
},
initCap(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
initCap: function (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
}
}

View File

@@ -22,54 +22,60 @@
<template>
<section id="conditionCollection"
:class="{ 'is-expanded': expanded }"
class="c-cs__ui_section"
>
<div class="c-cs__header c-section__header">
<div class="c-cs__ui__header">
<span class="c-cs__ui__header-label">Conditions</span>
<span
class="c-disclosure-triangle c-tree__item__view-control is-enabled"
:class="{ 'c-disclosure-triangle--expanded': expanded }"
class="is-enabled flex-elem"
:class="['c-cs__disclosure-triangle', { 'c-cs__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"
class="c-cs__ui_content"
>
<div v-show="isEditing"
class="hint"
:class="{ 's-status-icon-warning-lo': !telemetryObjs.length }"
class="help"
>
<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>
<span v-if="!telemetryObjs.length">Drag telemetry into Condition Set in order to add conditions.</span>
<span v-else>The first condition to match is the one that wins. Drag conditions to rearrange.</span>
</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"
:class="{ 'is-active-dragging': isDragging }"
>
<Condition v-for="(condition, index) in conditionCollection"
:key="condition.id"
:condition="condition"
:condition-index="index"
:telemetry="telemetryObjs"
:is-editing="isEditing"
:move-index="moveIndex"
:is-dragging="isDragging"
@updateCondition="updateCondition"
@removeCondition="removeCondition"
@cloneCondition="cloneCondition"
@setMoveIndex="setMoveIndex"
@dragComplete="dragComplete"
@dropCondition="dropCondition"
/>
<div class="holder add-condition-button-wrapper align-left">
<button
v-show="isEditing"
id="addCondition"
class="c-cs-button c-cs-button--major add-condition-button"
:class="{ 'is-disabled': !telemetryObjs.length}"
:disabled="!telemetryObjs.length"
@click="addCondition"
>
<span class="c-cs-button__label">Add Condition</span>
</button>
</div>
<div class="c-c__condition-collection">
<ul class="c-c__container-holder">
<li v-for="(conditionIdentifier, index) in conditionCollection"
:key="conditionIdentifier.key"
>
<div v-if="isEditing"
class="c-c__drag-ghost"
@drop.prevent="dropCondition"
@dragenter="dragEnter"
@dragleave="dragLeave"
@dragover.prevent
></div>
<Condition :condition-identifier="conditionIdentifier"
:current-condition-identifier="currentConditionIdentifier"
:condition-index="index"
:telemetry="telemetryObjs"
:is-editing="isEditing"
@removeCondition="removeCondition"
@cloneCondition="cloneCondition"
@setMoveIndex="setMoveIndex"
/>
</li>
</ul>
</div>
</div>
</section>
@@ -85,50 +91,28 @@ export default {
Condition
},
props: {
isEditing: Boolean,
testData: {
type: Object,
required: true,
default: () => {
return {
applied: false,
conditionTestInputs: []
}
}
}
isEditing: Boolean
},
data() {
return {
expanded: true,
parentKeyString: this.openmct.objects.makeKeyString(this.domainObject.identifier),
conditionCollection: [],
conditionResults: {},
conditions: [],
currentConditionIdentifier: this.currentConditionIdentifier || {},
telemetryObjs: [],
moveIndex: undefined,
isDragging: false,
defaultOutput: undefined,
dragCounter: 0
moveIndex: Number,
isDragging: false
};
},
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) {
if (typeof this.stopObservingForChanges === 'function') {
this.stopObservingForChanges();
}
},
@@ -138,31 +122,22 @@ export default {
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();
this.observeForChanges();
},
methods: {
handleConditionSetResultUpdated(data) {
this.$emit('conditionSetResultUpdated', data)
},
observeForChanges() {
this.stopObservingForChanges = this.openmct.objects.observe(this.domainObject, 'configuration.conditionCollection', (newConditionCollection) => {
this.conditionCollection = newConditionCollection;
this.updateDefaultCondition();
this.stopObservingForChanges = this.openmct.objects.observe(this.domainObject, '*', (newDomainObject) => {
this.conditionCollection = newDomainObject.configuration.conditionCollection;
});
},
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(targetIndex) {
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) {
@@ -188,13 +163,22 @@ export default {
}
this.reorder(reorderPlan);
},
dragComplete() {
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) => {
@@ -206,23 +190,20 @@ export default {
this.telemetryObjs.splice(index, 1);
}
},
addCondition() {
this.conditionManager.addCondition();
addCondition(event, isDefault, index) {
this.conditionManager.addCondition(!!isDefault, index);
},
updateCondition(data) {
this.conditionManager.updateCondition(data.condition, data.index);
updateCurrentCondition(identifier) {
this.currentConditionIdentifier = identifier;
},
removeCondition(index) {
this.conditionManager.removeCondition(index);
removeCondition(identifier) {
this.conditionManager.removeCondition(identifier);
},
reorder(reorderPlan) {
this.conditionManager.reorderConditions(reorderPlan);
},
cloneCondition(data) {
this.conditionManager.cloneCondition(data.condition, data.index);
},
updateTestData() {
this.conditionManager.updateTestData(this.testData);
cloneCondition(condition) {
this.conditionManager.cloneCondition(condition.identifier, condition.index);
}
}
}

View File

@@ -1,154 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<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) {
if (!criterion.telemetry) {
let description = `Unknown ${criterion.metadata} ${this.getOperatorText(criterion.operation, criterion.input)}`;
this.criterionDescriptions.splice(index, 0, description);
} else if (criterion.telemetry === 'all' || criterion.telemetry === 'any') {
const telemetryDescription = criterion.telemetry === 'all' ? 'All telemetry' : 'Any telemetry';
let description = `${telemetryDescription} ${criterion.metadata} ${this.getOperatorText(criterion.operation, criterion.input)}`;
this.criterionDescriptions.splice(index, 0, description);
} else {
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

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

@@ -21,34 +21,23 @@
*****************************************************************************/
<template>
<div class="c-cs">
<section class="c-cs__current-output c-section">
<div class="c-cs__content c-cs__current-output-value">
<span class="c-cs__current-output-value__label">Current Output</span>
<span class="c-cs__current-output-value__value">
<template v-if="currentConditionOutput">
<div class="c-cs-edit w-condition-set">
<div class="c-sw-edit__ui holder">
<section id="current-output">
<div class="c-cs__ui__header">
<span class="c-cs__ui__header-label">Current Output</span>
</div>
<div class="c-cs__ui_content">
<span v-if="currentConditionOutput"
class="current-output"
>
{{ currentConditionOutput }}
</template>
<template v-else>
{{ defaultConditionOutput }}
</template>
</span>
</div>
</section>
<div class="c-cs__test-data-and-conditions-w">
<TestData class="c-cs__test-data"
:is-editing="isEditing"
:test-data="testData"
:telemetry="telemetryObjs"
@updateTestData="updateTestData"
/>
<ConditionCollection class="c-cs__conditions"
:is-editing="isEditing"
:test-data="testData"
@conditionSetResultUpdated="updateCurrentOutput"
@updateDefaultOutput="updateDefaultOutput"
@telemetryUpdated="updateTelemetry"
/>
</span>
<span v-else>No output selected</span>
</div>
</section>
<TestData :is-editing="isEditing" />
<ConditionCollection :is-editing="isEditing" />
</div>
</div>
</template>
@@ -68,31 +57,25 @@ export default {
},
data() {
return {
currentConditionOutput: '',
defaultConditionOutput: '',
telemetryObjs: [],
testData: {}
currentConditionOutput: ''
}
},
mounted() {
this.conditionSetIdentifier = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.testData = {
applied: false,
conditionTestInputs: this.domainObject.configuration.conditionTestData || []
};
this.provideTelemetry();
},
beforeDestroy() {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
}
},
methods: {
updateCurrentOutput(currentConditionResult) {
this.currentConditionOutput = currentConditionResult.output;
},
updateDefaultOutput(output) {
this.currentConditionOutput = output;
},
updateTelemetry(telemetryObjs) {
this.telemetryObjs = telemetryObjs;
},
updateTestData(testData) {
this.testData = testData;
provideTelemetry() {
this.stopProvidingTelemetry = this.openmct.telemetry
.subscribe(this.domainObject, output => { this.updateCurrentOutput(output); });
}
}
};

View File

@@ -1,38 +1,12 @@
/*****************************************************************************
* 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"
<div>
<label>{{ setRowLabel }}</label>
<span class="t-configuration">
<span class="controls">
<select 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"
@@ -42,10 +16,9 @@
</select>
</span>
<span v-if="criterion.telemetry"
class="c-cdef__control"
class="controls"
>
<select ref="metadataSelect"
v-model="criterion.metadata"
<select v-model="criterion.metadata"
@change="updateOperations"
>
<option value="">- Select Field -</option>
@@ -58,10 +31,10 @@
</select>
</span>
<span v-if="criterion.telemetry && criterion.metadata"
class="c-cdef__control"
class="controls"
>
<select v-model="criterion.operation"
@change="updateInputVisibilityAndValues"
@change="updateOperationInputVisibility"
>
<option value="">- Select Comparison -</option>
<option v-for="option in filteredOps"
@@ -71,34 +44,15 @@
{{ option.text }}
</option>
</select>
<template v-if="!enumerations.length">
<span v-for="(item, inputIndex) in inputCount"
:key="inputIndex"
class="c-cdef__control__inputs"
<span v-for="(item, inputIndex) in inputCount"
:key="inputIndex"
>
<input v-model="criterion.input[inputIndex]"
class="t-condition-name-input"
type="text"
@blur="persist"
>
<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 v-if="inputIndex < inputCount-1">and</span>
</span>
</span>
</span>
@@ -107,7 +61,6 @@
<script>
import { OPERATIONS } from '../utils/operations';
import { INPUT_TYPES } from '../utils/operations';
export default {
inject: ['openmct'],
@@ -132,13 +85,12 @@ export default {
},
data() {
return {
telemetryMetadataOptions: [],
telemetryMetadata: {},
telemetryMetadataOptions: {},
operations: OPERATIONS,
inputCount: 0,
rowLabel: '',
operationFormat: '',
enumerations: [],
inputTypes: INPUT_TYPES
operationFormat: ''
}
},
computed: {
@@ -147,145 +99,70 @@ export default {
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
return [...this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1)];
}
},
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();
getOperationFormat() {
this.telemetryMetadata.valueMetadatas.forEach((value, index) => {
if (value.key === this.criterion.metadata) {
let valueMetadata = this.telemetryMetadataOptions[index];
if (valueMetadata.enumerations !== undefined) {
this.operationFormat = 'enum';
} else if (valueMetadata.hints.hasOwnProperty('range')) {
this.operationFormat = 'number';
} else if (valueMetadata.hints.hasOwnProperty('domain')) {
this.operationFormat = 'number';
} else if (valueMetadata.key === 'name') {
this.operationFormat = 'string';
} else {
this.operationFormat = 'string';
}
}
}
},
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 (ev) {this.clearInputs()}
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.openmct.objects.get(this.criterion.telemetry).then((telemetryObject) => {
this.criterion.telemetry.name = telemetryObject.name;
this.telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryMetadataOptions = this.telemetryMetadata.values();
this.updateOperations();
this.updateOperationInputVisibility();
});
} else {
this.criterion.metadata = '';
}
},
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();
}
if (ev) {this.clearInputs()}
this.getOperationFormat();
this.persist();
},
updateInputVisibilityAndValues(ev) {
updateOperationInputVisibility(ev) {
if (ev) {
this.clearDependentFields();
this.persist();
this.criterion.input = [];
this.inputCount = 0;
}
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 = []}
}
}
if (!this.inputCount) {
this.criterion.input = [];
}
this.persist();
},
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;
}
clearInputs() {
this.criterion.operation = '';
this.criterion.input = [];
this.inputCount = 0;
},
updateMetadataSelection() {
this.updateOperationInputVisibility();
},
persist() {
this.$emit('persist', this.criterion);

View File

@@ -23,98 +23,54 @@
<template>
<section v-show="isEditing"
id="test-data"
:class="{ 'is-expanded': expanded }"
class="test-data"
>
<div class="c-cs__header c-section__header">
<div class="c-cs__ui__header">
<span class="c-cs__ui__header-label">Test Data</span>
<span
class="c-disclosure-triangle c-tree__item__view-control is-enabled"
:class="{ 'c-disclosure-triangle--expanded': expanded }"
class="is-enabled flex-elem"
:class="['c-cs__disclosure-triangle', { 'c-cs__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"
class="c-cs__ui_content"
>
<div class="c-cs__test-data__controls 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"
<label class="c-toggle-switch">
<input
type="checkbox"
:checked="isApplied"
@change="applyTestData"
>
<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>
<span class="c-toggle-switch__slider"></span>
<span>Apply Test Data</span>
</label>
<div class="t-test-data-config">
<div class="c-cs-editui__conditions widget-condition">
<form>
<label>
<span>Set</span>
<select>
<option>- Select Input -</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>
</label>
<span class="is-enabled flex-elem c-cs__duplicate"></span>
<span class="is-enabled flex-elem c-cs__trash"></span>
</form>
</div>
<div class="c-cs-editui__conditions widget-condition">
<form>
<label>
<span>Set</span>
<select>
<option>- Select Input -</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-cs-test__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>
</label>
<span class="is-enabled c-cs__duplicate"></span>
<span class="is-enabled c-cs__trash"></span>
</form>
</div>
</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>
@@ -123,113 +79,17 @@
export default {
inject: ['openmct'],
props: {
isEditing: Boolean,
telemetry: {
type: Array,
required: true,
default: () => []
},
testData: {
type: Object,
required: true,
default: () => {
return {
applied: false,
conditionTestInputs: []
}
}
}
isEditing: Boolean
},
data() {
return {
expanded: true,
isApplied: false,
testInputs: [],
telemetryMetadataOptions: {}
isApplied: true
};
},
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
});
applyTestData(ev) {
this.$emit('change', ev.target.checked);
}
}
}

View File

@@ -0,0 +1,155 @@
.c-cs-edit {
padding: 0;
}
section {
padding-bottom: 5px;
}
.c-cs__ui__header {
background-color: #D0D1D3;
padding: 0.4em 0.6em;
text-transform: uppercase;
font-size: 0.8em;
font-weight: bold;
color: #7C7D80;
display: flex;
justify-content: stretch;
align-items: center;
}
.c-cs__ui__header-label {
display: inline-block;
width: 100%;
}
.c-cs__ui_content {
padding: 0.4em;
}
.c-cs-ui__label {
color: #333;
}
.c-cs__ui_content .help {
font-style: italic;
padding: 0.4em 0;
}
.current-output {
text-transform: uppercase;
font-weight: bold;
margin: 0.4em 0.6em;
}
.condition-output {
color: #999;
}
.condition-description {
color: #333;
}
.checkbox.custom {
display: flex;
align-items: center;
margin-left: 0.6em;
}
.checkbox.custom span {
display: inline-block;
margin-left: 0.6em;
}
.t-test-data-config {
margin-top: 5px;
}
.c-c__condition-collection.is-disabled {
opacity: 0.5;
}
.widget-condition form {
padding: 0.5em;
display: flex;
align-items: center;
justify-content: stretch;
}
.widget-condition form label {
flex-grow: 1;
margin-left: 0;
}
.widget-condition form input {
min-height: 24px;
}
.c-cs-button[class*="--major"],
.c-cs-button[class*='is-active'],
.c-cs-button--menu[class*="--major"],
.c-cs-button--menu[class*='is-active'] {
border: solid 1px #0B427C;
background-color: #4778A3;
padding: 0.2em 0.6em;
margin: 0.4em;
font-weight: bold;
color: #eee;
border-radius: 6px;
&.is-disabled {
opacity: 0.5;
}
}
.c-cs__disclosure-triangle,
.c-cs__menu-hamburger,
.c-cs__duplicate,
.c-cs__trash {
$d: 8px;
color: $colorDisclosureCtrl;
width: $d;
visibility: hidden;
&.is-enabled {
cursor: pointer;
visibility: visible;
&:hover {
color: $colorDisclosureCtrlHov;
}
&:before {
$s: .5;
display: block;
font-family: symbolsfont;
font-size: 1rem * $s;
}
}
}
.c-cs__disclosure-triangle {
&:before {
content: $glyph-icon-arrow-right;
}
&--expanded {
&:before {
transform: rotate(90deg);
}
}
}
.c-cs__duplicate {
margin-right: 0.3em;
&:before {
content: $glyph-icon-duplicate;
}
}
.c-cs__trash {
&:before {
content: $glyph-icon-trash;
}
}

View File

@@ -0,0 +1,208 @@
.widget-condition {
background-color: #eee;
margin: 0 0 0.33em;
border-radius: 3px;
&--current {
background-color: #DEECFA;
}
}
.c-c-editui__conditions.widget-condition {
margin: 0;
}
.c-c-button-wrapper {
border-top: solid 1px #ccc;
padding: 2px;
}
.c-c-label-spacer {
display: inline-block;
width: 90px;
}
.c-c-button[class*="--minor"],
.c-c-button[class*='is-active'],
.c-c-button--menu[class*="--minor"],
.c-c-button--menu[class*='is-active'] {
border: solid 1px #666;
background-color: #fff;
padding: 0.1em 0.4em;
margin: 0.4em;
font-weight: normal;
color: #666;
border-radius: 6px;
}
.title-bar {
display: flex;
align-items: center;
justify-content: stretch;
padding: 0.4em 0;
}
.title-bar span {
margin-right: 0.6em;
}
.title-bar span.c-c__duplicate,
.title-bar span.c-c__trash{
margin-right: 0;
margin-left: 0.3em;
}
.widget-condition > div {
margin: 0 0.4em;
}
.condition-name {
font-weight: bold;
}
.condition-summary .condition-description {
color: #999;
}
.condition-summary {
flex-grow: 1;
}
.condition-config {
padding: 0.3em 0;
}
.widget-condition form label {
flex-grow: 1;
margin-left: 0;
}
.l-widget-condition {
padding: 0;
}
.l-compact-form ul li {
padding: 0;
}
.widget-condition-content.expanded {
margin: 0 3px;
}
.widget-condition-content.expanded ul li {
border-top: solid 1px #ccc;
padding: 2px;
}
.l-compact-form ul li .controls {
display: inline-flex;
flex-grow: inherit;
padding: 2px 0;
}
.l-compact-form ul li > label {
display: block;
width: 90px;
min-width: 90px;
text-align: right;
line-height: 20px;
}
.l-compact-form ul li .controls input[type="text"],
.l-compact-form ul li .controls input[type="search"],
.l-compact-form ul li .controls input[type="number"],
.l-compact-form ul li .controls button,
.l-compact-form ul li .controls select {
min-height: 20px;
}
.condition-config-edit {
padding: 3px 0;
}
.c-c__disclosure-triangle,
.c-c__menu-hamburger,
.c-c__duplicate,
.c-c__trash {
$d: 8px;
color: $colorDisclosureCtrl;
width: $d;
visibility: hidden;
&.is-enabled {
cursor: pointer;
visibility: visible;
&:hover {
color: $colorDisclosureCtrlHov;
}
&:before {
$s: .5;
display: block;
font-family: symbolsfont;
font-size: 1rem * $s;
}
}
}
.c-c__disclosure-triangle {
&:before {
content: $glyph-icon-arrow-right;
}
&--expanded {
&:before {
transform: rotate(90deg);
}
}
}
.c-c__menu-hamburger {
&:active {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}
&:before {
content: $glyph-icon-menu-hamburger;
}
}
.c-c__duplicate {
&:before {
content: $glyph-icon-duplicate;
}
}
.c-c__trash {
&:before {
content: $glyph-icon-trash;
}
}
.c-c__drag-ghost {
width: 100%;
min-height: 0.33em;
&.dragging {
min-height: 2em;
border: solid 1px blue;
background-color: lightblue;
border-radius: 2px;
}
}
.c-c__criterion-controls {
width: 28px;
.c-c__duplicate,
.c-c__trash {
display: inline-block;
}
}

View File

@@ -1,311 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/***************************** DRAGGING */
.is-active-dragging {
.c-condition-h__drop-target {
height: 3px;
margin-bottom: $interiorMarginSm;
}
}
.c-condition-h {
&__drop-target {
border-radius: $controlCr;
height: 0;
min-height: 0;
transition: background-color, height;
transition-duration: 150ms;
}
&.is-drag-target {
.c-condition > * {
pointer-events: none; // Keeps the JS drop handler from being intercepted by internal elements
}
.c-condition-h__drop-target {
background-color: rgba($colorKey, 0.7);
}
}
}
.c-cs {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
/************************** CONDITION SET LAYOUT */
&__current-output {
flex: 0 0 auto;
}
&__test-data-and-conditions-w {
display: flex;
flex-direction: column;
flex: 1 1 auto;
height: 100%;
overflow: hidden;
}
&__test-data,
&__conditions {
flex: 0 0 auto;
overflow: hidden;
}
&__test-data {
flex: 0 0 auto;
max-height: 50%;
&.is-expanded {
margin-bottom: $interiorMargin * 4;
}
}
&__conditions {
flex: 1 1 auto;
> * + * {
margin-top: $interiorMarginSm;
}
}
&__content {
display: flex;
flex-direction: column;
flex: 0 1 auto;
overflow: hidden;
> * {
flex: 0 0 auto;
overflow: hidden;
+ * {
margin-top: $interiorMarginSm;
}
}
.c-button {
align-self: start;
}
}
.is-editing & {
// Add some space to kick away from blue editing border indication
padding: $interiorMargin;
}
section {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__conditions-h {
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.hint {
padding: $interiorMarginSm;
}
/************************** SPECIFIC ITEMS */
&__current-output-value {
flex-direction: row;
align-items: baseline;
padding: 0 $interiorMargin $interiorMarginLg $interiorMargin;
> * {
padding: $interiorMargin 0; // Must do this to align label and value
}
&__label {
color: $colorInspectorSectionHeaderFg;
opacity: 0.9;
text-transform: uppercase;
}
&__value {
$p: $interiorMargin * 3;
font-size: 1.25em;
margin-left: $interiorMargin;
padding-left: $p;
padding-right: $p;
background: rgba(black, 0.2);
border-radius: 5px;
}
}
}
/***************************** CONDITIONS AND TEST DATUM ELEMENTS */
.c-condition,
.c-test-datum {
@include discreteItem();
display: flex;
padding: $interiorMargin;
line-height: 170%; // Aligns text with controls like selects
}
.c-cdef,
.c-cs-test {
&__controls {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
white-space: nowrap;
}
}
.c-condition {
flex-direction: column;
min-width: 400px;
> * + * {
margin-top: $interiorMarginSm;
}
&--browse {
.c-condition__summary {
border-top: 1px solid $colorInteriorBorder;
padding-top: $interiorMargin;
}
}
/***************************** HEADER */
&__header {
$h: 22px;
display: flex;
align-items: start;
align-content: stretch;
overflow: hidden;
min-height: $h;
line-height: $h;
> * {
flex: 0 0 auto;
+ * {
margin-left: $interiorMarginSm;
}
}
}
&__drag-grippy {
transform: translateY(50%);
}
&__name {
font-weight: bold;
align-self: baseline; // Fixes bold line-height offset problem
}
&__output,
&__summary {
flex: 1 1 auto;
}
}
/***************************** CONDITION DEFINITION, EDITING */
.c-cdef {
display: grid;
grid-row-gap: $interiorMarginSm;
grid-column-gap: $interiorMargin;
grid-auto-columns: min-content 1fr max-content;
align-items: start;
min-width: 150px;
margin-left: 29px;
overflow: hidden;
&__criteria,
&__match-and-criteria {
display: contents;
}
&__label {
grid-column: 1;
text-align: right;
white-space: nowrap;
}
&__separator {
grid-column: 1 / span 3;
}
&__controls {
align-items: flex-start;
grid-column: 2;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
grid-column: 3;
}
}
.c-c__drag-ghost {
width: 100%;
min-height: $interiorMarginSm;
&.dragging {
min-height: 5em;
background-color: lightblue;
border-radius: 2px;
}
}
/***************************** TEST DATA */
.c-cs__test-data {
&__controls {
flex: 0 0 auto;
}
}
.c-cs-tests {
flex: 0 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.c-cs-test {
> * + * {
margin-left: $interiorMargin;
}
}

View File

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

@@ -1,172 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<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,38 @@
<template>
<div>
<div v-if="condition"
class="holder c-c-button-wrapper align-left"
>
<div>{{ condition.configuration.name }}</div>
</div>
</div>
</template>
<script>
export default {
components: {
},
inject: [
'openmct'
],
props: {
conditionIdentifier: {
type: Object,
required: true
}
},
data() {
return {
condition: null
}
},
destroyed() {
},
mounted() {
this.openmct.objects.get(this.conditionIdentifier).then((conditionDomainObject) => {
this.condition = conditionDomainObject;
});
}
}
</script>

View File

@@ -1,400 +1,109 @@
/*****************************************************************************
* 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>
<div>
<div v-if="!conditionalStyles.length"
class="holder c-c-button-wrapper align-left">
<button
class="c-c-button c-c-button--minor add-criteria-button"
@click="addConditionSet">
<span class="c-c-button__label">Use conditional styling</span>
</button>
</div>
<div v-else>
<div class="holder c-c-button-wrapper align-left">
<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>
class="c-c-button c-c-button--minor add-criteria-button"
@click="removeConditionSet">
<span class="c-c-button__label">Remove 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>
<ul>
<li v-for="conditionStyle in conditionalStyles"
:key="conditionStyle.conditionIdentifier.key">
<conditional-style :condition-identifier="conditionStyle.conditionIdentifier"
:condition-style="conditionStyle.style"/>
</li>
</ul>
</div>
</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";
import {getApplicableStylesForItem} from "@/plugins/condition/utils/styleUtils";
import ConditionalStyle from "./ConditionalStyle.vue";
export default {
name: 'ConditionalStylesView',
components: {
ConditionDescription,
ConditionError,
StyleEditor
ConditionalStyle
},
inject: [
'openmct',
'selection'
'domainObject',
'layoutItem'
],
data() {
return {
conditionalStyles: [],
staticStyle: undefined,
conditionSetDomainObject: undefined,
isEditing: this.openmct.editor.isEditing(),
conditions: undefined,
conditionsLoaded: false,
navigateToPath: ''
conditionalStyles: []
}
},
destroyed() {
this.removeListeners();
},
mounted() {
this.itemId = '';
this.getDomainObjectFromSelection();
this.previewAction = new PreviewAction(this.openmct);
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
let objectStyles = this.itemId ? this.domainObject.configuration.objectStyles[this.itemId] : this.domainObject.configuration.objectStyles;
this.initializeStaticStyle(objectStyles);
if (objectStyles && objectStyles.conditionSetIdentifier) {
this.openmct.objects.get(objectStyles.conditionSetIdentifier).then(this.initialize);
this.conditionalStyles = objectStyles.styles;
}
} else {
this.initializeStaticStyle();
if (this.layoutItem) {
//TODO: Handle layout items
}
if (this.domainObject.configuration) {
this.defautStyle = this.domainObject.configuration.defaultStyle;
if (this.domainObject.configuration.conditionalStyle) {
this.conditionalStyles = this.domainObject.configuration.conditionalStyle.styles || [];
}
}
this.openmct.editor.on('isEditing', this.setEditState);
},
methods: {
isItemType(type, item) {
return item && (item.type === type);
},
getDomainObjectFromSelection() {
let layoutItem;
let domainObject;
if (this.selection[0].length > 1) {
//If there are more than 1 items in the this.selection[0] list, the first one could either be a sub domain object OR a layout drawing control.
//The second item in the this.selection[0] list is the container object (usually a layout)
layoutItem = this.selection[0][0].context.layoutItem;
const item = this.selection[0][0].context.item;
this.canHide = true;
if (item &&
(!layoutItem || (this.isItemType('subobject-view', layoutItem)))) {
domainObject = item;
} else {
domainObject = this.selection[0][1].context.item;
if (layoutItem) {
this.itemId = layoutItem.id;
}
}
} else {
domainObject = this.selection[0][0].context.item;
}
this.domainObject = domainObject;
this.initialStyles = getApplicableStylesForItem(domainObject, layoutItem);
this.$nextTick(() => {
this.removeListeners();
if (this.domainObject) {
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
this.stopObservingItems = this.openmct.objects.observe(this.domainObject, 'configuration.items', this.updateDomainObjectItemStyles);
}
});
},
removeListeners() {
if (this.stopObserving) {
this.stopObserving();
}
if (this.stopObservingItems) {
this.stopObservingItems();
}
},
initialize(conditionSetDomainObject) {
//If there are new conditions in the conditionSet we need to set those styles to default
this.conditionSetDomainObject = conditionSetDomainObject;
this.enableConditionSetNav();
addConditionSet() {
//TODO: this.conditionSetIdentifier will be set by the UI before calling this
this.conditionSetIdentifier = {
namespace: '',
key: 'bb0f61ad-268d-4d3e-bb30-90ca4a2053c4'
};
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.conditionSetIdentifier = '';
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);
},
updateDomainObjectItemStyles(newItems) {
//check that all items that have been styles still exist. Otherwise delete those styles
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
let itemsToRemove = [];
let keys = Object.keys(domainObjectStyles);
keys.forEach((key) => {
if ((key !== 'styles') &&
(key !== 'staticStyle') &&
(key !== 'conditionSetIdentifier')) {
if (!(newItems.find(item => item.id === key))) {
itemsToRemove.push(key);
}
}
});
if (itemsToRemove.length) {
this.removeItemStyles(itemsToRemove, domainObjectStyles);
}
},
removeItemStyles(itemIds, domainObjectStyles) {
itemIds.forEach(itemId => {
if (domainObjectStyles[itemId]) {
domainObjectStyles[itemId] = undefined;
delete domainObjectStyles[this.itemId];
}
});
if (_.isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
this.persist(domainObjectStyles);
this.persist(undefined);
},
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)
const backgroundColors = [{backgroundColor: 'red'},{backgroundColor: 'orange'}, {backgroundColor: 'blue'}];
this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {
conditionSetDomainObject.configuration.conditionCollection.forEach((identifier, index) => {
this.conditionalStyles.push({
conditionIdentifier: identifier,
style: backgroundColors[index]
});
}
});
this.persist({
defaultStyle: this.defaultStyle || {backgroundColor: 'inherit'},
conditionSetIdentifier: this.conditionSetIdentifier,
styles: this.conditionalStyles
});
});
//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;
if (staticStyle) {
this.staticStyle = {
style: Object.assign({}, this.initialStyles, staticStyle.style)
};
} else {
this.staticStyle = {
style: Object.assign({}, this.initialStyles)
};
}
},
findStyleByConditionId(id) {
return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id);
},
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
for(let i=0, ii=this.conditionalStyles.length; i < ii; i++) {
if (this.openmct.objects.makeKeyString(this.conditionalStyles[i].conditionIdentifier) === this.openmct.objects.makeKeyString(id)) {
return {
index: i,
item: this.conditionalStyles[i]
};
}
}
return domainObjectStyles;
},
getCondition(id) {
return this.conditions ? this.conditions[id] : {};
updateConditionalStyle(conditionIdentifier, style) {
let found = this.findStyleByConditionId(conditionIdentifier);
if (found) {
this.conditionalStyles[found.index].style = style;
}
this.persist(undefined);
},
persist(style) {
this.openmct.objects.mutate(this.domainObject, 'configuration.objectStyles', style);
persist(conditionalStyle) {
this.openmct.objects.mutate(this.domainObject, 'configuration.conditionalStyle', conditionalStyle);
}
}
}

View File

@@ -1,269 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-inspector__styles c-inspect-styles">
<div class="c-inspect-styles__header">
Object Style
</div>
<div class="c-inspect-styles__content">
<div v-if="isStaticAndConditionalStyles"
class="c-inspect-styles__mixed-static-and-conditional u-alert u-alert--block u-alert--with-icon"
>
Your selection includes one or more items that use Conditional Styling. Applying a static style below will replace any Conditional Styling with the new choice.
</div>
<div v-if="staticStyle"
class="c-inspect-styles__style"
>
<style-editor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
:mixed-styles="mixedStyles"
@persist="updateStaticStyle"
/>
</div>
</div>
</div>
</template>
<script>
import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionalStyleForItem } from "@/plugins/condition/utils/styleUtils";
export default {
name: 'MultiSelectStylesView',
components: {
StyleEditor
},
inject: [
'openmct',
'selection'
],
data() {
return {
staticStyle: undefined,
isEditing: this.openmct.editor.isEditing(),
mixedStyles: [],
isStaticAndConditionalStyles: false
}
},
destroyed() {
this.removeListeners();
},
mounted() {
this.items = [];
this.previewAction = new PreviewAction(this.openmct);
this.getObjectsAndItemsFromSelection();
this.initializeStaticStyle();
this.openmct.editor.on('isEditing', this.setEditState);
},
methods: {
isItemType(type, item) {
return item && (item.type === type);
},
hasConditionalStyles(domainObject, id) {
return getConditionalStyleForItem(domainObject, id) !== undefined;
},
getObjectsAndItemsFromSelection() {
let domainObject;
let subObjects = [];
//multiple selection
let itemInitialStyles = [];
let itemStyle;
this.selection.forEach((selectionItem) => {
const item = selectionItem[0].context.item;
const layoutItem = selectionItem[0].context.layoutItem;
if (item && this.isItemType('subobject-view', layoutItem)) {
subObjects.push(item);
itemStyle = getApplicableStylesForItem(item);
if (!this.isStaticAndConditionalStyles) {
this.isStaticAndConditionalStyles = this.hasConditionalStyles(item);
}
} else {
domainObject = selectionItem[1].context.item;
itemStyle = getApplicableStylesForItem(domainObject, layoutItem || item);
this.items.push({
id: layoutItem.id,
applicableStyles: itemStyle
});
if (!this.isStaticAndConditionalStyles) {
this.isStaticAndConditionalStyles = this.hasConditionalStyles(domainObject, layoutItem.id);
}
}
itemInitialStyles.push(itemStyle);
});
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
this.initialStyles = styles;
this.mixedStyles = mixedStyles;
this.domainObject = domainObject;
this.removeListeners();
if (this.domainObject) {
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
this.stopObservingItems = this.openmct.objects.observe(this.domainObject, 'configuration.items', this.updateDomainObjectItemStyles);
}
subObjects.forEach(this.registerListener);
},
updateDomainObjectItemStyles(newItems) {
//check that all items that have been styles still exist. Otherwise delete those styles
let keys = Object.keys(this.domainObject.configuration.objectStyles || {});
keys.forEach((key) => {
if ((key !== 'styles') &&
(key !== 'staticStyle') &&
(key !== 'conditionSetIdentifier')) {
if (!(newItems.find(item => item.id === key))) {
this.removeItemStyles(key);
}
}
});
},
registerListener(domainObject) {
let id = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.domainObjectsById) {
this.domainObjectsById = {};
}
if (!this.domainObjectsById[id]) {
this.domainObjectsById[id] = domainObject;
this.observeObject(domainObject, id);
}
},
observeObject(domainObject, id) {
let unobserveObject = this.openmct.objects.observe(domainObject, '*', function (newObject) {
this.domainObjectsById[id] = JSON.parse(JSON.stringify(newObject));
}.bind(this));
this.unObserveObjects.push(unobserveObject);
},
removeListeners() {
if (this.stopObserving) {
this.stopObserving();
}
if (this.stopObservingItems) {
this.stopObservingItems();
}
if (this.unObserveObjects) {
this.unObserveObjects.forEach((unObserveObject) => {
unObserveObject();
});
}
this.unObserveObjects = [];
},
removeItemStyles(itemId) {
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (itemId && domainObjectStyles[itemId]) {
domainObjectStyles[itemId] = undefined;
delete domainObjectStyles[this.itemId];
if (_.isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
this.persist(this.domainObject, domainObjectStyles);
}
},
removeConditionalStyles(domainObjectStyles, itemId) {
if (itemId) {
domainObjectStyles[itemId].conditionSetIdentifier = undefined;
delete domainObjectStyles[itemId].conditionSetIdentifier;
domainObjectStyles[itemId].styles = undefined;
delete domainObjectStyles[itemId].styles;
} else {
domainObjectStyles.conditionSetIdentifier = undefined;
delete domainObjectStyles.conditionSetIdentifier;
domainObjectStyles.styles = undefined;
delete domainObjectStyles.styles;
}
},
setEditState(isEditing) {
this.isEditing = isEditing;
},
initializeStaticStyle() {
this.staticStyle = {
style: Object.assign({}, this.initialStyles)
};
},
updateStaticStyle(staticStyle, property) {
//update the static style for each of the layoutItems as well as each sub object item
this.staticStyle = staticStyle;
this.persist(this.domainObject, this.getDomainObjectStyle(this.domainObject, property, this.items));
if (this.domainObjectsById) {
const keys = Object.keys(this.domainObjectsById);
keys.forEach(key => {
let domainObject = this.domainObjectsById[key];
this.persist(domainObject, this.getDomainObjectStyle(domainObject, property));
});
}
this.isStaticAndConditionalStyles = false;
let foundIndex = this.mixedStyles.indexOf(property);
if (foundIndex > -1) {
this.mixedStyles.splice(foundIndex, 1);
}
},
getDomainObjectStyle(domainObject, property, items) {
let domainObjectStyles = (domainObject.configuration && domainObject.configuration.objectStyles) || {};
if (items) {
items.forEach(item => {
let itemStaticStyle = {};
if (domainObjectStyles[item.id] && domainObjectStyles[item.id].staticStyle) {
itemStaticStyle = domainObjectStyles[item.id].staticStyle.style;
}
Object.keys(item.applicableStyles).forEach(key => {
if (property === key) {
itemStaticStyle[key] = this.staticStyle.style[key];
}
});
if (this.isStaticAndConditionalStyles) {
this.removeConditionalStyles(domainObjectStyles, item.id);
}
if (_.isEmpty(itemStaticStyle)) {
itemStaticStyle = undefined;
domainObjectStyles[item.id] = undefined;
} else {
domainObjectStyles[item.id] = Object.assign({}, { staticStyle: { style: itemStaticStyle } });
}
});
} else {
if (!domainObjectStyles.staticStyle) {
domainObjectStyles.staticStyle = {
style: {}
}
}
if (this.isStaticAndConditionalStyles) {
this.removeConditionalStyles(domainObjectStyles);
}
domainObjectStyles.staticStyle.style[property] = this.staticStyle.style[property];
}
return domainObjectStyles;
},
persist(domainObject, style) {
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
}
}
}
</script>

View File

@@ -1,215 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-style">
<span :class="[
{ 'is-style-invisible': styleItem.style.isStyleInvisible },
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
class="c-style-thumb"
>
<span class="c-style-thumb__text"
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
>
ABC
</span>
</span>
<span class="c-toolbar">
<toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
class="c-style__toolbar-button--border-color u-menu-to--center"
:options="borderColorOption"
@change="updateStyleValue"
/>
<toolbar-color-picker v-if="hasProperty(styleItem.style.backgroundColor)"
class="c-style__toolbar-button--background-color u-menu-to--center"
:options="backgroundColorOption"
@change="updateStyleValue"
/>
<toolbar-color-picker v-if="hasProperty(styleItem.style.color)"
class="c-style__toolbar-button--color u-menu-to--center"
:options="colorOption"
@change="updateStyleValue"
/>
<toolbar-button v-if="hasProperty(styleItem.style.imageUrl)"
class="c-style__toolbar-button--image-url"
:options="imageUrlOption"
@change="updateStyleValue"
/>
<toolbar-toggle-button v-if="hasProperty(styleItem.style.isStyleInvisible)"
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";
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
export default {
name: 'StyleEditor',
components: {
ToolbarButton,
ToolbarColorPicker,
ToolbarToggleButton
},
inject: [
'openmct'
],
props: {
isEditing: {
type: Boolean
},
mixedStyles: {
type: Array,
default() {
return [];
}
},
styleItem: {
type: Object,
required: true
}
},
computed: {
itemStyle() {
return getStylesWithoutNoneValue(this.styleItem.style);
},
borderColorOption() {
let value = this.styleItem.style.border.replace('1px solid ', '');
return {
icon: 'icon-line-horz',
title: STYLE_CONSTANTS.borderColorTitle,
value: this.normalizeValueForSwatch(value),
property: 'border',
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('border') > -1
}
},
backgroundColorOption() {
let value = this.styleItem.style.backgroundColor;
return {
icon: 'icon-paint-bucket',
title: STYLE_CONSTANTS.backgroundColorTitle,
value: this.normalizeValueForSwatch(value),
property: 'backgroundColor',
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('backgroundColor') > -1
}
},
colorOption() {
let value = this.styleItem.style.color;
return {
icon: 'icon-font',
title: STYLE_CONSTANTS.textColorTitle,
value: this.normalizeValueForSwatch(value),
property: 'color',
isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('color') > -1
}
},
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,
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
}
},
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: {
hasProperty(property) {
return property !== undefined;
},
normalizeValueForSwatch(value) {
if (value && value.indexOf('__no_value') > -1) {
return value.replace('__no_value', 'transparent');
}
return value;
},
normalizeValueForStyle(value) {
if (value && value === 'transparent') {
return '__no_value';
}
return value;
},
updateStyleValue(value, item) {
value = this.normalizeValueForStyle(value);
if (item.property === 'border') {
value = '1px solid ' + value;
}
if (value && (value.url !== undefined)) {
this.styleItem.style[item.property] = value.url;
} else {
this.styleItem.style[item.property] = value;
}
this.$emit('persist', this.styleItem, item.property);
}
}
}
</script>

View File

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

@@ -1,152 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TelemetryCriterion from './TelemetryCriterion';
import { evaluateResults } from "../utils/evaluator";
export default class AllTelemetryCriterion extends TelemetryCriterion {
/**
* 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(telemetryDomainObjectDefinition, openmct);
}
initialize() {
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
this.telemetryDataCache = {};
}
isValid() {
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
}
updateTelemetry(telemetryObjects) {
this.telemetryObjects = { ...telemetryObjects };
this.removeTelemetryDataCache();
}
removeTelemetryDataCache() {
const telemetryCacheIds = Object.keys(this.telemetryDataCache);
Object.values(this.telemetryObjects).forEach(telemetryObject => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
const foundIndex = telemetryCacheIds.indexOf(id);
if (foundIndex > -1) {
telemetryCacheIds.splice(foundIndex, 1);
}
});
telemetryCacheIds.forEach(id => {
delete (this.telemetryDataCache[id]);
});
}
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: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry)
};
if (data) {
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key]
});
}
return datum;
}
getResult(data, telemetryObjects) {
const validatedData = this.isValid() ? data : {};
if (validatedData) {
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
}
Object.values(telemetryObjects).forEach(telemetryObject => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (this.telemetryDataCache[id] === undefined) {
this.telemetryDataCache[id] = false;
}
});
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
if (!this.isValid()) {
return this.formatData({}, options.telemetryObjects);
}
let keys = Object.keys(Object.assign({}, options.telemetryObjects));
const telemetryRequests = keys
.map(key => this.openmct.telemetry.request(
options.telemetryObjects[key],
options
));
return Promise.all(telemetryRequests)
.then(telemetryRequestsResults => {
let latestDatum;
telemetryRequestsResults.forEach((results, index) => {
latestDatum = results.length ? results[results.length - 1] : {};
if (index < telemetryRequestsResults.length-1) {
if (latestDatum) {
this.telemetryDataCache[latestDatum.id] = this.computeResult(latestDatum);
}
}
});
return {
id: this.id,
data: this.formatData(latestDatum, options.telemetryObjects)
};
});
}
destroy() {
delete this.telemetryObjects;
delete this.telemetryDataCache;
}
}

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { OPERATIONS } from '../utils/operations';
import {OPERATIONS} from '../utils/operations';
export default class TelemetryCriterion extends EventEmitter {
@@ -36,75 +36,37 @@ export default class TelemetryCriterion extends EventEmitter {
super();
this.openmct = openmct;
this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
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.result = undefined;
this.subscription = null;
this.telemetryObjectIdAsString = null;
this.objectAPI.get(this.objectAPI.makeKeyString(this.telemetry)).then((obj) => this.initialize(obj));
}
this.initialize();
initialize(obj) {
this.telemetryObject = obj;
this.telemetryObjectIdAsString = this.objectAPI.makeKeyString(this.telemetryObject.identifier);
this.emitEvent('criterionUpdated', this);
}
initialize() {
this.telemetryObject = this.telemetryDomainObjectDefinition.telemetryObject;
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
updateTelemetry(telemetryObjects) {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
}
formatData(data) {
handleSubscription(data) {
const datum = {
result: this.computeResult(data)
};
if (data) {
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
// TODO check back to see if we should format times here
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key]
});
}
return datum;
}
getResult(data) {
const validatedData = this.isValid() ? data : {};
this.result = this.computeResult(validatedData);
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
if (!this.isValid()) {
return {
id: this.id,
data: this.formatData({})
};
}
return this.openmct.telemetry.request(
this.telemetryObject,
options
).then(results => {
const latestDatum = results.length ? results[results.length - 1] : {};
return {
id: this.id,
data: this.formatData(latestDatum)
};
});
this.emitEvent('criterionResultUpdated', datum);
}
findOperation(operation) {
@@ -126,7 +88,7 @@ export default class TelemetryCriterion extends EventEmitter {
this.input.forEach(input => params.push(input));
}
if (typeof comparator === 'function') {
result = !!comparator(params);
result = comparator(params);
}
}
return result;
@@ -139,9 +101,40 @@ export default class TelemetryCriterion extends EventEmitter {
});
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
/**
* Subscribes to the telemetry object and returns an unsubscribe function
* If the telemetry is not valid, returns nothing
*/
subscribe() {
if (this.isValid()) {
this.unsubscribe();
this.subscription = this.telemetryAPI.subscribe(this.telemetryObject, (datum) => {
this.handleSubscription(datum);
});
} else {
this.handleSubscription();
}
}
/**
* Calls an unsubscribe function returned by subscribe() and deletes any initialized data
*/
unsubscribe() {
//unsubscribe from telemetry source
if (typeof this.subscription === 'function') {
this.subscription();
}
delete this.subscription;
}
destroy() {
delete this.telemetryObject;
this.unsubscribe();
this.emitEvent('criterionRemoved');
delete this.telemetryObjectIdAsString;
delete this.telemetryObject;
}
}

View File

@@ -24,6 +24,7 @@ import TelemetryCriterion from "./TelemetryCriterion";
let openmct = {},
mockListener,
mockListener2,
testCriterionDefinition,
testTelemetryObject,
telemetryCriterion;
@@ -36,39 +37,30 @@ describe("The telemetry criterion", function () {
type: "test-object",
name: "Test Object",
telemetry: {
valueMetadatas: [{
key: "value",
name: "Value",
hints: {
range: 2
}
},
{
key: "utc",
name: "Time",
format: "utc",
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "testSource",
source: "value",
name: "Test",
format: "string"
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}]
}
};
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", "getValueFormatter"]);
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata"]);
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.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values);
openmct.time = jasmine.createSpyObj('timeAPI',
['timeSystem', 'bounds', 'getAllTimeSystems']
@@ -79,14 +71,13 @@ describe("The telemetry criterion", function () {
testCriterionDefinition = {
id: 'test-criterion-id',
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
operation: 'textContains',
metadata: 'value',
input: ['Hell'],
telemetryObject: testTelemetryObject
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier)
};
mockListener = jasmine.createSpy('listener');
mockListener2 = jasmine.createSpy('updatedListener', (data) => {
console.log(data);
});
telemetryCriterion = new TelemetryCriterion(
testCriterionDefinition,
@@ -94,28 +85,39 @@ describe("The telemetry criterion", function () {
);
telemetryCriterion.on('criterionResultUpdated', mockListener);
telemetryCriterion.on('criterionUpdated', mockListener2);
});
it("initializes with a telemetry objectId as string", function () {
telemetryCriterion.initialize(testTelemetryObject);
expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key);
expect(mockListener2).toHaveBeenCalled();
});
it("returns a result on new data from relevant telemetry providers", function () {
telemetryCriterion.getResult({
value: 'Hello',
utc: 'Hi',
id: testTelemetryObject.identifier.key
it("subscribes to telemetry providers", function () {
telemetryCriterion.subscribe();
expect(telemetryCriterion.subscription).toBeDefined();
});
it("emits update event on new data from telemetry providers", function () {
spyOn(telemetryCriterion, 'emitEvent').and.callThrough();
telemetryCriterion.handleSubscription({
key: 'some-key',
source: 'testSource',
testSource: 'Hello'
});
expect(telemetryCriterion.result).toBeTrue();
expect(telemetryCriterion.emitEvent).toHaveBeenCalled();
expect(mockListener).toHaveBeenCalled();
});
it("un-subscribes from telemetry providers", function () {
telemetryCriterion.subscribe();
expect(telemetryCriterion.subscription).toBeDefined();
telemetryCriterion.destroy();
expect(telemetryCriterion.subscription).toBeUndefined();
expect(telemetryCriterion.telemetryObjectIdAsString).toBeUndefined();
expect(telemetryCriterion.telemetryObject).toBeUndefined();
});
// it("does not return a result on new data from irrelavant telemetry providers", function () {
// telemetryCriterion.getResult({
// value: 'Hello',
// utc: 'Hi',
// id: '1234'
// });
// expect(telemetryCriterion.result).toBeFalse();
// });
});

View File

@@ -23,42 +23,35 @@ 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('condition', {
name: 'Condition',
key: 'condition',
description: 'A list of criteria which will be evaluated based on a trigger',
creatable: false,
initialize: function (domainObject) {
domainObject.composition = [];
}
});
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.',
description: 'A set of one or more conditions based on user-specified criteria.',
creatable: true,
cssClass: 'icon-conditional',
cssClass: 'icon-conditional', // TODO: replace with class for new icon
initialize: function (domainObject) {
domainObject.configuration = {
conditionTestData: [],
conditionCollection: [{
isDefault: true,
id: uuid(),
configuration: {
name: 'Default',
output: 'Default',
trigger: 'all',
criteria: []
},
summary: 'Default condition'
}]
conditionCollection: []
};
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));

View File

@@ -26,14 +26,26 @@ import ConditionPlugin from "./plugin";
let openmct = createOpenMct();
openmct.install(new ConditionPlugin());
let conditionDefinition;
let conditionSetDefinition;
let mockConditionSetDomainObject;
let mockDomainObject;
let mockDomainObject2;
let element;
let child;
describe('the plugin', function () {
beforeAll((done) => {
conditionDefinition = openmct.types.get('condition').definition;
mockDomainObject = {
identifier: {
key: 'testConditionKey',
namespace: ''
},
type: 'condition'
};
conditionDefinition.initialize(mockDomainObject);
conditionSetDefinition = openmct.types.get('conditionSet').definition;
const appHolder = document.createElement('div');
@@ -44,7 +56,7 @@ describe('the plugin', function () {
child = document.createElement('div');
element.appendChild(child);
mockConditionSetDomainObject = {
mockDomainObject2 = {
identifier: {
key: 'testConditionSetKey',
namespace: ''
@@ -52,12 +64,22 @@ describe('the plugin', function () {
type: 'conditionSet'
};
conditionSetDefinition.initialize(mockConditionSetDomainObject);
conditionSetDefinition.initialize(mockDomainObject2);
openmct.on('start', done);
openmct.start(appHolder);
});
let mockConditionObject = {
name: 'Condition',
key: 'condition',
creatable: false
};
it('defines a condition object type with the correct key', () => {
expect(conditionDefinition.key).toEqual(mockConditionObject.key);
});
let mockConditionSetObject = {
name: 'Condition Set',
key: 'conditionSet',
@@ -68,6 +90,18 @@ describe('the plugin', function () {
expect(conditionSetDefinition.key).toEqual(mockConditionSetObject.key);
});
describe('the condition object', () => {
it('is not creatable', () => {
expect(conditionDefinition.creatable).toEqual(mockConditionObject.creatable);
});
it('initializes with an empty composition list', () => {
expect(mockDomainObject.composition instanceof Array).toBeTrue();
expect(mockDomainObject.composition.length).toEqual(0);
});
});
describe('the conditionSet object', () => {
it('is creatable', () => {
@@ -75,17 +109,14 @@ describe('the plugin', function () {
});
it('initializes with an empty composition list', () => {
expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();
expect(mockConditionSetDomainObject.composition.length).toEqual(0);
expect(mockDomainObject2.composition instanceof Array).toBeTrue();
expect(mockDomainObject2.composition.length).toEqual(0);
});
it('provides a view', () => {
const testViewObject = {
id:"test-object",
type: "conditionSet",
configuration: {
conditionCollection: []
}
type: "conditionSet"
};
const applicableViews = openmct.objectViews.get(testViewObject);

View File

@@ -1,54 +1,4 @@
/*****************************************************************************
* 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'
}
ALL: 'all'
};

View File

@@ -1,68 +1,16 @@
/*****************************************************************************
* 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 { TRIGGER } from "./constants";
export const evaluateResults = (results, trigger) => {
if (trigger && trigger === TRIGGER.XOR) {
return matchExact(results, 1);
} else if (trigger && trigger === TRIGGER.NOT) {
return matchExact(results, 0);
} else if (trigger && trigger === TRIGGER.ALL) {
return matchAll(results);
} else {
return matchAny(results);
}
}
function matchAll(results) {
for (const result of results) {
if (!result) {
return false;
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 true;
}
function matchAny(results) {
for (const result of results) {
if (result) {
return true;
}
}
return false;
}
function matchExact(results, target) {
let matches = 0;
for (const result of results) {
if (result) {
matches++;
}
if (matches > target) {
return false;
}
}
return matches === target;
}
return result;
};

View File

@@ -1,204 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { evaluateResults } from './evaluator';
import { TRIGGER } from './constants';
describe('evaluate results', () => {
// const allTrue = [true, true, true, true, true];
// const oneTrue = [false, false, false, false, true];
// const multipleTrue = [false, true, false, true, false];
// const noneTrue = [false, false, false, false, false];
// const allTrueWithUndefined = [true, true, true, undefined, true];
// const oneTrueWithUndefined = [undefined, undefined, undefined, undefined, true];
// const multipleTrueWithUndefined = [true, undefined, true, undefined, true];
// const allUndefined = [undefined, undefined, undefined, undefined, undefined];
// const singleTrue = [true];
// const singleFalse = [false];
// const singleUndefined = [undefined];
// const empty = [];
const tests = [
{
name: 'allTrue',
values: [true, true, true, true, true],
any: true,
all: true,
not: false,
xor: false
}, {
name: 'oneTrue',
values: [false, false, false, false, true],
any: true,
all: false,
not: false,
xor: true
}, {
name: 'multipleTrue',
values: [false, true, false, true, false],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'noneTrue',
values: [false, false, false, false, false],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'allTrueWithUndefined',
values: [true, true, true, undefined, true],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'oneTrueWithUndefined',
values: [undefined, undefined, undefined, undefined, true],
any: true,
all: false,
not: false,
xor: true
}, {
name: 'multipleTrueWithUndefined',
values: [true, undefined, true, undefined, true],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'allUndefined',
values: [undefined, undefined, undefined, undefined, undefined],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'singleTrue',
values: [true],
any: true,
all: true,
not: false,
xor: true
}, {
name: 'singleFalse',
values: [false],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'singleUndefined',
values: [undefined],
any: false,
all: false,
not: true,
xor: false
}
// , {
// name: 'empty',
// values: [],
// any: false,
// all: false,
// not: true,
// xor: false
// }
];
describe(`based on trigger ${TRIGGER.ANY}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.ANY);
expect(result).toEqual(test[TRIGGER.ANY])
});
});
});
describe(`based on trigger ${TRIGGER.ALL}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.ALL);
expect(result).toEqual(test[TRIGGER.ALL])
});
});
});
describe(`based on trigger ${TRIGGER.NOT}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.NOT);
expect(result).toEqual(test[TRIGGER.NOT])
});
});
});
describe(`based on trigger ${TRIGGER.XOR}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.XOR);
expect(result).toEqual(test[TRIGGER.XOR])
});
});
});
// it('should evaluate to true if trigger is NOT', () => {
// const results = {
// result: false,
// result1: false,
// result2: false
// };
// const result = computeConditionByLimit(results, 0);
// expect(result).toBeTrue();
// });
// it('should evaluate to false if trigger is NOT', () => {
// const results = {
// result: true,
// result1: false,
// result2: false
// };
// const result = computeConditionByLimit(results, 0);
// expect(result).toBeFalse();
// });
// it('should evaluate to true if trigger is XOR', () => {
// const results = {
// result: false,
// result1: true,
// result2: false
// };
// const result = computeConditionByLimit(results, 1);
// expect(result).toBeTrue();
// });
// it('should evaluate to false if trigger is XOR', () => {
// const results = {
// result: false,
// result1: true,
// result2: true
// };
// const result = computeConditionByLimit(results, 1);
// expect(result).toBeFalse();
// });
});

View File

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

View File

@@ -1,142 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
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');
let enumIsOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIs');
let enumIsNotOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIsNot');
describe('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();
});
it('should evaluate enumValueIs to true for number inputs', () => {
const inputs = [1, "1"];
expect(!!enumIsOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIs to true for string inputs', () => {
const inputs = ["45", "45"];
expect(!!enumIsOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIsNot to true for number inputs', () => {
const inputs = [45, "46"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIsNot to true for string inputs', () => {
const inputs = ["45", "46"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIs to false for number inputs', () => {
const inputs = [1, "2"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIs to false for string inputs', () => {
const inputs = ["45", "46"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to false for number inputs', () => {
const inputs = [45, "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to false for string inputs', () => {
const inputs = ["45", "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIs to false for undefined input', () => {
const inputs = [undefined, "45"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to true for undefined input', () => {
const inputs = [undefined, "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
});

View File

@@ -1,172 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const NONE_VALUE = '__no_value';
const styleProps = {
backgroundColor: {
svgProperty: 'fill',
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view' ||
type === 'telemetry-view' ||
type === 'box-view' ||
type === 'subobject-view');
}
},
border: {
svgProperty: 'stroke',
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view' ||
type === 'telemetry-view' ||
type === 'box-view' ||
type === 'image-view' ||
type === 'line-view'||
type === 'subobject-view');
}
},
color: {
svgProperty: 'color',
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view' ||
type === 'telemetry-view'||
type === 'subobject-view');
}
},
imageUrl: {
svgProperty: 'url',
noneValue: '',
applicableForType: type => {
return !type ? false : type === 'image-view';
}
}
};
const aggregateStyleValues = (accumulator, currentStyle) => {
const styleKeys = Object.keys(currentStyle);
const properties = Object.keys(styleProps);
properties.forEach((property) => {
if (!accumulator[property]) {
accumulator[property] = [];
}
const found = styleKeys.find(key => key === property);
if (found) {
accumulator[property].push(currentStyle[found]);
}
});
return accumulator;
};
// Returns a union of styles used by multiple items.
// Styles that are common to all items but don't have the same value are added to the mixedStyles list
export const getConsolidatedStyleValues = (multipleItemStyles) => {
let aggregatedStyleValues = multipleItemStyles.reduce(aggregateStyleValues, {});
let styleValues = {};
let mixedStyles = [];
const properties = Object.keys(styleProps);
properties.forEach((property) => {
const values = aggregatedStyleValues[property];
if (values.length) {
if (values.every(value => value === values[0])) {
styleValues[property] = values[0];
} else {
styleValues[property] = '';
mixedStyles.push(property);
}
}
});
return {
styles: styleValues,
mixedStyles
};
};
const getStaticStyleForItem = (domainObject, id) => {
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
if (domainObjectStyles) {
if (id) {
if(domainObjectStyles[id] && domainObjectStyles[id].staticStyle) {
return domainObjectStyles[id].staticStyle.style;
}
} else if (domainObjectStyles.staticStyle) {
return domainObjectStyles.staticStyle.style;
}
}
};
export const getConditionalStyleForItem = (domainObject, id) => {
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
if (domainObjectStyles) {
if (id) {
if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) {
return domainObjectStyles[id].styles;
}
} else if (domainObjectStyles.staticStyle) {
return domainObjectStyles.styles;
}
}
};
//Returns either existing static styles or uses SVG defaults if available
export const getApplicableStylesForItem = (domainObject, item) => {
const type = item && item.type;
const id = item && item.id;
let style = {};
let staticStyle = getStaticStyleForItem(domainObject, id);
const properties = Object.keys(styleProps);
properties.forEach(property => {
const styleProp = styleProps[property];
if (styleProp.applicableForType(type)) {
let defaultValue;
if (staticStyle) {
defaultValue = staticStyle[property];
} else if (item) {
defaultValue = item[styleProp.svgProperty];
}
style[property] = defaultValue === undefined ? styleProp.noneValue : defaultValue;
}
});
return style;
};
export const getStylesWithoutNoneValue = (style) => {
if (_.isEmpty(style) || !style) {
return;
}
let styleObj = {};
const keys = Object.keys(style);
keys.forEach(key => {
if ((typeof style[key] === 'string')) {
if (style[key].indexOf('__no_value') > -1) {
style[key] = '';
} else {
styleObj[key] = style[key];
}
}
});
return styleObj;
};

View File

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

View File

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

@@ -1,61 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<component :is="urlDefined ? 'a' : 'span'"
class="c-condition-widget"
:href="urlDefined ? internalDomainObject.url : null"
>
<div class="c-condition-widget__label">
{{ internalDomainObject.label }}
</div>
</component>
</template>
<script>
export default {
inject: ['openmct', 'domainObject'],
data: function () {
return {
internalDomainObject: this.domainObject
}
},
computed: {
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
}
}
}
</script>

View File

@@ -1,60 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.c-condition-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;
}
a.c-condition-widget {
// Widget is conditionally made into a <a> when URL property has been defined
cursor: pointer !important;
pointer-events: inherit;
}
// Make Condition Widget expand when in a hidden frame Layout context
// 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

@@ -1,58 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
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,6 +347,78 @@ 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",
@@ -357,7 +429,7 @@ define(['lodash'], function (_) {
property: function (selectionPath) {
return getPath(selectionPath);
},
icon: "icon-font",
icon: "icon-gear",
title: "Edit text properties",
dialog: DIALOG_FORM.text
};
@@ -433,14 +505,14 @@ define(['lodash'], function (_) {
let toolbar = {
'add-menu': [],
'text': [],
'url': [],
'toggle-frame': [],
'display-mode': [],
'telemetry-value': [],
'style': [],
'text-style': [],
'position': [],
'text': [],
'url': [],
'remove': []
};
@@ -448,10 +520,6 @@ 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)];
@@ -478,8 +546,15 @@ 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)
];
}
@@ -496,8 +571,15 @@ 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)
];
}
@@ -517,6 +599,12 @@ 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),
@@ -530,6 +618,11 @@ 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),
@@ -539,10 +632,18 @@ 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-inspect-properties"
class="c-properties"
>
<div class="c-inspect-properties__header">
<div class="c-properties__header">
Alphanumeric Format
</div>
<ul class="c-inspect-properties__section">
<li class="c-inspect-properties__row">
<ul class="c-properties__section">
<li class="c-properties__row">
<div
class="c-inspect-properties__label"
class="c-properties__label"
title="Printf formatting for the selected telemetry"
>
<label for="telemetryPrintfFormat">Format</label>
</div>
<div class="c-inspect-properties__value">
<div class="c-properties__value">
<input
id="telemetryPrintfFormat"
type="text"
@@ -85,13 +85,7 @@ export default {
return;
}
let layoutItem = selection[0][0].context.layoutItem;
if (!layoutItem) {
return;
}
let format = layoutItem.format;
let format = selection[0][0].context.layoutItem.format;
this.nonMixedFormat = selection.every(selectionPath => {
return selectionPath[0].context.layoutItem.format === format;
});

View File

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

View File

@@ -29,7 +29,6 @@
>
<div
class="c-image-view"
:class="[styleClass]"
:style="style"
></div>
</layout-frame>
@@ -37,7 +36,6 @@
<script>
import LayoutFrame from './LayoutFrame.vue'
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
export default {
makeDefinition(openmct, gridSize, element) {
@@ -54,7 +52,6 @@ export default {
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
props: {
item: {
type: Object,
@@ -74,18 +71,10 @@ export default {
},
computed: {
style() {
let backgroundImage = 'url(' + this.item.url + ')';
let border = '1px solid ' + this.item.stroke;
if (this.itemStyle) {
if (this.itemStyle.imageUrl !== undefined) {
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
}
border = this.itemStyle.border;
}
return {
backgroundImage,
border
};
backgroundImage: 'url(' + this.item.url + ')',
border: '1px solid ' + this.item.stroke
}
}
},
watch: {

View File

@@ -23,7 +23,6 @@
<template>
<div
class="l-layout__frame c-frame no-frame"
:class="[styleClass]"
:style="style"
>
<svg
@@ -32,7 +31,7 @@
>
<line
v-bind="linePosition"
:stroke="stroke"
:stroke="item.stroke"
stroke-width="2"
/>
</svg>
@@ -61,8 +60,6 @@
<script>
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
const START_HANDLE_QUADRANTS = {
1: 'c-frame-edit__handle--sw',
2: 'c-frame-edit__handle--se',
@@ -88,7 +85,6 @@ export default {
};
},
inject: ['openmct'],
mixins: [conditionalStylesMixin],
props: {
item: {
type: Object,
@@ -126,16 +122,6 @@ export default {
}
return {x, y, x2, y2};
},
stroke() {
if (this.itemStyle) {
if (this.itemStyle.border) {
return this.itemStyle.border.replace('1px solid ', '');
}
return '';
} 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);

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', 'conditionWidget'];
DEFAULT_HIDDEN_FRAME_TYPES = ['hyperlink', 'summary-widget'];
function getDefaultDimensions(gridSize) {
return MINIMUM_FRAME_SIZE.map((min, index) => {

View File

@@ -30,13 +30,13 @@
<div
v-if="domainObject"
class="c-telemetry-view"
:class="styleClass"
:style="styleObject"
@contextmenu.prevent="showContextMenu"
>
<div
v-if="showLabel"
class="c-telemetry-view__label"
:style="conditionalStyle"
>
<div class="c-telemetry-view__label-text">
{{ domainObject.name }}
@@ -48,6 +48,7 @@
:title="fieldName"
class="c-telemetry-view__value"
:class="[telemetryClass]"
:style="!telemetryClass && conditionalStyle"
>
<div class="c-telemetry-view__value-text">
{{ telemetryValue }}
@@ -60,7 +61,7 @@
<script>
import LayoutFrame from './LayoutFrame.vue'
import printj from 'printj'
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import StyleRuleManager from "../../condition/StyleRuleManager";
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5],
DEFAULT_POSITION = [1, 1],
@@ -79,8 +80,8 @@ export default {
height: DEFAULT_TELEMETRY_DIMENSIONS[1],
displayMode: 'all',
value: metadata.getDefaultDisplayValue(),
stroke: "",
fill: "",
stroke: "transparent",
fill: "transparent",
color: "",
size: "13px"
};
@@ -89,7 +90,6 @@ export default {
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
props: {
item: {
type: Object,
@@ -112,7 +112,8 @@ export default {
datum: undefined,
formats: undefined,
domainObject: undefined,
currentObjectPath: undefined
currentObjectPath: undefined,
conditionalStyle: ''
}
},
computed: {
@@ -125,10 +126,12 @@ export default {
return displayMode === 'all' || displayMode === 'value';
},
styleObject() {
return Object.assign({}, {
return {
backgroundColor: this.item.fill,
borderColor: this.item.stroke,
color: this.item.color,
fontSize: this.item.size
}, this.itemStyle);
}
},
fieldName() {
return this.valueMetadata && this.valueMetadata.name;
@@ -183,6 +186,16 @@ export default {
this.removeSelectable();
}
if (this.unlistenStyles) {
this.unlistenStyles();
}
if (this.styleRuleManager) {
this.styleRuleManager.destroy();
this.styleRuleManager.off('conditionalStyleUpdated', this.updateStyle.bind(this));
delete this.styleRuleManager;
}
this.openmct.time.off("bounds", this.refreshData);
},
methods: {
@@ -225,6 +238,7 @@ export default {
},
setObject(domainObject) {
this.domainObject = domainObject;
this.initConditionalStyles();
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
@@ -249,6 +263,21 @@ export default {
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
},
initConditionalStyles() {
this.styleRuleManager = new StyleRuleManager(this.domainObject.configuration.conditionalStyle, this.openmct);
this.styleRuleManager.on('conditionalStyleUpdated', this.updateStyle.bind(this));
if (this.unlistenStyles) {
this.unlistenStyles();
}
this.unlistenStyles = this.openmct.objects.observe(this.domainObject, 'configuration.conditionalStyle', (newConditionalStyle) => {
//Updating conditional styles in the inspector view will trigger this so that the changes are reflected immediately
this.styleRuleManager.updateConditionalStyleConfig(newConditionalStyle);
});
},
updateStyle(styleObj) {
this.conditionalStyle = styleObj;
}
}
}

View File

@@ -29,23 +29,21 @@
>
<div
class="c-text-view"
:class="[styleClass]"
:style="style"
>
<div class="c-text-view__text">{{ item.text }}</div>
{{ item.text }}
</div>
</layout-frame>
</template>
<script>
import LayoutFrame from './LayoutFrame.vue'
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
export default {
makeDefinition(openmct, gridSize, element) {
return {
fill: '',
stroke: '',
fill: 'transparent',
stroke: 'transparent',
size: '13px',
color: '',
x: 1,
@@ -59,7 +57,6 @@ export default {
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
props: {
item: {
type: Object,
@@ -79,9 +76,12 @@ export default {
},
computed: {
style() {
return Object.assign({
return {
backgroundColor: this.item.fill,
borderColor: this.item.stroke,
color: this.item.color,
fontSize: this.item.size
}, this.itemStyle);
};
}
},
watch: {

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
<template>
<div class="c-inspect-properties__section c-filter-settings">
<div class="c-properties__section c-filter-settings">
<li
v-for="(filter, index) in filterField.filters"
:key="index"
class="c-inspect-properties__row c-filter-settings__setting"
class="c-properties__row c-filter-settings__setting"
>
<div
class="c-inspect-properties__label label"
class="c-properties__label label"
:disabled="useGlobal"
>
{{ filterField.name }} =
</div>
<div class="c-inspect-properties__value value">
<div class="c-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-inspect-properties">
<ul class="c-properties">
<div
v-if="!isEditing && persistedFilters.useGlobal"
class="c-inspect-properties__label span-all"
class="c-properties__label span-all"
>
Uses global filter
</div>
<div
v-if="isEditing"
class="c-inspect-properties__label span-all"
class="c-properties__label span-all"
>
<toggle-switch
:id="keyString"

View File

@@ -23,7 +23,7 @@
</div>
<ul
v-if="expanded"
class="c-inspect-properties"
class="c-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-inspect-properties {
.c-properties {
// Add extra margin to account for filter-indicator
margin-left: 38px;
}

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ export default {
data() {
return {
autoScroll: true,
bounds: {},
date: '',
filters : {
brightness: 100,
contrast: 100
@@ -78,37 +78,19 @@ export default {
imageHistory: [],
imageUrl: '',
isPaused: false,
metadata: {},
requestCount: 0,
timeFormat: ''
}
},
mounted() {
// set
this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageFormat = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
// initialize
this.bounds = this.openmct.time.bounds();
this.timeKey = this.openmct.time.timeSystem().key;
this.timeFormat = this.getTimeFormat();
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
// kickoff
this.subscribe();
this.requestHistory(false);
this.subscribe(this.domainObject);
},
updated() {
this.scrollToRight();
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
this.stopListening();
},
methods: {
datumMatchesMostRecent(datum) {
@@ -133,15 +115,6 @@ export default {
this.timeFormat.format(datum) :
this.time;
},
getTimeFormat() {
let tf = false;
try {
tf = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
} catch(e) {
alert('Issue receiving time format.');
}
return tf;
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper
if (!thumbsWrapper) {
@@ -174,6 +147,21 @@ export default {
return this.isPaused;
},
requestHistory(bounds) {
this.requestCount++;
this.imageHistory = [];
const requestId = this.requestCount;
this.openmct.telemetry
.request(this.domainObject, bounds)
.then((values = []) => {
if (this.requestCount > requestId) {
return Promise.resolve('Stale request');
}
values.forEach(this.updateHistory);
this.updateValues(values[values.length - 1]);
});
},
scrollToRight() {
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
return;
@@ -200,44 +188,28 @@ export default {
image.selected = true;
}
},
boundsChange(bounds, isTick) {
this.bounds = bounds;
this.requestHistory(isTick);
},
requestHistory(isTick) {
if(!isTick) {
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
this.openmct.telemetry
.request(this.domainObject, this.bounds)
.then((values = []) => {
if (this.requestCount > requestId) {
return Promise.resolve('Stale request');
}
if(this.timeFormat) {
values.forEach(this.updateHistory);
this.updateValues(values[values.length - 1]);
}
});
stopListening() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
},
timeSystemChange(system) {
// reset timesystem dependent variables
this.timeKey = system.key;
this.timeFormat = this.getTimeFormat();
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
if(this.timeFormat) {
let parsedTimestamp = this.timeFormat.parse(datum[this.timeKey]);
if(parsedTimestamp >= this.bounds.start && parsedTimestamp <= this.bounds.end) {
subscribe(domainObject) {
this.date = ''
this.imageUrl = '';
this.openmct.objects.get(this.keystring)
.then((object) => {
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.timeKey = this.openmct.time.timeSystem().key;
this.timeFormat = this.openmct.telemetry.getValueFormatter(metadata.value(this.timeKey));
this.imageFormat = this.openmct.telemetry.getValueFormatter(metadata.valuesForHints(['image'])[0]);
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
this.updateHistory(datum);
this.updateValues(datum);
}
}
});
this.requestHistory(this.openmct.time.bounds());
});
},
unselectAllImages() {

View File

@@ -1,21 +0,0 @@
<template>
<div class="c-menu">
<ul>
<li
v-for="(item, index) in popupMenuItems"
:key="index"
:class="item.cssClass"
:title="item.name"
@click="item.callback"
>
{{ item.name }}
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['popupMenuItems']
}
</script>

View File

@@ -1,239 +0,0 @@
<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>
<PopupMenu :popup-menu-items="popupMenuItems" />
</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 PopupMenu from './popup-menu.vue';
import PreviewAction from '../../../ui/preview/PreviewAction';
import Painterro from 'painterro';
import RemoveDialog from '../utils/removeDialog';
import SnapshotTemplate from './snapshot-template.html';
import Vue from 'vue';
export default {
inject: ['openmct'],
components: {
PopupMenu
},
props: {
embed: {
type: Object,
default() {
return {};
}
},
removeActionString: {
type: String,
default() {
return 'Remove This Embed';
}
}
},
data() {
return {
popupMenuItems: []
}
},
watch: {
},
mounted() {
this.addPopupMenuItems();
},
methods: {
addPopupMenuItems() {
const removeEmbed = {
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
}
const preview = {
cssClass: 'icon-eye-open',
name: 'Preview',
callback: this.previewEmbed.bind(this)
}
this.popupMenuItems = [removeEmbed, preview];
},
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);
},
getRemoveDialog() {
const options = {
name: this.removeActionString,
callback: this.removeEmbed.bind(this)
}
const removeDialog = new RemoveDialog(this.openmct, options);
removeDialog.show();
},
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();
}
}
]
});
},
previewEmbed() {
const self = this;
const previewAction = new PreviewAction(self.openmct);
previewAction.invoke(JSON.parse(self.embed.objectPath));
},
removeEmbed(success) {
if (!success) {
return;
}
this.$emit('removeEmbed', this.embed.id);
},
updateEmbed(embed) {
this.$emit('updateEmbed', embed);
}
}
}
</script>

View File

@@ -1,316 +0,0 @@
<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

@@ -1,114 +0,0 @@
<template>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu icon-notebook"
title="Take a Notebook Snapshot"
@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

@@ -1,129 +0,0 @@
<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>
<PopupMenu v-if="snapshots.length > 0"
:popup-menu-items="popupMenuItems"
/>
</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 PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
export default {
inject: ['openmct', 'snapshotContainer'],
components: {
NotebookEmbed,
PopupMenu
},
props: {
toggleSnapshot: {
type: Function,
default() {
return () => {};
}
}
},
data() {
return {
popupMenuItems: [],
removeActionString: 'Delete all snapshots',
snapshots: []
}
},
mounted() {
this.addPopupMenuItems();
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.snapshots = this.snapshotContainer.getSnapshots();
},
beforeDestory() {
},
methods: {
addPopupMenuItems() {
const removeSnapshot = {
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
}
this.popupMenuItems = [removeSnapshot];
},
close() {
this.toggleSnapshot();
},
getNotebookSnapshotMaxCount() {
return NOTEBOOK_SNAPSHOT_MAX_COUNT;
},
getRemoveDialog() {
const options = {
name: this.removeActionString,
callback: this.removeAllSnapshots.bind(this)
}
const removeDialog = new RemoveDialog(this.openmct, options);
removeDialog.show();
},
removeAllSnapshots(success) {
if (!success) {
return;
}
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);
},
updateSnapshot(snapshot) {
this.snapshotContainer.updateSnapshot(snapshot);
}
}
}
</script>

View File

@@ -1,97 +0,0 @@
<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

@@ -1,563 +0,0 @@
<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="showTime === 0"
>
Show all
</option>
<option value="1"
:selected="showTime === 1"
>Last hour</option>
<option value="8"
:selected="showTime === 8"
>Last 8 hours</option>
<option value="24"
:selected="showTime === 24"
>Last 24 hours</option>
</select>
<select v-model="defaultSort"
class="c-notebook__controls__time"
>
<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"
ref="notebookEntries"
class="c-notebook__entries"
>
<NotebookEntry v-for="entry in filteredAndSortedEntries"
: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,
focusEntryId: null,
internalDomainObject: this.domainObject,
search: '',
showTime: 0,
showNav: false,
sidebarCoversEntries: false
}
},
computed: {
filteredAndSortedEntries() {
const filterTime = Date.now();
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
const hours = parseInt(this.showTime, 10);
const filteredPageEntriesByTime = hours
? pageEntries.filter(entry => (filterTime - entry.createdOn) <= hours * 60 * 60 * 1000)
: pageEntries;
return this.defaultSort === 'oldest'
? filteredPageEntriesByTime
: [...filteredPageEntriesByTime].reverse();
},
pages() {
return this.getPages() || [];
},
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();
}
},
updated: function () {
this.$nextTick(function () {
this.focusOnEntryId();
})
},
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('');
},
createNotebookStorageObject() {
const notebookMeta = {
name: this.internalDomainObject.name,
identifier: this.internalDomainObject.identifier
};
const page = this.getSelectedPage();
const section = this.getSelectedSection();
return {
notebookMeta,
section,
page
}
},
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);
},
focusOnEntryId() {
if (!this.focusEntryId) {
return;
}
const element = this.$refs.notebookEntries.querySelector(`#${this.focusEntryId}`);
if (!element) {
return;
}
element.focus();
this.focusEntryId = null;
},
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) {
this.search = '';
const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage);
const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed);
this.focusEntryId = id;
this.search = '';
},
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;
},
toggleNav() {
this.showNav = !this.showNav;
},
async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(notebookStorage);
this.addDefaultClass();
this.defaultSectionId = notebookStorage.section.id;
this.defaultPageId = notebookStorage.page.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

@@ -1,132 +0,0 @@
<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

@@ -1,127 +0,0 @@
<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>
<PopupMenu :popup-menu-items="popupMenuItems" />
</div>
</template>
<script>
import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
export default {
inject: ['openmct'],
components: {
PopupMenu
},
props: {
defaultPageId: {
type: String,
default() {
return '';
}
},
page: {
type: Object,
required: true
},
pageTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
popupMenuItems: [],
removeActionString: `Delete ${this.pageTitle}`
}
},
watch: {
page(newPage) {
this.toggleContentEditable(newPage);
}
},
mounted() {
this.addPopupMenuItems();
this.toggleContentEditable();
},
destroyed() {
},
methods: {
addPopupMenuItems() {
const removePage = {
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
}
this.popupMenuItems = [removePage];
},
deletePage(success) {
if (!success) {
return;
}
this.$emit('deletePage', this.page.id);
},
getRemoveDialog() {
const message = 'This action will delete this page and all of its entries. Do you want to continue?';
const options = {
name: this.removeActionString,
callback: this.deletePage.bind(this),
message
}
const removeDialog = new RemoveDialog(this.openmct, options);
removeDialog.show();
},
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);
},
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

@@ -1,93 +0,0 @@
<template>
<button
class="c-popup-menu-button c-disclosure-button"
title="popup menu"
@click="showMenuItems"
>
</button>
</template>
<script>
import MenuItems from './menu-items.vue';
import Vue from 'vue';
export default {
inject: ['openmct'],
props: {
domainObject: {
type: Object,
default() {
return {};
}
},
popupMenuItems: {
type: Array,
default() {
return [];
}
}
},
data() {
return {
menuItems: null
}
},
mounted() {
},
methods: {
calculateMenuPosition(event, element) {
let eventPosX = event.clientX;
let eventPosY = event.clientY;
let menuDimensions = element.getBoundingClientRect();
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: eventPosX,
y: eventPosY
}
},
hideMenuItems() {
document.body.removeChild(this.menuItems.$el);
this.menuItems.$destroy();
this.menuItems = null;
document.removeEventListener('click', this.hideMenuItems);
return;
},
showMenuItems($event) {
const menuItems = new Vue({
components: {
MenuItems
},
provide: {
popupMenuItems: this.popupMenuItems
},
template: '<MenuItems />'
});
this.menuItems = menuItems;
menuItems.$mount();
const element = this.menuItems.$el;
document.body.appendChild(element);
const position = this.calculateMenuPosition($event, element);
element.style.left = `${position.x}px`;
element.style.top = `${position.y}px`;
setTimeout(() => {
document.addEventListener('click', this.hideMenuItems);
}, 0);
}
}
}
</script>

View File

@@ -1,50 +0,0 @@
<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

@@ -1,113 +0,0 @@
<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

@@ -1,132 +0,0 @@
<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>
<PopupMenu :popup-menu-items="popupMenuItems" />
</div>
</template>
<style lang="scss">
</style>
<script>
import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
export default {
inject: ['openmct'],
components: {
PopupMenu
},
props: {
defaultSectionId: {
type: String,
default() {
return '';
}
},
section: {
type: Object,
required: true
},
sectionTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
popupMenuItems: [],
removeActionString: `Delete ${this.sectionTitle}`
}
},
watch: {
section(newSection) {
this.toggleContentEditable(newSection);
}
},
mounted() {
this.addPopupMenuItems();
this.toggleContentEditable();
},
destroyed() {
},
methods: {
addPopupMenuItems() {
const removeSection = {
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
}
this.popupMenuItems = [removeSection];
},
deleteSection(success) {
if (!success) {
return;
}
this.$emit('deleteSection', this.section.id);
},
getRemoveDialog() {
const message = 'This action will delete this section and all of its pages and entries. Do you want to continue?';
const options = {
name: this.removeActionString,
callback: this.deleteSection.bind(this),
message
}
const removeDialog = new RemoveDialog(this.openmct, options);
removeDialog.show();
},
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);
},
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

@@ -1,119 +0,0 @@
.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

@@ -1,189 +0,0 @@
<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

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

View File

@@ -1,134 +1,99 @@
import Notebook from './components/notebook.vue';
import NotebookSnapshotIndicator from './components/notebook-snapshot-indicator.vue';
import SnapshotContainer from './snapshot-container';
import Vue from 'vue';
/*****************************************************************************
* 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.
*****************************************************************************/
let installed = false;
define([
"./src/controllers/NotebookController"
], function (
NotebookController
) {
var installed = false;
export default function NotebookPlugin() {
return function install(openmct) {
if (installed) {
return;
}
function NotebookPlugin() {
return function install(openmct) {
if (installed) {
return;
}
installed = true;
installed = true;
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: [
openmct.legacyRegistry.register('notebook', {
name: 'Notebook Plugin',
extensions: {
types: [
{
name: 'Newest First',
value: "newest"
},
{
name: 'Oldest First',
value: "oldest"
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'
}
]
}
],
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);
});
const snapshotContainer = new SnapshotContainer(openmct);
const notebookSnapshotIndicator = new Vue ({
provide: {
openmct,
snapshotContainer
},
components: {
NotebookSnapshotIndicator
},
template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
});
const indicator = {
element: notebookSnapshotIndicator.$mount().$el
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
};
}
});
};
openmct.indicators.add(indicator);
}
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();
}
};
}
});
};
}
return NotebookPlugin;
});

View File

@@ -0,0 +1,33 @@
<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>

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