feat: AMD -> ES6 (#7029)

* feat: full amd -> es6 conversion

* fix: move MCT to ES6 class

* fix: default drop, correct imports

* fix: correct all imports

* fix: property typo

* fix: avoid anonymous functions

* fix: correct typo

scarily small - can see why this e2e coverage issue is high priority

* fix: use proper uuid format

* style: fmt

* fix: import vue correctly, get correct layout

* fix: createApp without JSON

fixes template issues

* fix: don't use default on InspectorDataVisualization

* fix: remove more .default calls

* Update src/api/api.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* Update src/plugins/plugins.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* Update src/plugins/plugins.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* fix: suggestions

* fix: drop unnecessary this.annotation initialization

* fix: move all initialization calls to constructor

* refactor: move vue dist import to webpack alias

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
This commit is contained in:
Tristan F
2023-12-27 15:15:51 -05:00
committed by GitHub
parent 715a44864e
commit 2e03bc394c
102 changed files with 11730 additions and 12016 deletions

View File

@ -76,7 +76,8 @@ const config = {
MCT: path.join(projectRootDir, 'src/MCT'), MCT: path.join(projectRootDir, 'src/MCT'),
testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'), testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'),
objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'), objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'),
utils: path.join(projectRootDir, 'src/utils') utils: path.join(projectRootDir, 'src/utils'),
vue: 'vue/dist/vue.esm-bundler'
} }
}, },
plugins: [ plugins: [

View File

@ -1,138 +1,134 @@
define(['lodash'], function (_) { const METADATA_BY_TYPE = {
var METADATA_BY_TYPE = { generator: {
generator: { values: [
values: [ {
{ key: 'name',
key: 'name', name: 'Name',
name: 'Name', format: 'string'
format: 'string' },
}, {
{ key: 'utc',
key: 'utc', name: 'Time',
name: 'Time', format: 'utc',
format: 'utc', hints: {
hints: { domain: 1
domain: 1
}
},
{
key: 'yesterday',
name: 'Yesterday',
format: 'utc',
hints: {
domain: 2
}
},
{
key: 'wavelengths',
name: 'Wavelength',
unit: 'nm',
format: 'string[]',
hints: {
range: 4
}
},
// Need to enable "LocalTimeSystem" plugin to make use of this
// {
// key: "local",
// name: "Time",
// format: "local-format",
// source: "utc",
// hints: {
// domain: 3
// }
// },
{
key: 'sin',
name: 'Sine',
unit: 'Hz',
formatString: '%0.2f',
hints: {
range: 1
}
},
{
key: 'cos',
name: 'Cosine',
unit: 'deg',
formatString: '%0.2f',
hints: {
range: 2
}
},
{
key: 'intensities',
name: 'Intensities',
format: 'number[]',
hints: {
range: 3
}
} }
] },
}, {
'example.state-generator': { key: 'yesterday',
values: [ name: 'Yesterday',
{ format: 'utc',
key: 'name', hints: {
name: 'Name', domain: 2
format: 'string'
},
{
key: 'utc',
name: 'Time',
format: 'utc',
hints: {
domain: 1
}
},
{
key: 'local',
name: 'Time',
format: 'utc',
source: 'utc',
hints: {
domain: 2
}
},
{
key: 'state',
source: 'value',
name: 'State',
format: 'enum',
enumerations: [
{
value: 0,
string: 'OFF'
},
{
value: 1,
string: 'ON'
}
],
hints: {
range: 1
}
},
{
key: 'value',
name: 'Value',
hints: {
range: 2
}
} }
] },
} {
}; key: 'wavelengths',
name: 'Wavelength',
unit: 'nm',
format: 'string[]',
hints: {
range: 4
}
},
// Need to enable "LocalTimeSystem" plugin to make use of this
// {
// key: "local",
// name: "Time",
// format: "local-format",
// source: "utc",
// hints: {
// domain: 3
// }
// },
{
key: 'sin',
name: 'Sine',
unit: 'Hz',
formatString: '%0.2f',
hints: {
range: 1
}
},
{
key: 'cos',
name: 'Cosine',
unit: 'deg',
formatString: '%0.2f',
hints: {
range: 2
}
},
{
key: 'intensities',
name: 'Intensities',
format: 'number[]',
hints: {
range: 3
}
}
]
},
'example.state-generator': {
values: [
{
key: 'name',
name: 'Name',
format: 'string'
},
{
key: 'utc',
name: 'Time',
format: 'utc',
hints: {
domain: 1
}
},
{
key: 'local',
name: 'Time',
format: 'utc',
source: 'utc',
hints: {
domain: 2
}
},
{
key: 'state',
source: 'value',
name: 'State',
format: 'enum',
enumerations: [
{
value: 0,
string: 'OFF'
},
{
value: 1,
string: 'ON'
}
],
hints: {
range: 1
}
},
{
key: 'value',
name: 'Value',
hints: {
range: 2
}
}
]
}
};
function GeneratorMetadataProvider() {} export default function GeneratorMetadataProvider() {}
GeneratorMetadataProvider.prototype.supportsMetadata = function (domainObject) { GeneratorMetadataProvider.prototype.supportsMetadata = function (domainObject) {
return Object.prototype.hasOwnProperty.call(METADATA_BY_TYPE, domainObject.type); return Object.prototype.hasOwnProperty.call(METADATA_BY_TYPE, domainObject.type);
}; };
GeneratorMetadataProvider.prototype.getMetadata = function (domainObject) { GeneratorMetadataProvider.prototype.getMetadata = function (domainObject) {
return Object.assign({}, domainObject.telemetry, METADATA_BY_TYPE[domainObject.type]); return Object.assign({}, domainObject.telemetry, METADATA_BY_TYPE[domainObject.type]);
}; };
return GeneratorMetadataProvider;
});

View File

@ -20,86 +20,84 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./WorkerInterface'], function (WorkerInterface) { import WorkerInterface from './WorkerInterface';
var REQUEST_DEFAULTS = {
amplitude: 1,
period: 10,
offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0,
loadDelay: 0,
infinityValues: false,
exceedFloat32: false
};
function GeneratorProvider(openmct, StalenessProvider) { const REQUEST_DEFAULTS = {
this.openmct = openmct; amplitude: 1,
this.workerInterface = new WorkerInterface(openmct, StalenessProvider); period: 10,
} offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0,
loadDelay: 0,
infinityValues: false,
exceedFloat32: false
};
GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) { export default function GeneratorProvider(openmct, StalenessProvider) {
return domainObject.type === 'generator'; this.openmct = openmct;
}; this.workerInterface = new WorkerInterface(openmct, StalenessProvider);
}
GeneratorProvider.prototype.supportsRequest = GeneratorProvider.prototype.supportsSubscribe = GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
GeneratorProvider.prototype.canProvideTelemetry; return domainObject.type === 'generator';
};
GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request) { GeneratorProvider.prototype.supportsRequest = GeneratorProvider.prototype.supportsSubscribe =
var props = [ GeneratorProvider.prototype.canProvideTelemetry;
'amplitude',
'period',
'offset',
'dataRateInHz',
'randomness',
'phase',
'loadDelay',
'infinityValues',
'exceedFloat32'
];
request = request || {}; GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request) {
var props = [
'amplitude',
'period',
'offset',
'dataRateInHz',
'randomness',
'phase',
'loadDelay',
'infinityValues',
'exceedFloat32'
];
var workerRequest = {}; request = request || {};
props.forEach(function (prop) { var workerRequest = {};
if (
domainObject.telemetry &&
Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)
) {
workerRequest[prop] = domainObject.telemetry[prop];
}
if (request && Object.prototype.hasOwnProperty.call(request, prop)) { props.forEach(function (prop) {
workerRequest[prop] = request[prop]; if (
} domainObject.telemetry &&
Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)
) {
workerRequest[prop] = domainObject.telemetry[prop];
}
if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) { if (request && Object.prototype.hasOwnProperty.call(request, prop)) {
workerRequest[prop] = REQUEST_DEFAULTS[prop]; workerRequest[prop] = request[prop];
} }
workerRequest[prop] = Number(workerRequest[prop]); if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) {
}); workerRequest[prop] = REQUEST_DEFAULTS[prop];
}
workerRequest.id = this.openmct.objects.makeKeyString(domainObject.identifier); workerRequest[prop] = Number(workerRequest[prop]);
workerRequest.name = domainObject.name; });
return workerRequest; workerRequest.id = this.openmct.objects.makeKeyString(domainObject.identifier);
}; workerRequest.name = domainObject.name;
GeneratorProvider.prototype.request = function (domainObject, request) { return workerRequest;
var workerRequest = this.makeWorkerRequest(domainObject, request); };
workerRequest.start = request.start;
workerRequest.end = request.end;
return this.workerInterface.request(workerRequest); GeneratorProvider.prototype.request = function (domainObject, request) {
}; var workerRequest = this.makeWorkerRequest(domainObject, request);
workerRequest.start = request.start;
workerRequest.end = request.end;
GeneratorProvider.prototype.subscribe = function (domainObject, callback) { return this.workerInterface.request(workerRequest);
var workerRequest = this.makeWorkerRequest(domainObject, {}); };
return this.workerInterface.subscribe(workerRequest, callback); GeneratorProvider.prototype.subscribe = function (domainObject, callback) {
}; var workerRequest = this.makeWorkerRequest(domainObject, {});
return GeneratorProvider; return this.workerInterface.subscribe(workerRequest, callback);
}); };

View File

@ -20,147 +20,143 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { var PURPLE = {
var PURPLE = { sin: 2.2,
sin: 2.2, cos: 2.2
cos: 2.2 },
RED = {
sin: 0.9,
cos: 0.9
},
ORANGE = {
sin: 0.7,
cos: 0.7
},
YELLOW = {
sin: 0.5,
cos: 0.5
},
CYAN = {
sin: 0.45,
cos: 0.45
},
LIMITS = {
rh: {
cssClass: 'is-limit--upr is-limit--red',
low: RED,
high: Number.POSITIVE_INFINITY,
name: 'Red High'
}, },
RED = { rl: {
sin: 0.9, cssClass: 'is-limit--lwr is-limit--red',
cos: 0.9 high: -RED,
low: Number.NEGATIVE_INFINITY,
name: 'Red Low'
}, },
ORANGE = { yh: {
sin: 0.7, cssClass: 'is-limit--upr is-limit--yellow',
cos: 0.7 low: YELLOW,
high: RED,
name: 'Yellow High'
}, },
YELLOW = { yl: {
sin: 0.5, cssClass: 'is-limit--lwr is-limit--yellow',
cos: 0.5 low: -RED,
}, high: -YELLOW,
CYAN = { name: 'Yellow Low'
sin: 0.45, }
cos: 0.45
},
LIMITS = {
rh: {
cssClass: 'is-limit--upr is-limit--red',
low: RED,
high: Number.POSITIVE_INFINITY,
name: 'Red High'
},
rl: {
cssClass: 'is-limit--lwr is-limit--red',
high: -RED,
low: Number.NEGATIVE_INFINITY,
name: 'Red Low'
},
yh: {
cssClass: 'is-limit--upr is-limit--yellow',
low: YELLOW,
high: RED,
name: 'Yellow High'
},
yl: {
cssClass: 'is-limit--lwr is-limit--yellow',
low: -RED,
high: -YELLOW,
name: 'Yellow Low'
}
};
function SinewaveLimitProvider() {}
SinewaveLimitProvider.prototype.supportsLimits = function (domainObject) {
return domainObject.type === 'generator';
}; };
SinewaveLimitProvider.prototype.getLimitEvaluator = function (domainObject) { export default function SinewaveLimitProvider() {}
return {
evaluate: function (datum, valueMetadata) {
var range = valueMetadata && valueMetadata.key;
if (datum[range] > RED[range]) { SinewaveLimitProvider.prototype.supportsLimits = function (domainObject) {
return LIMITS.rh; return domainObject.type === 'generator';
} };
if (datum[range] < -RED[range]) { SinewaveLimitProvider.prototype.getLimitEvaluator = function (domainObject) {
return LIMITS.rl; return {
} evaluate: function (datum, valueMetadata) {
var range = valueMetadata && valueMetadata.key;
if (datum[range] > YELLOW[range]) { if (datum[range] > RED[range]) {
return LIMITS.yh; return LIMITS.rh;
}
if (datum[range] < -YELLOW[range]) {
return LIMITS.yl;
}
} }
};
};
SinewaveLimitProvider.prototype.getLimits = function (domainObject) { if (datum[range] < -RED[range]) {
return { return LIMITS.rl;
limits: function () { }
return Promise.resolve({
WATCH: { if (datum[range] > YELLOW[range]) {
low: { return LIMITS.yh;
color: 'cyan', }
sin: -CYAN.sin,
cos: -CYAN.cos if (datum[range] < -YELLOW[range]) {
}, return LIMITS.yl;
high: { }
color: 'cyan', }
...CYAN };
} };
SinewaveLimitProvider.prototype.getLimits = function (domainObject) {
return {
limits: function () {
return Promise.resolve({
WATCH: {
low: {
color: 'cyan',
sin: -CYAN.sin,
cos: -CYAN.cos
}, },
WARNING: { high: {
low: { color: 'cyan',
color: 'yellow', ...CYAN
sin: -YELLOW.sin,
cos: -YELLOW.cos
},
high: {
color: 'yellow',
...YELLOW
}
},
DISTRESS: {
low: {
color: 'orange',
sin: -ORANGE.sin,
cos: -ORANGE.cos
},
high: {
color: 'orange',
...ORANGE
}
},
CRITICAL: {
low: {
color: 'red',
sin: -RED.sin,
cos: -RED.cos
},
high: {
color: 'red',
...RED
}
},
SEVERE: {
low: {
color: 'purple',
sin: -PURPLE.sin,
cos: -PURPLE.cos
},
high: {
color: 'purple',
...PURPLE
}
} }
}); },
} WARNING: {
}; low: {
color: 'yellow',
sin: -YELLOW.sin,
cos: -YELLOW.cos
},
high: {
color: 'yellow',
...YELLOW
}
},
DISTRESS: {
low: {
color: 'orange',
sin: -ORANGE.sin,
cos: -ORANGE.cos
},
high: {
color: 'orange',
...ORANGE
}
},
CRITICAL: {
low: {
color: 'red',
sin: -RED.sin,
cos: -RED.cos
},
high: {
color: 'red',
...RED
}
},
SEVERE: {
low: {
color: 'purple',
sin: -PURPLE.sin,
cos: -PURPLE.cos
},
high: {
color: 'purple',
...PURPLE
}
}
});
}
}; };
};
return SinewaveLimitProvider;
});

View File

@ -20,56 +20,52 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { export default function StateGeneratorProvider() {}
function StateGeneratorProvider() {}
function pointForTimestamp(timestamp, duration, name) { function pointForTimestamp(timestamp, duration, name) {
return { return {
name: name, name: name,
utc: Math.floor(timestamp / duration) * duration, utc: Math.floor(timestamp / duration) * duration,
value: Math.floor(timestamp / duration) % 2 value: Math.floor(timestamp / duration) % 2
}; };
}
StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'example.state-generator';
};
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var duration = domainObject.telemetry.duration * 1000;
var interval = setInterval(function () {
var now = Date.now();
var datum = pointForTimestamp(now, duration, domainObject.name);
datum.value = String(datum.value);
callback(datum);
}, duration);
return function () {
clearInterval(interval);
};
};
StateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
return domainObject.type === 'example.state-generator';
};
StateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = Math.min(Date.now(), options.end); // no future values
var duration = domainObject.telemetry.duration * 1000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;
} }
StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) { var data = [];
return domainObject.type === 'example.state-generator'; while (start <= end && data.length < 5000) {
}; data.push(pointForTimestamp(start, duration, domainObject.name));
start += duration;
}
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) { return Promise.resolve(data);
var duration = domainObject.telemetry.duration * 1000; };
var interval = setInterval(function () {
var now = Date.now();
var datum = pointForTimestamp(now, duration, domainObject.name);
datum.value = String(datum.value);
callback(datum);
}, duration);
return function () {
clearInterval(interval);
};
};
StateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
return domainObject.type === 'example.state-generator';
};
StateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = Math.min(Date.now(), options.end); // no future values
var duration = domainObject.telemetry.duration * 1000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;
}
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, duration, domainObject.name));
start += duration;
}
return Promise.resolve(data);
};
return StateGeneratorProvider;
});

View File

@ -20,88 +20,86 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['uuid'], function ({ v4: uuid }) { import { v4 as uuid } from 'uuid';
function WorkerInterface(openmct, StalenessProvider) {
// eslint-disable-next-line no-undef
const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`;
this.StalenessProvider = StalenessProvider;
this.worker = new Worker(workerUrl);
this.worker.onmessage = this.onMessage.bind(this);
this.callbacks = {};
this.staleTelemetryIds = {};
this.watchStaleness(); export default function WorkerInterface(openmct, StalenessProvider) {
// eslint-disable-next-line no-undef
const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`;
this.StalenessProvider = StalenessProvider;
this.worker = new Worker(workerUrl);
this.worker.onmessage = this.onMessage.bind(this);
this.callbacks = {};
this.staleTelemetryIds = {};
this.watchStaleness();
}
WorkerInterface.prototype.watchStaleness = function () {
this.StalenessProvider.on('stalenessEvent', ({ id, isStale }) => {
this.staleTelemetryIds[id] = isStale;
});
};
WorkerInterface.prototype.onMessage = function (message) {
message = message.data;
var callback = this.callbacks[message.id];
if (callback) {
callback(message);
}
};
WorkerInterface.prototype.dispatch = function (request, data, callback) {
var message = {
request: request,
data: data,
id: uuid()
};
if (callback) {
this.callbacks[message.id] = callback;
} }
WorkerInterface.prototype.watchStaleness = function () { this.worker.postMessage(message);
this.StalenessProvider.on('stalenessEvent', ({ id, isStale }) => {
this.staleTelemetryIds[id] = isStale;
});
};
WorkerInterface.prototype.onMessage = function (message) { return message.id;
message = message.data; };
var callback = this.callbacks[message.id];
if (callback) {
callback(message);
}
};
WorkerInterface.prototype.dispatch = function (request, data, callback) { WorkerInterface.prototype.request = function (request) {
var message = { var deferred = {};
request: request, var promise = new Promise(function (resolve, reject) {
data: data, deferred.resolve = resolve;
id: uuid() deferred.reject = reject;
}; });
var messageId;
if (callback) { let self = this;
this.callbacks[message.id] = callback; function callback(message) {
if (message.error) {
deferred.reject(message.error);
} else {
deferred.resolve(message.data);
} }
this.worker.postMessage(message); delete self.callbacks[messageId];
}
return message.id; messageId = this.dispatch('request', request, callback.bind(this));
};
WorkerInterface.prototype.request = function (request) { return promise;
var deferred = {}; };
var promise = new Promise(function (resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});
var messageId;
let self = this; WorkerInterface.prototype.subscribe = function (request, cb) {
function callback(message) { const { id, loadDelay } = request;
if (message.error) { const messageId = this.dispatch('subscribe', request, (message) => {
deferred.reject(message.error); if (!this.staleTelemetryIds[id]) {
} else { setTimeout(() => cb(message.data), Math.max(loadDelay, 0));
deferred.resolve(message.data);
}
delete self.callbacks[messageId];
} }
});
messageId = this.dispatch('request', request, callback.bind(this)); return function () {
this.dispatch('unsubscribe', {
return promise; id: messageId
};
WorkerInterface.prototype.subscribe = function (request, cb) {
const { id, loadDelay } = request;
const messageId = this.dispatch('subscribe', request, (message) => {
if (!this.staleTelemetryIds[id]) {
setTimeout(() => cb(message.data), Math.max(loadDelay, 0));
}
}); });
delete this.callbacks[messageId];
return function () { }.bind(this);
this.dispatch('unsubscribe', { };
id: messageId
});
delete this.callbacks[messageId];
}.bind(this);
};
return WorkerInterface;
});

View File

@ -75,7 +75,7 @@ if (document.currentScript) {
* @property {OpenMCTComponent[]} components * @property {OpenMCTComponent[]} components
*/ */
const MCT = require('./src/MCT'); const { MCT } = require('./src/MCT');
/** @type {OpenMCT} */ /** @type {OpenMCT} */
const openmct = new MCT(); const openmct = new MCT();

View File

@ -20,72 +20,66 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/* eslint-disable no-undef */ /* eslint-disable no-undef */
define([ import EventEmitter from 'EventEmitter';
'EventEmitter', import { createApp, markRaw } from 'vue';
'./api/api',
'./api/overlays/OverlayAPI',
'./api/tooltips/ToolTipAPI',
'./selection/Selection',
'./plugins/plugins',
'./ui/registries/ViewRegistry',
'./plugins/imagery/plugin',
'./ui/registries/InspectorViewRegistry',
'./ui/registries/ToolbarRegistry',
'./ui/router/ApplicationRouter',
'./ui/router/Browse',
'./ui/layout/AppLayout.vue',
'./ui/preview/plugin',
'./api/Branding',
'./plugins/licenses/plugin',
'./plugins/remove/plugin',
'./plugins/move/plugin',
'./plugins/linkAction/plugin',
'./plugins/duplicate/plugin',
'./plugins/importFromJSONAction/plugin',
'./plugins/exportAsJSONAction/plugin',
'vue'
], function (
EventEmitter,
api,
OverlayAPI,
ToolTipAPI,
Selection,
plugins,
ViewRegistry,
ImageryPlugin,
InspectorViewRegistry,
ToolbarRegistry,
ApplicationRouter,
Browse,
Layout,
PreviewPlugin,
BrandingAPI,
LicensesPlugin,
RemoveActionPlugin,
MoveActionPlugin,
LinkActionPlugin,
DuplicateActionPlugin,
ImportFromJSONAction,
ExportAsJSONAction,
Vue
) {
/**
* Open MCT is an extensible web application for building mission
* control user interfaces. This module is itself an instance of
* [MCT]{@link module:openmct.MCT}, which provides an interface for
* configuring and executing the application.
*
* @exports openmct
*/
/** import ActionsAPI from './api/actions/ActionsAPI';
* The Open MCT application. This may be configured by installing plugins import AnnotationAPI from './api/annotation/AnnotationAPI';
* or registering extensions before the application is started. import BrandingAPI from './api/Branding';
* @constructor import CompositionAPI from './api/composition/CompositionAPI';
* @memberof module:openmct import EditorAPI from './api/Editor';
*/ import FaultManagementAPI from './api/faultmanagement/FaultManagementAPI';
function MCT() { import FormsAPI from './api/forms/FormsAPI';
import IndicatorAPI from './api/indicators/IndicatorAPI';
import MenuAPI from './api/menu/MenuAPI';
import NotificationAPI from './api/notifications/NotificationAPI';
import ObjectAPI from './api/objects/ObjectAPI';
import OverlayAPI from './api/overlays/OverlayAPI';
import PriorityAPI from './api/priority/PriorityAPI';
import StatusAPI from './api/status/StatusAPI';
import TelemetryAPI from './api/telemetry/TelemetryAPI';
import TimeAPI from './api/time/TimeAPI';
import ToolTipAPI from './api/tooltips/ToolTipAPI';
import TypeRegistry from './api/types/TypeRegistry';
import UserAPI from './api/user/UserAPI';
import DuplicateActionPlugin from './plugins/duplicate/plugin';
import ExportAsJSONAction from './plugins/exportAsJSONAction/plugin';
import ImageryPlugin from './plugins/imagery/plugin';
import ImportFromJSONAction from './plugins/importFromJSONAction/plugin';
import LicensesPlugin from './plugins/licenses/plugin';
import LinkActionPlugin from './plugins/linkAction/plugin';
import MoveActionPlugin from './plugins/move/plugin';
import plugins from './plugins/plugins';
import RemoveActionPlugin from './plugins/remove/plugin';
import Selection from './selection/Selection';
import Layout from './ui/layout/AppLayout.vue';
import PreviewPlugin from './ui/preview/plugin';
import InspectorViewRegistry from './ui/registries/InspectorViewRegistry';
import ToolbarRegistry from './ui/registries/ToolbarRegistry';
import ViewRegistry from './ui/registries/ViewRegistry';
import ApplicationRouter from './ui/router/ApplicationRouter';
import Browse from './ui/router/Browse';
/**
* Open MCT is an extensible web application for building mission
* control user interfaces. This module is itself an instance of
* [MCT]{@link module:openmct.MCT}, which provides an interface for
* configuring and executing the application.
*
* @exports openmct
*/
/**
* The Open MCT application. This may be configured by installing plugins
* or registering extensions before the application is started.
* @constructor
* @memberof module:openmct
*/
export class MCT extends EventEmitter {
constructor() {
super();
EventEmitter.call(this); EventEmitter.call(this);
this.buildInfo = { this.buildInfo = {
version: __OPENMCT_VERSION__, version: __OPENMCT_VERSION__,
buildDate: __OPENMCT_BUILD_DATE__, buildDate: __OPENMCT_BUILD_DATE__,
@ -95,169 +89,140 @@ define([
this.destroy = this.destroy.bind(this); this.destroy = this.destroy.bind(this);
this.defaultClock = 'local'; this.defaultClock = 'local';
[
/**
* Tracks current selection state of the application.
* @private
*/
['selection', () => new Selection.default(this)],
/** this.plugins = plugins;
* MCT's time conductor, which may be used to synchronize view contents
* for telemetry- or time-based views.
* @type {module:openmct.TimeConductor}
* @memberof module:openmct.MCT#
* @name conductor
*/
['time', () => new api.TimeAPI(this)],
/** /**
* An interface for interacting with the composition of domain objects. * Tracks current selection state of the application.
* The composition of a domain object is the list of other domain * @private
* objects it "contains" (for instance, that should be displayed */
* beneath it in the tree.) this.selection = new Selection(this);
*
* `composition` may be called as a function, in which case it acts
* as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
*
* @type {module:openmct.CompositionAPI}
* @memberof module:openmct.MCT#
* @name composition
*/
['composition', () => new api.CompositionAPI.default(this)],
/** /**
* Registry for views of domain objects which should appear in the * MCT's time conductor, which may be used to synchronize view contents
* main viewing area. * for telemetry- or time-based views.
* * @type {module:openmct.TimeConductor}
* @type {module:openmct.ViewRegistry} * @memberof module:openmct.MCT#
* @memberof module:openmct.MCT# * @name conductor
* @name objectViews */
*/ this.time = new TimeAPI(this);
['objectViews', () => new ViewRegistry()],
/** /**
* Registry for views which should appear in the Inspector area. * An interface for interacting with the composition of domain objects.
* These views will be chosen based on the selection state. * The composition of a domain object is the list of other domain
* * objects it "contains" (for instance, that should be displayed
* @type {module:openmct.InspectorViewRegistry} * beneath it in the tree.)
* @memberof module:openmct.MCT# *
* @name inspectorViews * `composition` may be called as a function, in which case it acts
*/ * as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
['inspectorViews', () => new InspectorViewRegistry.default()], *
* @type {module:openmct.CompositionAPI}
* @memberof module:openmct.MCT#
* @name composition
*/
this.composition = new CompositionAPI(this);
/** /**
* Registry for views which should appear in Edit Properties * Registry for views of domain objects which should appear in the
* dialogs, and similar user interface elements used for * main viewing area.
* modifying domain objects external to its regular views. *
* * @type {module:openmct.ViewRegistry}
* @type {module:openmct.ViewRegistry} * @memberof module:openmct.MCT#
* @memberof module:openmct.MCT# * @name objectViews
* @name propertyEditors */
*/ this.objectViews = new ViewRegistry();
['propertyEditors', () => new ViewRegistry()],
/** /**
* Registry for views which should appear in the toolbar area while * Registry for views which should appear in the Inspector area.
* editing. These views will be chosen based on the selection state. * These views will be chosen based on the selection state.
* *
* @type {module:openmct.ToolbarRegistry} * @type {module:openmct.InspectorViewRegistry}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name toolbars * @name inspectorViews
*/ */
['toolbars', () => new ToolbarRegistry()], this.inspectorViews = new InspectorViewRegistry();
/** /**
* Registry for domain object types which may exist within this * Registry for views which should appear in Edit Properties
* instance of Open MCT. * dialogs, and similar user interface elements used for
* * modifying domain objects external to its regular views.
* @type {module:openmct.TypeRegistry} *
* @memberof module:openmct.MCT# * @type {module:openmct.ViewRegistry}
* @name types * @memberof module:openmct.MCT#
*/ * @name propertyEditors
['types', () => new api.TypeRegistry()], */
this.propertyEditors = new ViewRegistry();
/** /**
* An interface for interacting with domain objects and the domain * Registry for views which should appear in the toolbar area while
* object hierarchy. * editing. These views will be chosen based on the selection state.
* *
* @type {module:openmct.ObjectAPI} * @type {module:openmct.ToolbarRegistry}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name objects * @name toolbars
*/ */
['objects', () => new api.ObjectAPI.default(this.types, this)], this.toolbars = new ToolbarRegistry();
/** /**
* An interface for retrieving and interpreting telemetry data associated * Registry for domain object types which may exist within this
* with a domain object. * instance of Open MCT.
* *
* @type {module:openmct.TelemetryAPI} * @type {module:openmct.TypeRegistry}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name telemetry * @name types
*/ */
['telemetry', () => new api.TelemetryAPI.default(this)], this.types = new TypeRegistry();
/** /**
* An interface for creating new indicators and changing them dynamically. * An interface for interacting with domain objects and the domain
* * object hierarchy.
* @type {module:openmct.IndicatorAPI} *
* @memberof module:openmct.MCT# * @type {module:openmct.ObjectAPI}
* @name indicators * @memberof module:openmct.MCT#
*/ * @name objects
['indicators', () => new api.IndicatorAPI(this)], */
this.objects = new ObjectAPI(this.types, this);
/** /**
* MCT's user awareness management, to enable user and * An interface for retrieving and interpreting telemetry data associated
* role specific functionality. * with a domain object.
* @type {module:openmct.UserAPI} *
* @memberof module:openmct.MCT# * @type {module:openmct.TelemetryAPI}
* @name user * @memberof module:openmct.MCT#
*/ * @name telemetry
['user', () => new api.UserAPI(this)], */
this.telemetry = new TelemetryAPI(this);
['notifications', () => new api.NotificationAPI()], /**
* An interface for creating new indicators and changing them dynamically.
*
* @type {module:openmct.IndicatorAPI}
* @memberof module:openmct.MCT#
* @name indicators
*/
this.indicators = new IndicatorAPI(this);
['editor', () => new api.EditorAPI.default(this)], /**
* MCT's user awareness management, to enable user and
* role specific functionality.
* @type {module:openmct.UserAPI}
* @memberof module:openmct.MCT#
* @name user
*/
this.user = new UserAPI(this);
['overlays', () => new OverlayAPI.default()], this.notifications = new NotificationAPI();
this.editor = new EditorAPI(this);
['tooltips', () => new ToolTipAPI.default()], this.overlays = new OverlayAPI();
this.tooltips = new ToolTipAPI();
['menus', () => new api.MenuAPI(this)], this.menus = new MenuAPI(this);
this.actions = new ActionsAPI(this);
['actions', () => new api.ActionsAPI(this)], this.status = new StatusAPI(this);
this.priority = PriorityAPI;
['status', () => new api.StatusAPI(this)], this.router = new ApplicationRouter(this);
this.faults = new FaultManagementAPI(this);
['priority', () => api.PriorityAPI], this.forms = new FormsAPI(this);
this.branding = BrandingAPI;
['router', () => new ApplicationRouter(this)],
['faults', () => new api.FaultManagementAPI.default(this)],
['forms', () => new api.FormsAPI.default(this)],
['branding', () => BrandingAPI.default],
/**
* MCT's annotation API that enables
* human-created comments and categorization linked to data products
* @type {module:openmct.AnnotationAPI}
* @memberof module:openmct.MCT#
* @name annotation
*/
['annotation', () => new api.AnnotationAPI(this)]
].forEach((apiEntry) => {
const apiName = apiEntry[0];
const apiObject = apiEntry[1]();
Object.defineProperty(this, apiName, {
value: apiObject,
enumerable: false,
configurable: false,
writable: true
});
});
/** /**
* MCT's annotation API that enables * MCT's annotation API that enables
@ -266,23 +231,23 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name annotation * @name annotation
*/ */
this.annotation = new api.AnnotationAPI(this); this.annotation = new AnnotationAPI(this);
// Plugins that are installed by default // Plugins that are installed by default
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable.default()); this.install(this.plugins.TelemetryTable());
this.install(PreviewPlugin.default()); this.install(PreviewPlugin());
this.install(LicensesPlugin.default()); this.install(LicensesPlugin());
this.install(RemoveActionPlugin.default()); this.install(RemoveActionPlugin());
this.install(MoveActionPlugin.default()); this.install(MoveActionPlugin());
this.install(LinkActionPlugin.default()); this.install(LinkActionPlugin());
this.install(DuplicateActionPlugin.default()); this.install(DuplicateActionPlugin());
this.install(ExportAsJSONAction.default()); this.install(ExportAsJSONAction());
this.install(ImportFromJSONAction.default()); this.install(ImportFromJSONAction());
this.install(this.plugins.FormActions.default()); this.install(this.plugins.FormActions());
this.install(this.plugins.FolderView()); this.install(this.plugins.FolderView());
this.install(this.plugins.Tabs()); this.install(this.plugins.Tabs());
this.install(ImageryPlugin.default()); this.install(ImageryPlugin());
this.install(this.plugins.FlexibleLayout()); this.install(this.plugins.FlexibleLayout());
this.install(this.plugins.GoToOriginalAction()); this.install(this.plugins.GoToOriginalAction());
this.install(this.plugins.OpenInNewTabAction()); this.install(this.plugins.OpenInNewTabAction());
@ -300,26 +265,20 @@ define([
this.install(this.plugins.Gauge()); this.install(this.plugins.Gauge());
this.install(this.plugins.InspectorViews()); this.install(this.plugins.InspectorViews());
} }
MCT.prototype = Object.create(EventEmitter.prototype);
MCT.prototype.MCT = MCT;
/** /**
* Set path to where assets are hosted. This should be the path to main.js. * Set path to where assets are hosted. This should be the path to main.js.
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @method setAssetPath * @method setAssetPath
*/ */
MCT.prototype.setAssetPath = function (assetPath) { setAssetPath(assetPath) {
this._assetPath = assetPath; this._assetPath = assetPath;
}; }
/** /**
* Get path to where assets are hosted. * Get path to where assets are hosted.
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @method getAssetPath * @method getAssetPath
*/ */
MCT.prototype.getAssetPath = function () { getAssetPath() {
const assetPathLength = this._assetPath && this._assetPath.length; const assetPathLength = this._assetPath && this._assetPath.length;
if (!assetPathLength) { if (!assetPathLength) {
return '/'; return '/';
@ -330,8 +289,7 @@ define([
} }
return this._assetPath; return this._assetPath;
}; }
/** /**
* Start running Open MCT. This should be called only after any plugins * Start running Open MCT. This should be called only after any plugins
* have been installed. * have been installed.
@ -341,10 +299,7 @@ define([
* @param {HTMLElement} [domElement] the DOM element in which to run * @param {HTMLElement} [domElement] the DOM element in which to run
* MCT; if undefined, MCT will be run in the body of the document * MCT; if undefined, MCT will be run in the body of the document
*/ */
MCT.prototype.start = function ( start(domElement = document.body.firstElementChild, isHeadlessMode = false) {
domElement = document.body.firstElementChild,
isHeadlessMode = false
) {
// Create element to mount Layout if it doesn't exist // Create element to mount Layout if it doesn't exist
if (domElement === null) { if (domElement === null) {
domElement = document.createElement('div'); domElement = document.createElement('div');
@ -376,20 +331,12 @@ define([
* @event start * @event start
* @memberof module:openmct.MCT~ * @memberof module:openmct.MCT~
*/ */
if (!isHeadlessMode) { if (!isHeadlessMode) {
const appLayout = Vue.createApp({ const appLayout = createApp(Layout);
components: { appLayout.provide('openmct', markRaw(this));
Layout: Layout.default
},
provide: {
openmct: Vue.markRaw(this)
},
template: '<Layout ref="layout"></Layout>'
});
const component = appLayout.mount(domElement); const component = appLayout.mount(domElement);
component.$nextTick(() => { component.$nextTick(() => {
this.layout = component.$refs.layout; this.layout = component;
this.app = appLayout; this.app = appLayout;
Browse(this); Browse(this);
window.addEventListener('beforeunload', this.destroy); window.addEventListener('beforeunload', this.destroy);
@ -402,14 +349,12 @@ define([
this.router.start(); this.router.start();
this.emit('start'); this.emit('start');
} }
}; }
startHeadless() {
MCT.prototype.startHeadless = function () {
let unreachableNode = document.createElement('div'); let unreachableNode = document.createElement('div');
return this.start(unreachableNode, true); return this.start(unreachableNode, true);
}; }
/** /**
* Install a plugin in MCT. * Install a plugin in MCT.
* *
@ -417,17 +362,13 @@ define([
* invoked with the mct instance. * invoked with the mct instance.
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
*/ */
MCT.prototype.install = function (plugin) { install(plugin) {
plugin(this); plugin(this);
}; }
MCT.prototype.destroy = function () { destroy() {
window.removeEventListener('beforeunload', this.destroy); window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy'); this.emit('destroy');
this.router.destroy(); this.router.destroy();
}; }
}
MCT.prototype.plugins = plugins;
return MCT;
});

View File

@ -20,96 +20,98 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./plugins/plugins', 'utils/testing'], function (plugins, testUtils) { import * as testUtils from 'utils/testing';
describe('MCT', function () {
let openmct;
let mockPlugin;
let mockPlugin2;
let mockListener;
beforeEach(function () { import plugins from './plugins/plugins';
mockPlugin = jasmine.createSpy('plugin');
mockPlugin2 = jasmine.createSpy('plugin2');
mockListener = jasmine.createSpy('listener');
openmct = testUtils.createOpenMct(); describe('MCT', function () {
let openmct;
let mockPlugin;
let mockPlugin2;
let mockListener;
openmct.install(mockPlugin); beforeEach(function () {
openmct.install(mockPlugin2); mockPlugin = jasmine.createSpy('plugin');
openmct.on('start', mockListener); mockPlugin2 = jasmine.createSpy('plugin2');
mockListener = jasmine.createSpy('listener');
openmct = testUtils.createOpenMct();
openmct.install(mockPlugin);
openmct.install(mockPlugin2);
openmct.on('start', mockListener);
});
// Clean up the dirty singleton.
afterEach(function () {
return testUtils.resetApplicationState(openmct);
});
it('exposes plugins', function () {
expect(openmct.plugins).toEqual(plugins);
});
it('does not issue a start event before started', function () {
expect(mockListener).not.toHaveBeenCalled();
});
describe('start', function () {
let appHolder;
beforeEach(function (done) {
appHolder = document.createElement('div');
openmct.on('start', done);
openmct.start(appHolder);
}); });
// Clean up the dirty singleton. it('calls plugins for configuration', function () {
afterEach(function () { expect(mockPlugin).toHaveBeenCalledWith(openmct);
return testUtils.resetApplicationState(openmct); expect(mockPlugin2).toHaveBeenCalledWith(openmct);
}); });
it('exposes plugins', function () { it('emits a start event', function () {
expect(openmct.plugins).toEqual(plugins); expect(mockListener).toHaveBeenCalled();
}); });
it('does not issue a start event before started', function () { it('Renders the application into the provided container element', function () {
expect(mockListener).not.toHaveBeenCalled(); let openMctShellElements = appHolder.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(1);
});
});
describe('startHeadless', function () {
beforeEach(function (done) {
openmct.on('start', done);
openmct.startHeadless();
}); });
describe('start', function () { it('calls plugins for configuration', function () {
let appHolder; expect(mockPlugin).toHaveBeenCalledWith(openmct);
beforeEach(function (done) { expect(mockPlugin2).toHaveBeenCalledWith(openmct);
appHolder = document.createElement('div');
openmct.on('start', done);
openmct.start(appHolder);
});
it('calls plugins for configuration', function () {
expect(mockPlugin).toHaveBeenCalledWith(openmct);
expect(mockPlugin2).toHaveBeenCalledWith(openmct);
});
it('emits a start event', function () {
expect(mockListener).toHaveBeenCalled();
});
it('Renders the application into the provided container element', function () {
let openMctShellElements = appHolder.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(1);
});
}); });
describe('startHeadless', function () { it('emits a start event', function () {
beforeEach(function (done) { expect(mockListener).toHaveBeenCalled();
openmct.on('start', done);
openmct.startHeadless();
});
it('calls plugins for configuration', function () {
expect(mockPlugin).toHaveBeenCalledWith(openmct);
expect(mockPlugin2).toHaveBeenCalledWith(openmct);
});
it('emits a start event', function () {
expect(mockListener).toHaveBeenCalled();
});
it('Does not render Open MCT', function () {
let openMctShellElements = document.body.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(0);
});
}); });
describe('setAssetPath', function () { it('Does not render Open MCT', function () {
let testAssetPath; let openMctShellElements = document.body.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(0);
});
});
it('configures the path for assets', function () { describe('setAssetPath', function () {
testAssetPath = 'some/path/'; let testAssetPath;
openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath);
});
it('adds a trailing /', function () { it('configures the path for assets', function () {
testAssetPath = 'some/path'; testAssetPath = 'some/path/';
openmct.setAssetPath(testAssetPath); openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath + '/'); expect(openmct.getAssetPath()).toBe(testAssetPath);
}); });
it('adds a trailing /', function () {
testAssetPath = 'some/path';
openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath + '/');
}); });
}); });
}); });

View File

@ -20,24 +20,24 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([ import ActionsAPI from './actions/ActionsAPI';
'./actions/ActionsAPI', import AnnotationAPI from './annotation/AnnotationAPI';
'./composition/CompositionAPI', import CompositionAPI from './composition/CompositionAPI';
'./Editor', import EditorAPI from './Editor';
'./faultmanagement/FaultManagementAPI', import FaultManagementAPI from './faultmanagement/FaultManagementAPI';
'./forms/FormsAPI', import FormsAPI from './forms/FormsAPI';
'./indicators/IndicatorAPI', import IndicatorAPI from './indicators/IndicatorAPI';
'./menu/MenuAPI', import MenuAPI from './menu/MenuAPI';
'./notifications/NotificationAPI', import NotificationAPI from './notifications/NotificationAPI';
'./objects/ObjectAPI', import ObjectAPI from './objects/ObjectAPI';
'./priority/PriorityAPI', import PriorityAPI from './priority/PriorityAPI';
'./status/StatusAPI', import StatusAPI from './status/StatusAPI';
'./telemetry/TelemetryAPI', import TelemetryAPI from './telemetry/TelemetryAPI';
'./time/TimeAPI', import TimeAPI from './time/TimeAPI';
'./types/TypeRegistry', import TypeRegistry from './types/TypeRegistry';
'./user/UserAPI', import UserAPI from './user/UserAPI';
'./annotation/AnnotationAPI'
], function ( export default {
ActionsAPI, ActionsAPI,
CompositionAPI, CompositionAPI,
EditorAPI, EditorAPI,
@ -54,23 +54,4 @@ define([
TypeRegistry, TypeRegistry,
UserAPI, UserAPI,
AnnotationAPI AnnotationAPI
) { };
return {
ActionsAPI: ActionsAPI.default,
CompositionAPI: CompositionAPI,
EditorAPI: EditorAPI,
FaultManagementAPI: FaultManagementAPI,
FormsAPI: FormsAPI,
IndicatorAPI: IndicatorAPI.default,
MenuAPI: MenuAPI.default,
NotificationAPI: NotificationAPI.default,
ObjectAPI: ObjectAPI,
PriorityAPI: PriorityAPI.default,
StatusAPI: StatusAPI.default,
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry.default,
UserAPI: UserAPI.default,
AnnotationAPI: AnnotationAPI.default
};
});

View File

@ -20,163 +20,161 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { /**
/** * Utility for checking if a thing is an Open MCT Identifier.
* Utility for checking if a thing is an Open MCT Identifier. * @private
* @private */
*/ function isIdentifier(thing) {
function isIdentifier(thing) { return (
return ( typeof thing === 'object' &&
typeof thing === 'object' && Object.prototype.hasOwnProperty.call(thing, 'key') &&
Object.prototype.hasOwnProperty.call(thing, 'key') && Object.prototype.hasOwnProperty.call(thing, 'namespace')
Object.prototype.hasOwnProperty.call(thing, 'namespace') );
); }
/**
* Utility for checking if a thing is a key string. Not perfect.
* @private
*/
function isKeyString(thing) {
return typeof thing === 'string';
}
/**
* Convert a keyString into an Open MCT Identifier, ex:
* 'scratch:root' ==> {namespace: 'scratch', key: 'root'}
*
* Idempotent.
*
* @param keyString
* @returns identifier
*/
function parseKeyString(keyString) {
if (isIdentifier(keyString)) {
return keyString;
} }
/** let namespace = '';
* Utility for checking if a thing is a key string. Not perfect. let key = keyString;
* @private for (let i = 0; i < key.length; i++) {
*/ if (key[i] === '\\' && key[i + 1] === ':') {
function isKeyString(thing) { i++; // skip escape character.
return typeof thing === 'string'; } else if (key[i] === ':') {
key = key.slice(i + 1);
break;
}
namespace += key[i];
} }
/** if (keyString === namespace) {
* Convert a keyString into an Open MCT Identifier, ex: namespace = '';
* 'scratch:root' ==> {namespace: 'scratch', key: 'root'}
*
* Idempotent.
*
* @param keyString
* @returns identifier
*/
function parseKeyString(keyString) {
if (isIdentifier(keyString)) {
return keyString;
}
let namespace = '';
let key = keyString;
for (let i = 0; i < key.length; i++) {
if (key[i] === '\\' && key[i + 1] === ':') {
i++; // skip escape character.
} else if (key[i] === ':') {
key = key.slice(i + 1);
break;
}
namespace += key[i];
}
if (keyString === namespace) {
namespace = '';
}
return {
namespace: namespace,
key: key
};
}
/**
* Convert an Open MCT Identifier into a keyString, ex:
* {namespace: 'scratch', key: 'root'} ==> 'scratch:root'
*
* Idempotent
*
* @param identifier
* @returns keyString
*/
function makeKeyString(identifier) {
if (!identifier) {
throw new Error('Cannot make key string from null identifier');
}
if (isKeyString(identifier)) {
return identifier;
}
if (!identifier.namespace) {
return identifier.key;
}
return [identifier.namespace.replace(/:/g, '\\:'), identifier.key].join(':');
}
/**
* Convert a new domain object into an old format model, removing the
* identifier and converting the composition array from Open MCT Identifiers
* to old format keyStrings.
*
* @param domainObject
* @returns oldFormatModel
*/
function toOldFormat(model) {
model = JSON.parse(JSON.stringify(model));
delete model.identifier;
if (model.composition) {
model.composition = model.composition.map(makeKeyString);
}
return model;
}
/**
* Convert an old format domain object model into a new format domain
* object. Adds an identifier using the provided keyString, and converts
* the composition array to utilize Open MCT Identifiers.
*
* @param model
* @param keyString
* @returns domainObject
*/
function toNewFormat(model, keyString) {
model = JSON.parse(JSON.stringify(model));
model.identifier = parseKeyString(keyString);
if (model.composition) {
model.composition = model.composition.map(parseKeyString);
}
return model;
}
/**
* Compare two Open MCT Identifiers, returning true if they are equal.
*
* @param identifier
* @param otherIdentifier
* @returns Boolean true if identifiers are equal.
*/
function identifierEquals(a, b) {
return a.key === b.key && a.namespace === b.namespace;
}
/**
* Compare two domain objects, return true if they're the same object.
* Equality is determined by identifier.
*
* @param domainObject
* @param otherDomainOBject
* @returns Boolean true if objects are equal.
*/
function objectEquals(a, b) {
return identifierEquals(a.identifier, b.identifier);
}
function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
} }
return { return {
isIdentifier: isIdentifier, namespace: namespace,
toOldFormat: toOldFormat, key: key
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString,
equals: objectEquals,
identifierEquals: identifierEquals,
refresh: refresh
}; };
}); }
/**
* Convert an Open MCT Identifier into a keyString, ex:
* {namespace: 'scratch', key: 'root'} ==> 'scratch:root'
*
* Idempotent
*
* @param identifier
* @returns keyString
*/
function makeKeyString(identifier) {
if (!identifier) {
throw new Error('Cannot make key string from null identifier');
}
if (isKeyString(identifier)) {
return identifier;
}
if (!identifier.namespace) {
return identifier.key;
}
return [identifier.namespace.replace(/:/g, '\\:'), identifier.key].join(':');
}
/**
* Convert a new domain object into an old format model, removing the
* identifier and converting the composition array from Open MCT Identifiers
* to old format keyStrings.
*
* @param domainObject
* @returns oldFormatModel
*/
function toOldFormat(model) {
model = JSON.parse(JSON.stringify(model));
delete model.identifier;
if (model.composition) {
model.composition = model.composition.map(makeKeyString);
}
return model;
}
/**
* Convert an old format domain object model into a new format domain
* object. Adds an identifier using the provided keyString, and converts
* the composition array to utilize Open MCT Identifiers.
*
* @param model
* @param keyString
* @returns domainObject
*/
function toNewFormat(model, keyString) {
model = JSON.parse(JSON.stringify(model));
model.identifier = parseKeyString(keyString);
if (model.composition) {
model.composition = model.composition.map(parseKeyString);
}
return model;
}
/**
* Compare two Open MCT Identifiers, returning true if they are equal.
*
* @param identifier
* @param otherIdentifier
* @returns Boolean true if identifiers are equal.
*/
function identifierEquals(a, b) {
return a.key === b.key && a.namespace === b.namespace;
}
/**
* Compare two domain objects, return true if they're the same object.
* Equality is determined by identifier.
*
* @param domainObject
* @param otherDomainOBject
* @returns Boolean true if objects are equal.
*/
function objectEquals(a, b) {
return identifierEquals(a.identifier, b.identifier);
}
function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
}
export default {
isIdentifier: isIdentifier,
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString,
equals: objectEquals,
identifierEquals: identifierEquals,
refresh: refresh
};

View File

@ -1,89 +1,127 @@
define(['objectUtils'], function (objectUtils) { import objectUtils from 'objectUtils';
describe('objectUtils', function () {
describe('keyString util', function () { describe('objectUtils', function () {
const EXPECTATIONS = { describe('keyString util', function () {
ROOT: { const EXPECTATIONS = {
ROOT: {
namespace: '',
key: 'ROOT'
},
mine: {
namespace: '',
key: 'mine'
},
'extended:something:with:colons': {
key: 'something:with:colons',
namespace: 'extended'
},
'https\\://some/url:resourceId': {
key: 'resourceId',
namespace: 'https://some/url'
},
'scratch:root': {
namespace: 'scratch',
key: 'root'
},
'thingy\\:thing:abc123': {
namespace: 'thingy:thing',
key: 'abc123'
}
};
Object.keys(EXPECTATIONS).forEach(function (keyString) {
it('parses "' + keyString + '".', function () {
expect(objectUtils.parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
});
it('parses and re-encodes "' + keyString + '"', function () {
const identifier = objectUtils.parseKeyString(keyString);
expect(objectUtils.makeKeyString(identifier)).toEqual(keyString);
});
it('is idempotent for "' + keyString + '".', function () {
const identifier = objectUtils.parseKeyString(keyString);
let again = objectUtils.parseKeyString(identifier);
expect(identifier).toEqual(again);
again = objectUtils.parseKeyString(again);
again = objectUtils.parseKeyString(again);
expect(identifier).toEqual(again);
let againKeyString = objectUtils.makeKeyString(again);
expect(againKeyString).toEqual(keyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
expect(againKeyString).toEqual(keyString);
});
});
});
describe('old object conversions', function () {
it('translate ids', function () {
expect(
objectUtils.toNewFormat(
{
prop: 'someValue'
},
'objId'
)
).toEqual({
prop: 'someValue',
identifier: {
namespace: '', namespace: '',
key: 'ROOT' key: 'objId'
},
mine: {
namespace: '',
key: 'mine'
},
'extended:something:with:colons': {
key: 'something:with:colons',
namespace: 'extended'
},
'https\\://some/url:resourceId': {
key: 'resourceId',
namespace: 'https://some/url'
},
'scratch:root': {
namespace: 'scratch',
key: 'root'
},
'thingy\\:thing:abc123': {
namespace: 'thingy:thing',
key: 'abc123'
} }
};
Object.keys(EXPECTATIONS).forEach(function (keyString) {
it('parses "' + keyString + '".', function () {
expect(objectUtils.parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
});
it('parses and re-encodes "' + keyString + '"', function () {
const identifier = objectUtils.parseKeyString(keyString);
expect(objectUtils.makeKeyString(identifier)).toEqual(keyString);
});
it('is idempotent for "' + keyString + '".', function () {
const identifier = objectUtils.parseKeyString(keyString);
let again = objectUtils.parseKeyString(identifier);
expect(identifier).toEqual(again);
again = objectUtils.parseKeyString(again);
again = objectUtils.parseKeyString(again);
expect(identifier).toEqual(again);
let againKeyString = objectUtils.makeKeyString(again);
expect(againKeyString).toEqual(keyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
expect(againKeyString).toEqual(keyString);
});
}); });
}); });
describe('old object conversions', function () { it('translates composition', function () {
it('translate ids', function () { expect(
expect( objectUtils.toNewFormat(
objectUtils.toNewFormat( {
{ prop: 'someValue',
prop: 'someValue' composition: ['anotherObjectId', 'scratch:anotherObjectId']
}, },
'objId' 'objId'
) )
).toEqual({ ).toEqual({
prop: 'someValue',
composition: [
{
namespace: '',
key: 'anotherObjectId'
},
{
namespace: 'scratch',
key: 'anotherObjectId'
}
],
identifier: {
namespace: '',
key: 'objId'
}
});
});
});
describe('new object conversions', function () {
it('removes ids', function () {
expect(
objectUtils.toOldFormat({
prop: 'someValue', prop: 'someValue',
identifier: { identifier: {
namespace: '', namespace: '',
key: 'objId' key: 'objId'
} }
}); })
).toEqual({
prop: 'someValue'
}); });
});
it('translates composition', function () { it('translates composition', function () {
expect( expect(
objectUtils.toNewFormat( objectUtils.toOldFormat({
{
prop: 'someValue',
composition: ['anotherObjectId', 'scratch:anotherObjectId']
},
'objId'
)
).toEqual({
prop: 'someValue', prop: 'someValue',
composition: [ composition: [
{ {
@ -99,48 +137,10 @@ define(['objectUtils'], function (objectUtils) {
namespace: '', namespace: '',
key: 'objId' key: 'objId'
} }
}); })
}); ).toEqual({
}); prop: 'someValue',
composition: ['anotherObjectId', 'scratch:anotherObjectId']
describe('new object conversions', function () {
it('removes ids', function () {
expect(
objectUtils.toOldFormat({
prop: 'someValue',
identifier: {
namespace: '',
key: 'objId'
}
})
).toEqual({
prop: 'someValue'
});
});
it('translates composition', function () {
expect(
objectUtils.toOldFormat({
prop: 'someValue',
composition: [
{
namespace: '',
key: 'anotherObjectId'
},
{
namespace: 'scratch',
key: 'anotherObjectId'
}
],
identifier: {
namespace: '',
key: 'objId'
}
})
).toEqual({
prop: 'someValue',
composition: ['anotherObjectId', 'scratch:anotherObjectId']
});
}); });
}); });
}); });

View File

@ -20,17 +20,19 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['lodash'], function (_) { import _ from 'lodash';
/**
* This is the default metadata provider; for any object with a "telemetry" /**
* property, this provider will return the value of that property as the * This is the default metadata provider; for any object with a "telemetry"
* telemetry metadata. * property, this provider will return the value of that property as the
* * telemetry metadata.
* This provider also implements legacy support for telemetry metadata *
* defined on the type. Telemetry metadata definitions on type will be * This provider also implements legacy support for telemetry metadata
* depreciated in the future. * defined on the type. Telemetry metadata definitions on type will be
*/ * depreciated in the future.
function DefaultMetadataProvider(openmct) { */
export default class DefaultMetadataProvider {
constructor(openmct) {
this.openmct = openmct; this.openmct = openmct;
} }
@ -38,65 +40,14 @@ define(['lodash'], function (_) {
* Applies to any domain object with a telemetry property, or whose type * Applies to any domain object with a telemetry property, or whose type
* definition has a telemetry property. * definition has a telemetry property.
*/ */
DefaultMetadataProvider.prototype.supportsMetadata = function (domainObject) { supportsMetadata(domainObject) {
return Boolean(domainObject.telemetry) || Boolean(this.typeHasTelemetry(domainObject)); return Boolean(domainObject.telemetry) || Boolean(this.typeHasTelemetry(domainObject));
};
/**
* Retrieves valueMetadata from legacy metadata.
* @private
*/
function valueMetadatasFromOldFormat(metadata) {
const valueMetadatas = [];
valueMetadatas.push({
key: 'name',
name: 'Name'
});
metadata.domains.forEach(function (domain, index) {
const valueMetadata = _.clone(domain);
valueMetadata.hints = {
domain: index + 1
};
valueMetadatas.push(valueMetadata);
});
metadata.ranges.forEach(function (range, index) {
const valueMetadata = _.clone(range);
valueMetadata.hints = {
range: index,
priority: index + metadata.domains.length + 1
};
if (valueMetadata.type === 'enum') {
valueMetadata.key = 'enum';
valueMetadata.hints.y -= 10;
valueMetadata.hints.range -= 10;
valueMetadata.enumerations = _.sortBy(
valueMetadata.enumerations.map(function (e) {
return {
string: e.string,
value: Number(e.value)
};
}),
'e.value'
);
valueMetadata.values = valueMetadata.enumerations.map((e) => e.value);
valueMetadata.max = Math.max(valueMetadata.values);
valueMetadata.min = Math.min(valueMetadata.values);
}
valueMetadatas.push(valueMetadata);
});
return valueMetadatas;
} }
/** /**
* Returns telemetry metadata for a given domain object. * Returns telemetry metadata for a given domain object.
*/ */
DefaultMetadataProvider.prototype.getMetadata = function (domainObject) { getMetadata(domainObject) {
const metadata = domainObject.telemetry || {}; const metadata = domainObject.telemetry || {};
if (this.typeHasTelemetry(domainObject)) { if (this.typeHasTelemetry(domainObject)) {
const typeMetadata = this.openmct.types.get(domainObject.type).definition.telemetry; const typeMetadata = this.openmct.types.get(domainObject.type).definition.telemetry;
@ -109,16 +60,65 @@ define(['lodash'], function (_) {
} }
return metadata; return metadata;
}; }
/** /**
* @private * @private
*/ */
DefaultMetadataProvider.prototype.typeHasTelemetry = function (domainObject) { typeHasTelemetry(domainObject) {
const type = this.openmct.types.get(domainObject.type); const type = this.openmct.types.get(domainObject.type);
return Boolean(type.definition.telemetry); return Boolean(type.definition.telemetry);
}; }
}
return DefaultMetadataProvider; /**
}); * Retrieves valueMetadata from legacy metadata.
* @private
*/
function valueMetadatasFromOldFormat(metadata) {
const valueMetadatas = [];
valueMetadatas.push({
key: 'name',
name: 'Name'
});
metadata.domains.forEach(function (domain, index) {
const valueMetadata = _.clone(domain);
valueMetadata.hints = {
domain: index + 1
};
valueMetadatas.push(valueMetadata);
});
metadata.ranges.forEach(function (range, index) {
const valueMetadata = _.clone(range);
valueMetadata.hints = {
range: index,
priority: index + metadata.domains.length + 1
};
if (valueMetadata.type === 'enum') {
valueMetadata.key = 'enum';
valueMetadata.hints.y -= 10;
valueMetadata.hints.range -= 10;
valueMetadata.enumerations = _.sortBy(
valueMetadata.enumerations.map(function (e) {
return {
string: e.string,
value: Number(e.value)
};
}),
'e.value'
);
valueMetadata.values = valueMetadata.enumerations.map((e) => e.value);
valueMetadata.max = Math.max(valueMetadata.values);
valueMetadata.min = Math.min(valueMetadata.values);
}
valueMetadatas.push(valueMetadata);
});
return valueMetadatas;
}

View File

@ -20,143 +20,141 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['lodash'], function (_) { import _ from 'lodash';
function applyReasonableDefaults(valueMetadata, index) {
valueMetadata.source = valueMetadata.source || valueMetadata.key;
valueMetadata.hints = valueMetadata.hints || {};
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) { function applyReasonableDefaults(valueMetadata, index) {
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) { valueMetadata.source = valueMetadata.source || valueMetadata.key;
valueMetadata.hints.domain = valueMetadata.hints.x; valueMetadata.hints = valueMetadata.hints || {};
}
delete valueMetadata.hints.x; if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) {
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) {
valueMetadata.hints.domain = valueMetadata.hints.x;
} }
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) { delete valueMetadata.hints.x;
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {
valueMetadata.hints.range = valueMetadata.hints.y;
}
delete valueMetadata.hints.y;
}
if (valueMetadata.format === 'enum') {
if (!valueMetadata.values) {
valueMetadata.values = valueMetadata.enumerations.map((e) => e.value);
}
if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'max')) {
valueMetadata.max = Math.max(valueMetadata.values) + 1;
}
if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'min')) {
valueMetadata.min = Math.min(valueMetadata.values) - 1;
}
}
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'priority')) {
valueMetadata.hints.priority = index;
}
return valueMetadata;
} }
/** if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) {
* Utility class for handling and inspecting telemetry metadata. Applies if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {
* reasonable defaults to simplify the task of providing metadata, while valueMetadata.hints.range = valueMetadata.hints.y;
* also providing methods for interrogating telemetry metadata. }
*/
function TelemetryMetadataManager(metadata) {
this.metadata = metadata;
this.valueMetadatas = this.metadata.values delete valueMetadata.hints.y;
? this.metadata.values.map(applyReasonableDefaults)
: [];
} }
/** if (valueMetadata.format === 'enum') {
* Get value metadata for a single key. if (!valueMetadata.values) {
*/ valueMetadata.values = valueMetadata.enumerations.map((e) => e.value);
TelemetryMetadataManager.prototype.value = function (key) { }
return this.valueMetadatas.filter(function (metadata) {
return metadata.key === key; if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'max')) {
valueMetadata.max = Math.max(valueMetadata.values) + 1;
}
if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'min')) {
valueMetadata.min = Math.min(valueMetadata.values) - 1;
}
}
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'priority')) {
valueMetadata.hints.priority = index;
}
return valueMetadata;
}
/**
* Utility class for handling and inspecting telemetry metadata. Applies
* reasonable defaults to simplify the task of providing metadata, while
* also providing methods for interrogating telemetry metadata.
*/
export default function TelemetryMetadataManager(metadata) {
this.metadata = metadata;
this.valueMetadatas = this.metadata.values
? this.metadata.values.map(applyReasonableDefaults)
: [];
}
/**
* Get value metadata for a single key.
*/
TelemetryMetadataManager.prototype.value = function (key) {
return this.valueMetadatas.filter(function (metadata) {
return metadata.key === key;
})[0];
};
/**
* Returns all value metadatas, sorted by priority.
*/
TelemetryMetadataManager.prototype.values = function () {
return this.valuesForHints(['priority']);
};
/**
* Get an array of valueMetadatas that possess all hints requested.
* Array is sorted based on hint priority.
*
*/
TelemetryMetadataManager.prototype.valuesForHints = function (hints) {
function hasHint(hint) {
// eslint-disable-next-line no-invalid-this
return Object.prototype.hasOwnProperty.call(this.hints, hint);
}
function hasHints(metadata) {
return hints.every(hasHint, metadata);
}
const matchingMetadata = this.valueMetadatas.filter(hasHints);
let iteratees = hints.map((hint) => {
return (metadata) => {
return metadata.hints[hint];
};
});
return _.sortBy(matchingMetadata, ...iteratees);
};
/**
* check out of a given metadata has array values
*/
TelemetryMetadataManager.prototype.isArrayValue = function (metadata) {
const regex = /\[\]$/g;
if (!metadata.format && !metadata.formatString) {
return false;
}
return (metadata.format || metadata.formatString).match(regex) !== null;
};
TelemetryMetadataManager.prototype.getFilterableValues = function () {
return this.valueMetadatas.filter(
(metadatum) => metadatum.filters && metadatum.filters.length > 0
);
};
TelemetryMetadataManager.prototype.getUseToUpdateInPlaceValue = function () {
return this.valueMetadatas.find(this.isInPlaceUpdateValue);
};
TelemetryMetadataManager.prototype.isInPlaceUpdateValue = function (metadatum) {
return metadatum.useToUpdateInPlace === true;
};
TelemetryMetadataManager.prototype.getDefaultDisplayValue = function () {
let valueMetadata = this.valuesForHints(['range'])[0];
if (valueMetadata === undefined) {
valueMetadata = this.values().filter((values) => {
return !values.hints.domain;
})[0]; })[0];
}; }
/** if (valueMetadata === undefined) {
* Returns all value metadatas, sorted by priority. valueMetadata = this.values()[0];
*/ }
TelemetryMetadataManager.prototype.values = function () {
return this.valuesForHints(['priority']);
};
/** return valueMetadata;
* Get an array of valueMetadatas that possess all hints requested. };
* Array is sorted based on hint priority.
*
*/
TelemetryMetadataManager.prototype.valuesForHints = function (hints) {
function hasHint(hint) {
// eslint-disable-next-line no-invalid-this
return Object.prototype.hasOwnProperty.call(this.hints, hint);
}
function hasHints(metadata) {
return hints.every(hasHint, metadata);
}
const matchingMetadata = this.valueMetadatas.filter(hasHints);
let iteratees = hints.map((hint) => {
return (metadata) => {
return metadata.hints[hint];
};
});
return _.sortBy(matchingMetadata, ...iteratees);
};
/**
* check out of a given metadata has array values
*/
TelemetryMetadataManager.prototype.isArrayValue = function (metadata) {
const regex = /\[\]$/g;
if (!metadata.format && !metadata.formatString) {
return false;
}
return (metadata.format || metadata.formatString).match(regex) !== null;
};
TelemetryMetadataManager.prototype.getFilterableValues = function () {
return this.valueMetadatas.filter(
(metadatum) => metadatum.filters && metadatum.filters.length > 0
);
};
TelemetryMetadataManager.prototype.getUseToUpdateInPlaceValue = function () {
return this.valueMetadatas.find(this.isInPlaceUpdateValue);
};
TelemetryMetadataManager.prototype.isInPlaceUpdateValue = function (metadatum) {
return metadatum.useToUpdateInPlace === true;
};
TelemetryMetadataManager.prototype.getDefaultDisplayValue = function () {
let valueMetadata = this.valuesForHints(['range'])[0];
if (valueMetadata === undefined) {
valueMetadata = this.values().filter((values) => {
return !values.hints.domain;
})[0];
}
if (valueMetadata === undefined) {
valueMetadata = this.values()[0];
}
return valueMetadata;
};
return TelemetryMetadataManager;
});

View File

@ -20,96 +20,92 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { // Set of connection states; changing among these states will be
// Set of connection states; changing among these states will be // reflected in the indicator's appearance.
// reflected in the indicator's appearance. // CONNECTED: Everything nominal, expect to be able to read/write.
// CONNECTED: Everything nominal, expect to be able to read/write. // DISCONNECTED: HTTP failed; maybe misconfigured, disconnected.
// DISCONNECTED: HTTP failed; maybe misconfigured, disconnected. // PENDING: Still trying to connect, and haven't failed yet.
// PENDING: Still trying to connect, and haven't failed yet. const CONNECTED = {
const CONNECTED = { statusClass: 's-status-on'
statusClass: 's-status-on' };
}; const PENDING = {
const PENDING = { statusClass: 's-status-warning-lo'
statusClass: 's-status-warning-lo' };
}; const DISCONNECTED = {
const DISCONNECTED = { statusClass: 's-status-warning-hi'
statusClass: 's-status-warning-hi' };
}; export default function URLIndicator(options, simpleIndicator) {
function URLIndicator(options, simpleIndicator) { this.bindMethods();
this.bindMethods(); this.count = 0;
this.count = 0;
this.indicator = simpleIndicator; this.indicator = simpleIndicator;
this.setDefaultsFromOptions(options); this.setDefaultsFromOptions(options);
this.setIndicatorToState(PENDING); this.setIndicatorToState(PENDING);
this.fetchUrl(); this.fetchUrl();
setInterval(this.fetchUrl, this.interval); setInterval(this.fetchUrl, this.interval);
} }
URLIndicator.prototype.setIndicatorToState = function (state) { URLIndicator.prototype.setIndicatorToState = function (state) {
switch (state) { switch (state) {
case CONNECTED: { case CONNECTED: {
this.indicator.text(this.label + ' is connected'); this.indicator.text(this.label + ' is connected');
this.indicator.description( this.indicator.description(
this.label + ' is online, checking status every ' + this.interval + ' milliseconds.' this.label + ' is online, checking status every ' + this.interval + ' milliseconds.'
); );
break; break;
}
case PENDING: {
this.indicator.text('Checking status of ' + this.label + ' please stand by...');
this.indicator.description('Checking status of ' + this.label + ' please stand by...');
break;
}
case DISCONNECTED: {
this.indicator.text(this.label + ' is offline');
this.indicator.description(
this.label + ' is offline, checking status every ' + this.interval + ' milliseconds'
);
break;
}
} }
this.indicator.statusClass(state.statusClass); case PENDING: {
}; this.indicator.text('Checking status of ' + this.label + ' please stand by...');
this.indicator.description('Checking status of ' + this.label + ' please stand by...');
break;
}
URLIndicator.prototype.fetchUrl = function () { case DISCONNECTED: {
fetch(this.URLpath) this.indicator.text(this.label + ' is offline');
.then((response) => { this.indicator.description(
if (response.ok) { this.label + ' is offline, checking status every ' + this.interval + ' milliseconds'
this.handleSuccess(); );
} else { break;
this.handleError(); }
} }
})
.catch((error) => { this.indicator.statusClass(state.statusClass);
};
URLIndicator.prototype.fetchUrl = function () {
fetch(this.URLpath)
.then((response) => {
if (response.ok) {
this.handleSuccess();
} else {
this.handleError(); this.handleError();
}); }
}; })
.catch((error) => {
this.handleError();
});
};
URLIndicator.prototype.handleError = function (e) { URLIndicator.prototype.handleError = function (e) {
this.setIndicatorToState(DISCONNECTED); this.setIndicatorToState(DISCONNECTED);
}; };
URLIndicator.prototype.handleSuccess = function () { URLIndicator.prototype.handleSuccess = function () {
this.setIndicatorToState(CONNECTED); this.setIndicatorToState(CONNECTED);
}; };
URLIndicator.prototype.setDefaultsFromOptions = function (options) { URLIndicator.prototype.setDefaultsFromOptions = function (options) {
this.URLpath = options.url; this.URLpath = options.url;
this.label = options.label || options.url; this.label = options.label || options.url;
this.interval = options.interval || 10000; this.interval = options.interval || 10000;
this.indicator.iconClass(options.iconClass || 'icon-chain-links'); this.indicator.iconClass(options.iconClass || 'icon-chain-links');
}; };
URLIndicator.prototype.bindMethods = function () { URLIndicator.prototype.bindMethods = function () {
this.fetchUrl = this.fetchUrl.bind(this); this.fetchUrl = this.fetchUrl.bind(this);
this.handleSuccess = this.handleSuccess.bind(this); this.handleSuccess = this.handleSuccess.bind(this);
this.handleError = this.handleError.bind(this); this.handleError = this.handleError.bind(this);
this.setIndicatorToState = this.setIndicatorToState.bind(this); this.setIndicatorToState = this.setIndicatorToState.bind(this);
}; };
return URLIndicator;
});

View File

@ -19,15 +19,15 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./URLIndicator'], function URLIndicatorPlugin(URLIndicator) { import URLIndicator from './URLIndicator';
return function (opts) {
return function install(openmct) {
const simpleIndicator = openmct.indicators.simpleIndicator();
const urlIndicator = new URLIndicator(opts, simpleIndicator);
openmct.indicators.add(simpleIndicator); export default function URLIndicatorPlugin(opts) {
return function install(openmct) {
const simpleIndicator = openmct.indicators.simpleIndicator();
const urlIndicator = new URLIndicator(opts, simpleIndicator);
return urlIndicator; openmct.indicators.add(simpleIndicator);
};
return urlIndicator;
}; };
}); }

View File

@ -20,118 +20,115 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['utils/testing', './URLIndicator', './URLIndicatorPlugin', '../../MCT'], function ( import * as testingUtils from 'utils/testing';
testingUtils,
URLIndicator,
URLIndicatorPlugin,
MCT
) {
describe('The URLIndicator', function () {
let openmct;
let indicatorElement;
let pluginOptions;
let urlIndicator; // eslint-disable-line
let fetchSpy;
beforeEach(function () { import URLIndicatorPlugin from './URLIndicatorPlugin';
jasmine.clock().install();
openmct = new testingUtils.createOpenMct();
spyOn(openmct.indicators, 'add');
fetchSpy = spyOn(window, 'fetch').and.callFake(() =>
Promise.resolve({
ok: true
})
);
});
afterEach(function () { describe('The URLIndicator', function () {
if (window.fetch.restore) { let openmct;
window.fetch.restore(); let indicatorElement;
} let pluginOptions;
let urlIndicator; // eslint-disable-line
let fetchSpy;
jasmine.clock().uninstall(); beforeEach(function () {
jasmine.clock().install();
openmct = new testingUtils.createOpenMct();
spyOn(openmct.indicators, 'add');
fetchSpy = spyOn(window, 'fetch').and.callFake(() =>
Promise.resolve({
ok: true
})
);
});
return testingUtils.resetApplicationState(openmct); afterEach(function () {
}); if (window.fetch.restore) {
window.fetch.restore();
}
describe('on initialization', function () { jasmine.clock().uninstall();
describe('with default options', function () {
beforeEach(function () {
pluginOptions = {
url: 'someURL'
};
urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct);
indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element;
});
it('has a default icon class if none supplied', function () { return testingUtils.resetApplicationState(openmct);
expect(indicatorElement.classList.contains('icon-chain-links')).toBe(true); });
});
it('defaults to the URL if no label supplied', function () { describe('on initialization', function () {
expect(indicatorElement.textContent.indexOf(pluginOptions.url) >= 0).toBe(true); describe('with default options', function () {
});
});
describe('with custom options', function () {
beforeEach(function () {
pluginOptions = {
url: 'customURL',
interval: 1814,
iconClass: 'iconClass-checked',
label: 'custom label'
};
urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct);
indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element;
});
it('uses the custom iconClass', function () {
expect(indicatorElement.classList.contains('iconClass-checked')).toBe(true);
});
it('uses custom interval', function () {
expect(window.fetch).toHaveBeenCalledTimes(1);
jasmine.clock().tick(1);
expect(window.fetch).toHaveBeenCalledTimes(1);
jasmine.clock().tick(pluginOptions.interval + 1);
expect(window.fetch).toHaveBeenCalledTimes(2);
});
it('uses custom label if supplied in initialization', function () {
expect(indicatorElement.textContent.indexOf(pluginOptions.label) >= 0).toBe(true);
});
});
});
describe('when running', function () {
beforeEach(function () { beforeEach(function () {
pluginOptions = { pluginOptions = {
url: 'someURL', url: 'someURL'
interval: 100
}; };
urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct); urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct);
indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element; indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element;
}); });
it('requests the provided URL', function () { it('has a default icon class if none supplied', function () {
jasmine.clock().tick(pluginOptions.interval + 1); expect(indicatorElement.classList.contains('icon-chain-links')).toBe(true);
expect(window.fetch).toHaveBeenCalledWith(pluginOptions.url);
}); });
it('indicates success if connection is nominal', async function () { it('defaults to the URL if no label supplied', function () {
jasmine.clock().tick(pluginOptions.interval + 1); expect(indicatorElement.textContent.indexOf(pluginOptions.url) >= 0).toBe(true);
await urlIndicator.fetchUrl(); });
expect(indicatorElement.classList.contains('s-status-on')).toBe(true); });
describe('with custom options', function () {
beforeEach(function () {
pluginOptions = {
url: 'customURL',
interval: 1814,
iconClass: 'iconClass-checked',
label: 'custom label'
};
urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct);
indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element;
}); });
it('indicates an error when the server cannot be reached', async function () { it('uses the custom iconClass', function () {
fetchSpy.and.callFake(() => expect(indicatorElement.classList.contains('iconClass-checked')).toBe(true);
Promise.resolve({ });
ok: false it('uses custom interval', function () {
}) expect(window.fetch).toHaveBeenCalledTimes(1);
); jasmine.clock().tick(1);
expect(window.fetch).toHaveBeenCalledTimes(1);
jasmine.clock().tick(pluginOptions.interval + 1); jasmine.clock().tick(pluginOptions.interval + 1);
await urlIndicator.fetchUrl(); expect(window.fetch).toHaveBeenCalledTimes(2);
expect(indicatorElement.classList.contains('s-status-warning-hi')).toBe(true); });
it('uses custom label if supplied in initialization', function () {
expect(indicatorElement.textContent.indexOf(pluginOptions.label) >= 0).toBe(true);
}); });
}); });
}); });
describe('when running', function () {
beforeEach(function () {
pluginOptions = {
url: 'someURL',
interval: 100
};
urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct);
indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element;
});
it('requests the provided URL', function () {
jasmine.clock().tick(pluginOptions.interval + 1);
expect(window.fetch).toHaveBeenCalledWith(pluginOptions.url);
});
it('indicates success if connection is nominal', async function () {
jasmine.clock().tick(pluginOptions.interval + 1);
await urlIndicator.fetchUrl();
expect(indicatorElement.classList.contains('s-status-on')).toBe(true);
});
it('indicates an error when the server cannot be reached', async function () {
fetchSpy.and.callFake(() =>
Promise.resolve({
ok: false
})
);
jasmine.clock().tick(pluginOptions.interval + 1);
await urlIndicator.fetchUrl();
expect(indicatorElement.classList.contains('s-status-warning-hi')).toBe(true);
});
});
}); });

View File

@ -20,15 +20,13 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { /**
/** * Constant values used by the Autoflow Tabular View.
* Constant values used by the Autoflow Tabular View. */
*/ export default {
return { ROW_HEIGHT: 16,
ROW_HEIGHT: 16, SLIDER_HEIGHT: 10,
SLIDER_HEIGHT: 10, INITIAL_COLUMN_WIDTH: 225,
INITIAL_COLUMN_WIDTH: 225, MAX_COLUMN_WIDTH: 525,
MAX_COLUMN_WIDTH: 525, COLUMN_WIDTH_STEP: 25
COLUMN_WIDTH_STEP: 25 };
};
});

View File

@ -20,104 +20,102 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./AutoflowTabularRowController'], function (AutoflowTabularRowController) { import AutoflowTabularRowController from './AutoflowTabularRowController';
/**
* Controller for an Autoflow Tabular View. Subscribes to telemetry
* associated with children of the domain object and passes that
* information on to the view.
*
* @param {DomainObject} domainObject the object being viewed
* @param {*} data the view data
* @param openmct a reference to the openmct application
*/
function AutoflowTabularController(domainObject, data, openmct) {
this.composition = openmct.composition.get(domainObject);
this.data = data;
this.openmct = openmct;
this.rows = {}; /**
this.controllers = {}; * Controller for an Autoflow Tabular View. Subscribes to telemetry
* associated with children of the domain object and passes that
* information on to the view.
*
* @param {DomainObject} domainObject the object being viewed
* @param {*} data the view data
* @param openmct a reference to the openmct application
*/
export default function AutoflowTabularController(domainObject, data, openmct) {
this.composition = openmct.composition.get(domainObject);
this.data = data;
this.openmct = openmct;
this.addRow = this.addRow.bind(this); this.rows = {};
this.removeRow = this.removeRow.bind(this); this.controllers = {};
this.addRow = this.addRow.bind(this);
this.removeRow = this.removeRow.bind(this);
}
/**
* Set the "Last Updated" value to be displayed.
* @param {String} value the value to display
* @private
*/
AutoflowTabularController.prototype.trackLastUpdated = function (value) {
this.data.updated = value;
};
/**
* Respond to an `add` event from composition by adding a new row.
* @private
*/
AutoflowTabularController.prototype.addRow = function (childObject) {
const identifier = childObject.identifier;
const id = [identifier.namespace, identifier.key].join(':');
if (!this.rows[id]) {
this.rows[id] = {
classes: '',
name: childObject.name,
value: undefined
};
this.controllers[id] = new AutoflowTabularRowController(
childObject,
this.rows[id],
this.openmct,
this.trackLastUpdated.bind(this)
);
this.controllers[id].activate();
this.data.items.push(this.rows[id]);
} }
};
/** /**
* Set the "Last Updated" value to be displayed. * Respond to an `remove` event from composition by removing any
* @param {String} value the value to display * related row.
* @private * @private
*/ */
AutoflowTabularController.prototype.trackLastUpdated = function (value) { AutoflowTabularController.prototype.removeRow = function (identifier) {
this.data.updated = value; const id = [identifier.namespace, identifier.key].join(':');
};
/** if (this.rows[id]) {
* Respond to an `add` event from composition by adding a new row. this.data.items = this.data.items.filter(
* @private function (item) {
*/ return item !== this.rows[id];
AutoflowTabularController.prototype.addRow = function (childObject) {
const identifier = childObject.identifier;
const id = [identifier.namespace, identifier.key].join(':');
if (!this.rows[id]) {
this.rows[id] = {
classes: '',
name: childObject.name,
value: undefined
};
this.controllers[id] = new AutoflowTabularRowController(
childObject,
this.rows[id],
this.openmct,
this.trackLastUpdated.bind(this)
);
this.controllers[id].activate();
this.data.items.push(this.rows[id]);
}
};
/**
* Respond to an `remove` event from composition by removing any
* related row.
* @private
*/
AutoflowTabularController.prototype.removeRow = function (identifier) {
const id = [identifier.namespace, identifier.key].join(':');
if (this.rows[id]) {
this.data.items = this.data.items.filter(
function (item) {
return item !== this.rows[id];
}.bind(this)
);
this.controllers[id].destroy();
delete this.controllers[id];
delete this.rows[id];
}
};
/**
* Activate this controller; begin listening for changes.
*/
AutoflowTabularController.prototype.activate = function () {
this.composition.on('add', this.addRow);
this.composition.on('remove', this.removeRow);
this.composition.load();
};
/**
* Destroy this controller; detach any associated resources.
*/
AutoflowTabularController.prototype.destroy = function () {
Object.keys(this.controllers).forEach(
function (id) {
this.controllers[id].destroy();
}.bind(this) }.bind(this)
); );
this.controllers = {}; this.controllers[id].destroy();
this.composition.off('add', this.addRow); delete this.controllers[id];
this.composition.off('remove', this.removeRow); delete this.rows[id];
}; }
};
return AutoflowTabularController; /**
}); * Activate this controller; begin listening for changes.
*/
AutoflowTabularController.prototype.activate = function () {
this.composition.on('add', this.addRow);
this.composition.on('remove', this.removeRow);
this.composition.load();
};
/**
* Destroy this controller; detach any associated resources.
*/
AutoflowTabularController.prototype.destroy = function () {
Object.keys(this.controllers).forEach(
function (id) {
this.controllers[id].destroy();
}.bind(this)
);
this.controllers = {};
this.composition.off('add', this.addRow);
this.composition.off('remove', this.removeRow);
};

View File

@ -20,23 +20,23 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./AutoflowTabularView'], function (AutoflowTabularView) { import AutoflowTabularView from './AutoflowTabularView';
return function (options) {
return function (openmct) {
const views = openmct.mainViews || openmct.objectViews;
views.addProvider({ export default function (options) {
name: 'Autoflow Tabular', return function (openmct) {
key: 'autoflow', const views = openmct.mainViews || openmct.objectViews;
cssClass: 'icon-packet',
description: 'A tabular view of packet contents.', views.addProvider({
canView: function (d) { name: 'Autoflow Tabular',
return !options || options.type === d.type; key: 'autoflow',
}, cssClass: 'icon-packet',
view: function (domainObject) { description: 'A tabular view of packet contents.',
return new AutoflowTabularView(domainObject, openmct, document); canView: function (d) {
} return !options || options.type === d.type;
}); },
}; view: function (domainObject) {
return new AutoflowTabularView(domainObject, openmct, document);
}
});
}; };
}); }

View File

@ -20,76 +20,72 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { /**
/** * Controller for individual rows of an Autoflow Tabular View.
* Controller for individual rows of an Autoflow Tabular View. * Subscribes to telemetry and updates row data.
* Subscribes to telemetry and updates row data. *
* * @param {DomainObject} domainObject the object being viewed
* @param {DomainObject} domainObject the object being viewed * @param {*} data the view data
* @param {*} data the view data * @param openmct a reference to the openmct application
* @param openmct a reference to the openmct application * @param {Function} callback a callback to invoke with "last updated" timestamps
* @param {Function} callback a callback to invoke with "last updated" timestamps */
*/ export default function AutoflowTabularRowController(domainObject, data, openmct, callback) {
function AutoflowTabularRowController(domainObject, data, openmct, callback) { this.domainObject = domainObject;
this.domainObject = domainObject; this.data = data;
this.data = data; this.openmct = openmct;
this.openmct = openmct; this.callback = callback;
this.callback = callback;
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.ranges = this.metadata.valuesForHints(['range']); this.ranges = this.metadata.valuesForHints(['range']);
this.domains = this.metadata.valuesForHints(['domain']); this.domains = this.metadata.valuesForHints(['domain']);
this.rangeFormatter = this.openmct.telemetry.getValueFormatter(this.ranges[0]); this.rangeFormatter = this.openmct.telemetry.getValueFormatter(this.ranges[0]);
this.domainFormatter = this.openmct.telemetry.getValueFormatter(this.domains[0]); this.domainFormatter = this.openmct.telemetry.getValueFormatter(this.domains[0]);
this.evaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); this.evaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
this.initialized = false; this.initialized = false;
}
/**
* Update row to reflect incoming telemetry data.
* @private
*/
AutoflowTabularRowController.prototype.updateRowData = function (datum) {
const violations = this.evaluator.evaluate(datum, this.ranges[0]);
this.initialized = true;
this.data.classes = violations ? violations.cssClass : '';
this.data.value = this.rangeFormatter.format(datum);
this.callback(this.domainFormatter.format(datum));
};
/**
* Activate this controller; begin listening for changes.
*/
AutoflowTabularRowController.prototype.activate = function () {
this.unsubscribe = this.openmct.telemetry.subscribe(
this.domainObject,
this.updateRowData.bind(this)
);
const options = {
size: 1,
strategy: 'latest',
timeContext: this.openmct.time.getContextForView([])
};
this.openmct.telemetry.request(this.domainObject, options).then(
function (history) {
if (!this.initialized && history.length > 0) {
this.updateRowData(history[history.length - 1]);
}
}.bind(this)
);
};
/**
* Destroy this controller; detach any associated resources.
*/
AutoflowTabularRowController.prototype.destroy = function () {
if (this.unsubscribe) {
this.unsubscribe();
} }
};
/**
* Update row to reflect incoming telemetry data.
* @private
*/
AutoflowTabularRowController.prototype.updateRowData = function (datum) {
const violations = this.evaluator.evaluate(datum, this.ranges[0]);
this.initialized = true;
this.data.classes = violations ? violations.cssClass : '';
this.data.value = this.rangeFormatter.format(datum);
this.callback(this.domainFormatter.format(datum));
};
/**
* Activate this controller; begin listening for changes.
*/
AutoflowTabularRowController.prototype.activate = function () {
this.unsubscribe = this.openmct.telemetry.subscribe(
this.domainObject,
this.updateRowData.bind(this)
);
const options = {
size: 1,
strategy: 'latest',
timeContext: this.openmct.time.getContextForView([])
};
this.openmct.telemetry.request(this.domainObject, options).then(
function (history) {
if (!this.initialized && history.length > 0) {
this.updateRowData(history[history.length - 1]);
}
}.bind(this)
);
};
/**
* Destroy this controller; detach any associated resources.
*/
AutoflowTabularRowController.prototype.destroy = function () {
if (this.unsubscribe) {
this.unsubscribe();
}
};
return AutoflowTabularRowController;
});

View File

@ -20,96 +20,92 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([ import autoflowTemplate from './autoflow-tabular.html';
'./AutoflowTabularController', import AutoflowTabularConstants from './AutoflowTabularConstants';
'./AutoflowTabularConstants', import AutoflowTabularController from './AutoflowTabularController';
'./VueView', import VueView from './VueView';
'./autoflow-tabular.html'
], function (AutoflowTabularController, AutoflowTabularConstants, VueView, autoflowTemplate) {
const ROW_HEIGHT = AutoflowTabularConstants.ROW_HEIGHT;
const SLIDER_HEIGHT = AutoflowTabularConstants.SLIDER_HEIGHT;
const INITIAL_COLUMN_WIDTH = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
const MAX_COLUMN_WIDTH = AutoflowTabularConstants.MAX_COLUMN_WIDTH;
const COLUMN_WIDTH_STEP = AutoflowTabularConstants.COLUMN_WIDTH_STEP;
/** const ROW_HEIGHT = AutoflowTabularConstants.ROW_HEIGHT;
* Implements the Autoflow Tabular view of a domain object. const SLIDER_HEIGHT = AutoflowTabularConstants.SLIDER_HEIGHT;
*/ const INITIAL_COLUMN_WIDTH = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
function AutoflowTabularView(domainObject, openmct) { const MAX_COLUMN_WIDTH = AutoflowTabularConstants.MAX_COLUMN_WIDTH;
const data = { const COLUMN_WIDTH_STEP = AutoflowTabularConstants.COLUMN_WIDTH_STEP;
items: [],
columns: [],
width: INITIAL_COLUMN_WIDTH,
filter: '',
updated: 'No updates',
rowCount: 1
};
const controller = new AutoflowTabularController(domainObject, data, openmct);
let interval;
VueView.call(this, { /**
data: data, * Implements the Autoflow Tabular view of a domain object.
methods: { */
increaseColumnWidth: function () { export default function AutoflowTabularView(domainObject, openmct) {
data.width += COLUMN_WIDTH_STEP; const data = {
data.width = data.width > MAX_COLUMN_WIDTH ? INITIAL_COLUMN_WIDTH : data.width; items: [],
}, columns: [],
reflow: function () { width: INITIAL_COLUMN_WIDTH,
let column = []; filter: '',
let index = 0; updated: 'No updates',
const filteredItems = data.items.filter(function (item) { rowCount: 1
return item.name.toLowerCase().indexOf(data.filter.toLowerCase()) !== -1; };
}); const controller = new AutoflowTabularController(domainObject, data, openmct);
let interval;
data.columns = []; VueView.call(this, {
data: data,
methods: {
increaseColumnWidth: function () {
data.width += COLUMN_WIDTH_STEP;
data.width = data.width > MAX_COLUMN_WIDTH ? INITIAL_COLUMN_WIDTH : data.width;
},
reflow: function () {
let column = [];
let index = 0;
const filteredItems = data.items.filter(function (item) {
return item.name.toLowerCase().indexOf(data.filter.toLowerCase()) !== -1;
});
while (index < filteredItems.length) { data.columns = [];
if (column.length >= data.rowCount) {
data.columns.push(column);
column = [];
}
column.push(filteredItems[index]); while (index < filteredItems.length) {
index += 1; if (column.length >= data.rowCount) {
}
if (column.length > 0) {
data.columns.push(column); data.columns.push(column);
column = [];
} }
column.push(filteredItems[index]);
index += 1;
} }
},
watch: {
filter: 'reflow',
items: 'reflow',
rowCount: 'reflow'
},
template: autoflowTemplate,
unmounted: function () {
controller.destroy();
if (interval) { if (column.length > 0) {
clearInterval(interval); data.columns.push(column);
interval = undefined;
} }
},
mounted: function () {
controller.activate();
const updateRowHeight = function () {
const tabularArea = this.$refs.autoflowItems;
const height = tabularArea ? tabularArea.clientHeight : 0;
const available = height - SLIDER_HEIGHT;
const rows = Math.max(1, Math.floor(available / ROW_HEIGHT));
data.rowCount = rows;
}.bind(this);
interval = setInterval(updateRowHeight, 50);
this.$nextTick(updateRowHeight);
} }
}); },
} watch: {
filter: 'reflow',
items: 'reflow',
rowCount: 'reflow'
},
template: autoflowTemplate,
unmounted: function () {
controller.destroy();
AutoflowTabularView.prototype = Object.create(VueView.default.prototype); if (interval) {
clearInterval(interval);
interval = undefined;
}
},
mounted: function () {
controller.activate();
return AutoflowTabularView; const updateRowHeight = function () {
}); const tabularArea = this.$refs.autoflowItems;
const height = tabularArea ? tabularArea.clientHeight : 0;
const available = height - SLIDER_HEIGHT;
const rows = Math.max(1, Math.floor(available / ROW_HEIGHT));
data.rowCount = rows;
}.bind(this);
interval = setInterval(updateRowHeight, 50);
this.$nextTick(updateRowHeight);
}
});
}
AutoflowTabularView.prototype = Object.create(VueView.prototype);

View File

@ -22,13 +22,15 @@
import mount from 'utils/mount'; import mount from 'utils/mount';
export default function () { export default function () {
return function VueView(options) { class VueView {
const { vNode, destroy } = mount(options); constructor(options) {
const { vNode, destroy } = mount(options);
this.show = function (container) {
container.appendChild(vNode.el);
};
this.destroy = destroy;
}
}
this.show = function (container) { return VueView;
container.appendChild(vNode.el);
};
this.destroy = destroy;
};
} }

View File

@ -20,44 +20,40 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { export default function DOMObserver(element) {
function DOMObserver(element) { this.element = element;
this.element = element; this.observers = [];
this.observers = []; }
}
DOMObserver.prototype.when = function (latchFunction) { DOMObserver.prototype.when = function (latchFunction) {
return new Promise( return new Promise(
function (resolve, reject) { function (resolve, reject) {
//Test latch function at least once //Test latch function at least once
if (latchFunction()) { if (latchFunction()) {
resolve(); resolve();
} else { } else {
//Latch condition not true yet, create observer on DOM and test again on change. //Latch condition not true yet, create observer on DOM and test again on change.
const config = { const config = {
attributes: true, attributes: true,
childList: true, childList: true,
subtree: true subtree: true
}; };
const observer = new MutationObserver(function () { const observer = new MutationObserver(function () {
if (latchFunction()) { if (latchFunction()) {
resolve(); resolve();
} }
}); });
observer.observe(this.element, config); observer.observe(this.element, config);
this.observers.push(observer); this.observers.push(observer);
} }
}.bind(this) }.bind(this)
); );
}; };
DOMObserver.prototype.destroy = function () { DOMObserver.prototype.destroy = function () {
this.observers.forEach( this.observers.forEach(
function (observer) { function (observer) {
observer.disconnect(); observer.disconnect();
}.bind(this) }.bind(this)
); );
}; };
return DOMObserver;
});

View File

@ -20,53 +20,49 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(function () { export default function DisplayLayoutType() {
function DisplayLayoutType() { return {
return { name: 'Display Layout',
name: 'Display Layout', creatable: true,
creatable: true, description:
description: 'Assemble other objects and components together into a reusable screen layout. Simply drag in the objects you want, position and size them. Save your design and view or edit it at any time.',
'Assemble other objects and components together into a reusable screen layout. Simply drag in the objects you want, position and size them. Save your design and view or edit it at any time.', cssClass: 'icon-layout',
cssClass: 'icon-layout', initialize(domainObject) {
initialize(domainObject) { domainObject.composition = [];
domainObject.composition = []; domainObject.configuration = {
domainObject.configuration = { items: [],
items: [], layoutGrid: [10, 10]
layoutGrid: [10, 10] };
}; },
form: [
{
name: 'Horizontal grid (px)',
control: 'numberfield',
cssClass: 'l-input-sm l-numeric',
property: ['configuration', 'layoutGrid', 0],
required: true
}, },
form: [ {
{ name: 'Vertical grid (px)',
name: 'Horizontal grid (px)', control: 'numberfield',
control: 'numberfield', cssClass: 'l-input-sm l-numeric',
cssClass: 'l-input-sm l-numeric', property: ['configuration', 'layoutGrid', 1],
property: ['configuration', 'layoutGrid', 0], required: true
required: true },
}, {
{ name: 'Horizontal size (px)',
name: 'Vertical grid (px)', control: 'numberfield',
control: 'numberfield', cssClass: 'l-input-sm l-numeric',
cssClass: 'l-input-sm l-numeric', property: ['configuration', 'layoutDimensions', 0],
property: ['configuration', 'layoutGrid', 1], required: false
required: true },
}, {
{ name: 'Vertical size (px)',
name: 'Horizontal size (px)', control: 'numberfield',
control: 'numberfield', cssClass: 'l-input-sm l-numeric',
cssClass: 'l-input-sm l-numeric', property: ['configuration', 'layoutDimensions', 1],
property: ['configuration', 'layoutDimensions', 0], required: false
required: false }
}, ]
{ };
name: 'Vertical size (px)', }
control: 'numberfield',
cssClass: 'l-input-sm l-numeric',
property: ['configuration', 'layoutDimensions', 1],
required: false
}
]
};
}
return DisplayLayoutType;
});

View File

@ -20,93 +20,89 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { /**
/** * Handles drag interactions on frames in layouts. This will
* Handles drag interactions on frames in layouts. This will * provides new positions/dimensions for frames based on
* provides new positions/dimensions for frames based on * relative pixel positions provided; these will take into account
* relative pixel positions provided; these will take into account * the grid size (in a snap-to sense) and will enforce some minimums
* the grid size (in a snap-to sense) and will enforce some minimums * on both position and dimensions.
* on both position and dimensions. *
* * The provided position and dimensions factors will determine
* The provided position and dimensions factors will determine * whether this is a move or a resize, and what type of resize it
* whether this is a move or a resize, and what type of resize it * will be. For instance, a position factor of [1, 1]
* will be. For instance, a position factor of [1, 1] * will move a frame along with the mouse as the drag
* will move a frame along with the mouse as the drag * proceeds, while a dimension factor of [0, 0] will leave
* proceeds, while a dimension factor of [0, 0] will leave * dimensions unchanged. Combining these in different
* dimensions unchanged. Combining these in different * ways results in different handles; a position factor of
* ways results in different handles; a position factor of * [1, 0] and a dimensions factor of [-1, 0] will implement
* [1, 0] and a dimensions factor of [-1, 0] will implement * a left-edge resize, as the horizontal position will move
* a left-edge resize, as the horizontal position will move * with the mouse while the horizontal dimensions shrink in
* with the mouse while the horizontal dimensions shrink in * kind (and vertical properties remain unmodified.)
* kind (and vertical properties remain unmodified.) *
* * @param {object} rawPosition the initial position/dimensions
* @param {object} rawPosition the initial position/dimensions * of the frame being interacted with
* of the frame being interacted with * @param {number[]} posFactor the position factor
* @param {number[]} posFactor the position factor * @param {number[]} dimFactor the dimensions factor
* @param {number[]} dimFactor the dimensions factor * @param {number[]} the size of each grid element, in pixels
* @param {number[]} the size of each grid element, in pixels * @constructor
* @constructor * @memberof platform/features/layout
* @memberof platform/features/layout */
*/ export default function LayoutDrag(rawPosition, posFactor, dimFactor, gridSize) {
function LayoutDrag(rawPosition, posFactor, dimFactor, gridSize) { this.rawPosition = rawPosition;
this.rawPosition = rawPosition; this.posFactor = posFactor;
this.posFactor = posFactor; this.dimFactor = dimFactor;
this.dimFactor = dimFactor; this.gridSize = gridSize;
this.gridSize = gridSize; }
}
// Convert a delta from pixel coordinates to grid coordinates, // Convert a delta from pixel coordinates to grid coordinates,
// rounding to whole-number grid coordinates. // rounding to whole-number grid coordinates.
function toGridDelta(gridSize, pixelDelta) { function toGridDelta(gridSize, pixelDelta) {
return pixelDelta.map(function (v, i) { return pixelDelta.map(function (v, i) {
return Math.round(v / gridSize[i]); return Math.round(v / gridSize[i]);
}); });
} }
// Utility function to perform element-by-element multiplication // Utility function to perform element-by-element multiplication
function multiply(array, factors) { function multiply(array, factors) {
return array.map(function (v, i) { return array.map(function (v, i) {
return v * factors[i]; return v * factors[i];
}); });
} }
// Utility function to perform element-by-element addition // Utility function to perform element-by-element addition
function add(array, other) { function add(array, other) {
return array.map(function (v, i) { return array.map(function (v, i) {
return v + other[i]; return v + other[i];
}); });
} }
// Utility function to perform element-by-element max-choosing // Utility function to perform element-by-element max-choosing
function max(array, other) { function max(array, other) {
return array.map(function (v, i) { return array.map(function (v, i) {
return Math.max(v, other[i]); return Math.max(v, other[i]);
}); });
} }
/** /**
* Get a new position object in grid coordinates, with * Get a new position object in grid coordinates, with
* position and dimensions both offset appropriately * position and dimensions both offset appropriately
* according to the factors supplied in the constructor. * according to the factors supplied in the constructor.
* @param {number[]} pixelDelta the offset from the * @param {number[]} pixelDelta the offset from the
* original position, in pixels * original position, in pixels
*/ */
LayoutDrag.prototype.getAdjustedPositionAndDimensions = function (pixelDelta) { LayoutDrag.prototype.getAdjustedPositionAndDimensions = function (pixelDelta) {
const gridDelta = toGridDelta(this.gridSize, pixelDelta); const gridDelta = toGridDelta(this.gridSize, pixelDelta);
return { return {
position: max(add(this.rawPosition.position, multiply(gridDelta, this.posFactor)), [0, 0]), position: max(add(this.rawPosition.position, multiply(gridDelta, this.posFactor)), [0, 0]),
dimensions: max(add(this.rawPosition.dimensions, multiply(gridDelta, this.dimFactor)), [1, 1]) dimensions: max(add(this.rawPosition.dimensions, multiply(gridDelta, this.dimFactor)), [1, 1])
};
}; };
};
LayoutDrag.prototype.getAdjustedPosition = function (pixelDelta) { LayoutDrag.prototype.getAdjustedPosition = function (pixelDelta) {
const gridDelta = toGridDelta(this.gridSize, pixelDelta); const gridDelta = toGridDelta(this.gridSize, pixelDelta);
return { return {
position: max(add(this.rawPosition.position, multiply(gridDelta, this.posFactor)), [0, 0]) position: max(add(this.rawPosition.position, multiply(gridDelta, this.posFactor)), [0, 0])
};
}; };
};
return LayoutDrag;
});

View File

@ -20,12 +20,12 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./FiltersInspectorViewProvider'], function (FiltersInspectorViewProvider) { import FiltersInspectorViewProvider from './FiltersInspectorViewProvider';
return function plugin(supportedObjectTypesArray) {
return function install(openmct) { export default function plugin(supportedObjectTypesArray) {
openmct.inspectorViews.addProvider( return function install(openmct) {
new FiltersInspectorViewProvider.default(openmct, supportedObjectTypesArray) openmct.inspectorViews.addProvider(
); new FiltersInspectorViewProvider(openmct, supportedObjectTypesArray)
}; );
}; };
}); }

View File

@ -20,33 +20,31 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./flexibleLayoutViewProvider', './utils/container', './toolbarProvider'], function ( import FlexibleLayoutViewProvider from './flexibleLayoutViewProvider';
FlexibleLayoutViewProvider, import ToolBarProvider from './toolbarProvider';
Container, import Container from './utils/container';
ToolBarProvider
) {
return function plugin() {
return function install(openmct) {
openmct.objectViews.addProvider(new FlexibleLayoutViewProvider.default(openmct));
openmct.types.addType('flexible-layout', { export default function plugin() {
name: 'Flexible Layout', return function install(openmct) {
creatable: true, openmct.objectViews.addProvider(new FlexibleLayoutViewProvider(openmct));
description:
'A fluid, flexible layout canvas that can display multiple objects in rows or columns.',
cssClass: 'icon-flexible-layout',
initialize: function (domainObject) {
domainObject.configuration = {
containers: [new Container.default(50), new Container.default(50)],
rowsLayout: false
};
domainObject.composition = [];
}
});
let toolbar = ToolBarProvider.default(openmct); openmct.types.addType('flexible-layout', {
name: 'Flexible Layout',
creatable: true,
description:
'A fluid, flexible layout canvas that can display multiple objects in rows or columns.',
cssClass: 'icon-flexible-layout',
initialize: function (domainObject) {
domainObject.configuration = {
containers: [new Container(50), new Container(50)],
rowsLayout: false
};
domainObject.composition = [];
}
});
openmct.toolbars.addProvider(toolbar); let toolbar = ToolBarProvider(openmct);
};
openmct.toolbars.addProvider(toolbar);
}; };
}); }

View File

@ -20,80 +20,78 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { const helperFunctions = {
const helperFunctions = { listenTo: function (object, event, callback, context) {
listenTo: function (object, event, callback, context) { if (!this._listeningTo) {
if (!this._listeningTo) { this._listeningTo = [];
this._listeningTo = [];
}
const listener = {
object: object,
event: event,
callback: callback,
context: context,
_cb: context ? callback.bind(context) : callback
};
if (object.$watch && event.indexOf('change:') === 0) {
const scopePath = event.replace('change:', '');
listener.unlisten = object.$watch(scopePath, listener._cb, true);
} else if (object.$on) {
listener.unlisten = object.$on(event, listener._cb);
} else if (object.addEventListener) {
object.addEventListener(event, listener._cb);
} else {
object.on(event, listener._cb);
}
this._listeningTo.push(listener);
},
stopListening: function (object, event, callback, context) {
if (!this._listeningTo) {
this._listeningTo = [];
}
this._listeningTo
.filter(function (listener) {
if (object && object !== listener.object) {
return false;
}
if (event && event !== listener.event) {
return false;
}
if (callback && callback !== listener.callback) {
return false;
}
if (context && context !== listener.context) {
return false;
}
return true;
})
.map(function (listener) {
if (listener.unlisten) {
listener.unlisten();
} else if (listener.object.removeEventListener) {
listener.object.removeEventListener(listener.event, listener._cb);
} else {
listener.object.off(listener.event, listener._cb);
}
return listener;
})
.forEach(function (listener) {
this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);
}, this);
},
extend: function (object) {
object.listenTo = helperFunctions.listenTo;
object.stopListening = helperFunctions.stopListening;
} }
};
return helperFunctions; const listener = {
}); object: object,
event: event,
callback: callback,
context: context,
_cb: context ? callback.bind(context) : callback
};
if (object.$watch && event.indexOf('change:') === 0) {
const scopePath = event.replace('change:', '');
listener.unlisten = object.$watch(scopePath, listener._cb, true);
} else if (object.$on) {
listener.unlisten = object.$on(event, listener._cb);
} else if (object.addEventListener) {
object.addEventListener(event, listener._cb);
} else {
object.on(event, listener._cb);
}
this._listeningTo.push(listener);
},
stopListening: function (object, event, callback, context) {
if (!this._listeningTo) {
this._listeningTo = [];
}
this._listeningTo
.filter(function (listener) {
if (object && object !== listener.object) {
return false;
}
if (event && event !== listener.event) {
return false;
}
if (callback && callback !== listener.callback) {
return false;
}
if (context && context !== listener.context) {
return false;
}
return true;
})
.map(function (listener) {
if (listener.unlisten) {
listener.unlisten();
} else if (listener.object.removeEventListener) {
listener.object.removeEventListener(listener.event, listener._cb);
} else {
listener.object.off(listener.event, listener._cb);
}
return listener;
})
.forEach(function (listener) {
this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);
}, this);
},
extend: function (object) {
object.listenTo = helperFunctions.listenTo;
object.stopListening = helperFunctions.stopListening;
}
};
export default helperFunctions;

View File

@ -20,24 +20,24 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['../../../src/plugins/utcTimeSystem/LocalClock'], function (LocalClock) { import LocalClock from '../../../src/plugins/utcTimeSystem/LocalClock';
class LADClock extends LocalClock {
/** /**
* A {@link Clock} that mocks a "latest available data" type tick source. * A {@link Clock} that mocks a "latest available data" type tick source.
* This is for testing purposes only, and behaves identically to a local clock. * This is for testing purposes only, and behaves identically to a local clock.
* It DOES NOT tick on receipt of data. * It DOES NOT tick on receipt of data.
* @constructor * @constructor
*/ */
function LADClock(period) { constructor(period) {
LocalClock.call(this, period); super(period);
this.key = 'test-lad'; this.key = 'test-lad';
this.mode = 'lad'; this.mode = 'lad';
this.cssClass = 'icon-suitcase'; this.cssClass = 'icon-suitcase';
this.name = 'Latest available data'; this.name = 'Latest available data';
this.description = 'Updates when when new data is available'; this.description = 'Updates when new data is available';
} }
}
LADClock.prototype = Object.create(LocalClock.prototype); export default LADClock;
return LADClock;
});

View File

@ -20,10 +20,10 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./LADClock'], function (LADClock) { import LADClock from './LADClock';
return function () {
return function (openmct) { export default function () {
openmct.time.addClock(new LADClock()); return function (openmct) {
}; openmct.time.addClock(new LADClock());
}; };
}); }

View File

@ -20,49 +20,47 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['moment'], function (moment) { import moment from 'moment';
const DATE_FORMAT = 'YYYY-MM-DD h:mm:ss.SSS a';
const DATE_FORMATS = [DATE_FORMAT, 'YYYY-MM-DD h:mm:ss a', 'YYYY-MM-DD h:mm a', 'YYYY-MM-DD']; const DATE_FORMAT = 'YYYY-MM-DD h:mm:ss.SSS a';
/** const DATE_FORMATS = [DATE_FORMAT, 'YYYY-MM-DD h:mm:ss a', 'YYYY-MM-DD h:mm a', 'YYYY-MM-DD'];
* @typedef Scale
* @property {number} min the minimum scale value, in ms
* @property {number} max the maximum scale value, in ms
*/
/** /**
* Formatter for UTC timestamps. Interprets numeric values as * @typedef Scale
* milliseconds since the start of 1970. * @property {number} min the minimum scale value, in ms
* * @property {number} max the maximum scale value, in ms
* @implements {Format} */
* @constructor
* @memberof platform/commonUI/formats /**
*/ * Formatter for UTC timestamps. Interprets numeric values as
function LocalTimeFormat() { * milliseconds since the start of 1970.
this.key = 'local-format'; *
* @implements {Format}
* @constructor
* @memberof platform/commonUI/formats
*/
export default function LocalTimeFormat() {
this.key = 'local-format';
}
/**
*
* @param value
* @returns {string} the formatted date
*/
LocalTimeFormat.prototype.format = function (value, scale) {
return moment(value).format(DATE_FORMAT);
};
LocalTimeFormat.prototype.parse = function (text) {
if (typeof text === 'number') {
return text;
} }
/** return moment(text, DATE_FORMATS).valueOf();
* };
* @param value
* @returns {string} the formatted date
*/
LocalTimeFormat.prototype.format = function (value, scale) {
return moment(value).format(DATE_FORMAT);
};
LocalTimeFormat.prototype.parse = function (text) { LocalTimeFormat.prototype.validate = function (text) {
if (typeof text === 'number') { return moment(text, DATE_FORMATS).isValid();
return text; };
}
return moment(text, DATE_FORMATS).valueOf();
};
LocalTimeFormat.prototype.validate = function (text) {
return moment(text, DATE_FORMATS).isValid();
};
return LocalTimeFormat;
});

View File

@ -20,13 +20,13 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { /**
/** * This time system supports UTC dates and provides a ticking clock source.
* This time system supports UTC dates and provides a ticking clock source. * @implements TimeSystem
* @implements TimeSystem * @constructor
* @constructor */
*/ export default class LocalTimeSystem {
function LocalTimeSystem() { constructor() {
/** /**
* Some metadata, which will be used to identify the time system in * Some metadata, which will be used to identify the time system in
* the UI * the UI
@ -41,6 +41,4 @@ define([], function () {
this.isUTCBased = true; this.isUTCBased = true;
} }
}
return LocalTimeSystem;
});

View File

@ -20,11 +20,12 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./LocalTimeSystem', './LocalTimeFormat'], function (LocalTimeSystem, LocalTimeFormat) { import LocalTimeFormat from './LocalTimeFormat';
return function () { import LocalTimeSystem from './LocalTimeSystem';
return function (openmct) {
openmct.time.addTimeSystem(new LocalTimeSystem()); export default function () {
openmct.telemetry.addFormat(new LocalTimeFormat()); return function (openmct) {
}; openmct.time.addTimeSystem(new LocalTimeSystem());
openmct.telemetry.addFormat(new LocalTimeFormat());
}; };
}); }

View File

@ -20,260 +20,260 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['uuid'], function ({ v4: uuid }) { import { v4 as uuid } from 'uuid';
return function Migrations(openmct) {
function getColumnNameKeyMap(domainObject) {
let composition = openmct.composition.get(domainObject);
if (composition) {
return composition.load().then((composees) => {
return composees.reduce((nameKeyMap, composee) => {
let metadata = openmct.telemetry.getMetadata(composee);
if (metadata !== undefined) {
metadata.values().forEach((value) => {
nameKeyMap[value.name] = value.key;
});
}
return nameKeyMap; export default function Migrations(openmct) {
}, {}); function getColumnNameKeyMap(domainObject) {
}); let composition = openmct.composition.get(domainObject);
} else { if (composition) {
return Promise.resolve([]); return composition.load().then((composees) => {
} return composees.reduce((nameKeyMap, composee) => {
} let metadata = openmct.telemetry.getMetadata(composee);
if (metadata !== undefined) {
function isTelemetry(domainObject) { metadata.values().forEach((value) => {
if ( nameKeyMap[value.name] = value.key;
openmct.telemetry.isTelemetryObject(domainObject) &&
domainObject.type !== 'summary-widget' &&
domainObject.type !== 'example.imagery'
) {
return true;
} else {
return false;
}
}
function migrateDisplayLayout(domainObject, childObjects) {
const DEFAULT_GRID_SIZE = [32, 32];
let migratedObject = Object.assign({}, domainObject);
let panels = migratedObject.configuration.layout.panels;
let items = [];
Object.keys(panels).forEach((key) => {
let panel = panels[key];
let childDomainObject = childObjects[key];
let identifier = undefined;
if (isTelemetry(childDomainObject)) {
// If object is a telemetry point, convert it to a plot and
// replace the object in migratedObject composition with the plot.
identifier = {
key: uuid(),
namespace: migratedObject.identifier.namespace
};
let plotObject = {
identifier: identifier,
location: childDomainObject.location,
name: childDomainObject.name,
type: 'telemetry.plot.overlay'
};
let plotType = openmct.types.get('telemetry.plot.overlay');
plotType.definition.initialize(plotObject);
plotObject.composition.push(childDomainObject.identifier);
openmct.objects.mutate(plotObject, 'persisted', Date.now());
let keyString = openmct.objects.makeKeyString(childDomainObject.identifier);
let clonedComposition = Object.assign([], migratedObject.composition);
clonedComposition.forEach((objIdentifier, index) => {
if (openmct.objects.makeKeyString(objIdentifier) === keyString) {
migratedObject.composition[index] = plotObject.identifier;
}
});
}
items.push({
width: panel.dimensions[0],
height: panel.dimensions[1],
x: panel.position[0],
y: panel.position[1],
identifier: identifier || childDomainObject.identifier,
id: uuid(),
type: 'subobject-view',
hasFrame: panel.hasFrame
});
});
migratedObject.configuration.items = items;
migratedObject.configuration.layoutGrid = migratedObject.layoutGrid || DEFAULT_GRID_SIZE;
delete migratedObject.layoutGrid;
delete migratedObject.configuration.layout;
return migratedObject;
}
function migrateFixedPositionConfiguration(elements, telemetryObjects, gridSize) {
const DEFAULT_STROKE = 'transparent';
const DEFAULT_SIZE = '13px';
const DEFAULT_COLOR = '';
const DEFAULT_FILL = '';
let items = [];
elements.forEach((element) => {
let item = {
x: element.x,
y: element.y,
width: element.width,
height: element.height,
id: uuid()
};
if (!element.useGrid) {
item.x = Math.round(item.x / gridSize[0]);
item.y = Math.round(item.y / gridSize[1]);
item.width = Math.round(item.width / gridSize[0]);
item.height = Math.round(item.height / gridSize[1]);
}
if (element.type === 'fixed.telemetry') {
item.type = 'telemetry-view';
item.stroke = element.stroke || DEFAULT_STROKE;
item.fill = element.fill || DEFAULT_FILL;
item.color = element.color || DEFAULT_COLOR;
item.size = element.size || DEFAULT_SIZE;
item.identifier = telemetryObjects[element.id].identifier;
item.displayMode = element.titled ? 'all' : 'value';
item.value = openmct.telemetry
.getMetadata(telemetryObjects[element.id])
.getDefaultDisplayValue()?.key;
} else if (element.type === 'fixed.box') {
item.type = 'box-view';
item.stroke = element.stroke || DEFAULT_STROKE;
item.fill = element.fill || DEFAULT_FILL;
} else if (element.type === 'fixed.line') {
item.type = 'line-view';
item.x2 = element.x2;
item.y2 = element.y2;
item.stroke = element.stroke || DEFAULT_STROKE;
delete item.height;
delete item.width;
} else if (element.type === 'fixed.text') {
item.type = 'text-view';
item.text = element.text;
item.stroke = element.stroke || DEFAULT_STROKE;
item.fill = element.fill || DEFAULT_FILL;
item.color = element.color || DEFAULT_COLOR;
item.size = element.size || DEFAULT_SIZE;
} else if (element.type === 'fixed.image') {
item.type = 'image-view';
item.url = element.url;
item.stroke = element.stroke || DEFAULT_STROKE;
}
items.push(item);
});
return items;
}
return [
{
check(domainObject) {
return (
domainObject?.type === 'layout' &&
domainObject.configuration &&
domainObject.configuration.layout
);
},
migrate(domainObject) {
let childObjects = {};
let promises = Object.keys(domainObject.configuration.layout.panels).map((key) => {
return openmct.objects.get(key).then((object) => {
childObjects[key] = object;
}); });
}); }
return Promise.all(promises).then(function () { return nameKeyMap;
return migrateDisplayLayout(domainObject, childObjects); }, {});
}); });
} } else {
}, return Promise.resolve([]);
{ }
check(domainObject) { }
return (
domainObject?.type === 'telemetry.fixed' &&
domainObject.configuration &&
domainObject.configuration['fixed-display']
);
},
migrate(domainObject) {
const DEFAULT_GRID_SIZE = [64, 16];
let newLayoutObject = {
identifier: domainObject.identifier,
location: domainObject.location,
name: domainObject.name,
type: 'layout'
};
let gridSize = domainObject.layoutGrid || DEFAULT_GRID_SIZE;
let layoutType = openmct.types.get('layout');
layoutType.definition.initialize(newLayoutObject);
newLayoutObject.composition = domainObject.composition;
newLayoutObject.configuration.layoutGrid = gridSize;
let elements = domainObject.configuration['fixed-display'].elements; function isTelemetry(domainObject) {
let telemetryObjects = {}; if (
let promises = elements.map((element) => { openmct.telemetry.isTelemetryObject(domainObject) &&
if (element.id) { domainObject.type !== 'summary-widget' &&
return openmct.objects.get(element.id).then((object) => { domainObject.type !== 'example.imagery'
telemetryObjects[element.id] = object; ) {
}); return true;
} else { } else {
return Promise.resolve(false); return false;
} }
}); }
return Promise.all(promises).then(function () { function migrateDisplayLayout(domainObject, childObjects) {
newLayoutObject.configuration.items = migrateFixedPositionConfiguration( const DEFAULT_GRID_SIZE = [32, 32];
elements, let migratedObject = Object.assign({}, domainObject);
telemetryObjects, let panels = migratedObject.configuration.layout.panels;
gridSize let items = [];
);
return newLayoutObject; Object.keys(panels).forEach((key) => {
}); let panel = panels[key];
} let childDomainObject = childObjects[key];
}, let identifier = undefined;
{
check(domainObject) {
return (
domainObject?.type === 'table' &&
domainObject.configuration &&
domainObject.configuration.table
);
},
migrate(domainObject) {
let currentTableConfiguration = domainObject.configuration.table || {};
let currentColumnConfiguration = currentTableConfiguration.columns || {};
return getColumnNameKeyMap(domainObject).then((nameKeyMap) => { if (isTelemetry(childDomainObject)) {
let hiddenColumns = Object.keys(currentColumnConfiguration) // If object is a telemetry point, convert it to a plot and
.filter((columnName) => { // replace the object in migratedObject composition with the plot.
return currentColumnConfiguration[columnName] === false; identifier = {
}) key: uuid(),
.reduce((hiddenColumnsMap, hiddenColumnName) => { namespace: migratedObject.identifier.namespace
let key = nameKeyMap[hiddenColumnName]; };
hiddenColumnsMap[key] = true; let plotObject = {
identifier: identifier,
location: childDomainObject.location,
name: childDomainObject.name,
type: 'telemetry.plot.overlay'
};
let plotType = openmct.types.get('telemetry.plot.overlay');
plotType.definition.initialize(plotObject);
plotObject.composition.push(childDomainObject.identifier);
openmct.objects.mutate(plotObject, 'persisted', Date.now());
return hiddenColumnsMap; let keyString = openmct.objects.makeKeyString(childDomainObject.identifier);
}, {}); let clonedComposition = Object.assign([], migratedObject.composition);
clonedComposition.forEach((objIdentifier, index) => {
domainObject.configuration.hiddenColumns = hiddenColumns; if (openmct.objects.makeKeyString(objIdentifier) === keyString) {
delete domainObject.configuration.table; migratedObject.composition[index] = plotObject.identifier;
}
return domainObject; });
});
}
} }
];
}; items.push({
}); width: panel.dimensions[0],
height: panel.dimensions[1],
x: panel.position[0],
y: panel.position[1],
identifier: identifier || childDomainObject.identifier,
id: uuid(),
type: 'subobject-view',
hasFrame: panel.hasFrame
});
});
migratedObject.configuration.items = items;
migratedObject.configuration.layoutGrid = migratedObject.layoutGrid || DEFAULT_GRID_SIZE;
delete migratedObject.layoutGrid;
delete migratedObject.configuration.layout;
return migratedObject;
}
function migrateFixedPositionConfiguration(elements, telemetryObjects, gridSize) {
const DEFAULT_STROKE = 'transparent';
const DEFAULT_SIZE = '13px';
const DEFAULT_COLOR = '';
const DEFAULT_FILL = '';
let items = [];
elements.forEach((element) => {
let item = {
x: element.x,
y: element.y,
width: element.width,
height: element.height,
id: uuid()
};
if (!element.useGrid) {
item.x = Math.round(item.x / gridSize[0]);
item.y = Math.round(item.y / gridSize[1]);
item.width = Math.round(item.width / gridSize[0]);
item.height = Math.round(item.height / gridSize[1]);
}
if (element.type === 'fixed.telemetry') {
item.type = 'telemetry-view';
item.stroke = element.stroke || DEFAULT_STROKE;
item.fill = element.fill || DEFAULT_FILL;
item.color = element.color || DEFAULT_COLOR;
item.size = element.size || DEFAULT_SIZE;
item.identifier = telemetryObjects[element.id].identifier;
item.displayMode = element.titled ? 'all' : 'value';
item.value = openmct.telemetry
.getMetadata(telemetryObjects[element.id])
.getDefaultDisplayValue()?.key;
} else if (element.type === 'fixed.box') {
item.type = 'box-view';
item.stroke = element.stroke || DEFAULT_STROKE;
item.fill = element.fill || DEFAULT_FILL;
} else if (element.type === 'fixed.line') {
item.type = 'line-view';
item.x2 = element.x2;
item.y2 = element.y2;
item.stroke = element.stroke || DEFAULT_STROKE;
delete item.height;
delete item.width;
} else if (element.type === 'fixed.text') {
item.type = 'text-view';
item.text = element.text;
item.stroke = element.stroke || DEFAULT_STROKE;
item.fill = element.fill || DEFAULT_FILL;
item.color = element.color || DEFAULT_COLOR;
item.size = element.size || DEFAULT_SIZE;
} else if (element.type === 'fixed.image') {
item.type = 'image-view';
item.url = element.url;
item.stroke = element.stroke || DEFAULT_STROKE;
}
items.push(item);
});
return items;
}
return [
{
check(domainObject) {
return (
domainObject?.type === 'layout' &&
domainObject.configuration &&
domainObject.configuration.layout
);
},
migrate(domainObject) {
let childObjects = {};
let promises = Object.keys(domainObject.configuration.layout.panels).map((key) => {
return openmct.objects.get(key).then((object) => {
childObjects[key] = object;
});
});
return Promise.all(promises).then(function () {
return migrateDisplayLayout(domainObject, childObjects);
});
}
},
{
check(domainObject) {
return (
domainObject?.type === 'telemetry.fixed' &&
domainObject.configuration &&
domainObject.configuration['fixed-display']
);
},
migrate(domainObject) {
const DEFAULT_GRID_SIZE = [64, 16];
let newLayoutObject = {
identifier: domainObject.identifier,
location: domainObject.location,
name: domainObject.name,
type: 'layout'
};
let gridSize = domainObject.layoutGrid || DEFAULT_GRID_SIZE;
let layoutType = openmct.types.get('layout');
layoutType.definition.initialize(newLayoutObject);
newLayoutObject.composition = domainObject.composition;
newLayoutObject.configuration.layoutGrid = gridSize;
let elements = domainObject.configuration['fixed-display'].elements;
let telemetryObjects = {};
let promises = elements.map((element) => {
if (element.id) {
return openmct.objects.get(element.id).then((object) => {
telemetryObjects[element.id] = object;
});
} else {
return Promise.resolve(false);
}
});
return Promise.all(promises).then(function () {
newLayoutObject.configuration.items = migrateFixedPositionConfiguration(
elements,
telemetryObjects,
gridSize
);
return newLayoutObject;
});
}
},
{
check(domainObject) {
return (
domainObject?.type === 'table' &&
domainObject.configuration &&
domainObject.configuration.table
);
},
migrate(domainObject) {
let currentTableConfiguration = domainObject.configuration.table || {};
let currentColumnConfiguration = currentTableConfiguration.columns || {};
return getColumnNameKeyMap(domainObject).then((nameKeyMap) => {
let hiddenColumns = Object.keys(currentColumnConfiguration)
.filter((columnName) => {
return currentColumnConfiguration[columnName] === false;
})
.reduce((hiddenColumnsMap, hiddenColumnName) => {
let key = nameKeyMap[hiddenColumnName];
hiddenColumnsMap[key] = true;
return hiddenColumnsMap;
}, {});
domainObject.configuration.hiddenColumns = hiddenColumns;
delete domainObject.configuration.table;
return domainObject;
});
}
}
];
}

View File

@ -20,225 +20,155 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([ import ExampleDataVisualizationSourcePlugin from '../../example/dataVisualization/plugin';
'lodash', import EventGeneratorPlugin from '../../example/eventGenerator/plugin';
'./utcTimeSystem/plugin', import ExampleTags from '../../example/exampleTags/plugin';
'./remoteClock/plugin', import ExampleUser from '../../example/exampleUser/plugin';
'./localTimeSystem/plugin', import ExampleFaultSource from '../../example/faultManagement/exampleFaultSource';
'./ISOTimeFormat/plugin', import GeneratorPlugin from '../../example/generator/plugin';
'./myItems/plugin', import ExampleImagery from '../../example/imagery/plugin';
'../../example/generator/plugin', import AutoflowPlugin from './autoflow/AutoflowTabularPlugin';
'../../example/eventGenerator/plugin', import BarChartPlugin from './charts/bar/plugin';
'../../example/dataVisualization/plugin', import ScatterPlotPlugin from './charts/scatter/plugin';
'./autoflow/AutoflowTabularPlugin', import ClearData from './clearData/plugin';
'./timeConductor/plugin', import Clock from './clock/plugin';
'../../example/imagery/plugin', import ConditionPlugin from './condition/plugin';
'../../example/faultManagement/exampleFaultSource', import ConditionWidgetPlugin from './conditionWidget/plugin';
'./imagery/plugin', import CouchDBSearchFolder from './CouchDBSearchFolder/plugin';
'./summaryWidget/plugin', import DefaultRootName from './defaultRootName/plugin';
'./URLIndicatorPlugin/URLIndicatorPlugin', import DeviceClassifier from './DeviceClassifier/plugin';
'./telemetryMean/plugin', import DisplayLayoutPlugin from './displayLayout/plugin';
'./plot/plugin', import FaultManagementPlugin from './faultManagement/FaultManagementPlugin';
'./charts/bar/plugin', import Filters from './filters/plugin';
'./charts/scatter/plugin', import FlexibleLayout from './flexibleLayout/plugin';
'./telemetryTable/plugin', import FolderView from './folderView/plugin';
'./staticRootPlugin/plugin', import FormActions from './formActions/plugin';
'./notebook/plugin', import GaugePlugin from './gauge/GaugePlugin';
'./displayLayout/plugin', import GoToOriginalAction from './goToOriginalAction/plugin';
'./formActions/plugin', import Hyperlink from './hyperlink/plugin';
'./folderView/plugin', import ImageryPlugin from './imagery/plugin';
'./flexibleLayout/plugin', import InspectorDataVisualization from './inspectorDataVisualization/plugin';
'./tabs/plugin', import InspectorViews from './inspectorViews/plugin';
'./LADTable/plugin', import ObjectInterceptors from './interceptors/plugin';
'./filters/plugin', import ISOTimeFormat from './ISOTimeFormat/plugin';
'./objectMigration/plugin', import LADTable from './LADTable/plugin';
'./goToOriginalAction/plugin', import LocalStorage from './localStorage/plugin';
'./openInNewTabAction/plugin', import LocalTimeSystem from './localTimeSystem/plugin';
'./clearData/plugin', import MyItems from './myItems/plugin';
'./webPage/plugin', import NewFolderAction from './newFolderAction/plugin';
'./condition/plugin', import { NotebookPlugin, RestrictedNotebookPlugin } from './notebook/plugin';
'./conditionWidget/plugin', import NotificationIndicator from './notificationIndicator/plugin';
'./themes/espresso', import ObjectMigration from './objectMigration/plugin';
'./themes/snow', import OpenInNewTabAction from './openInNewTabAction/plugin';
'./URLTimeSettingsSynchronizer/plugin', import OperatorStatus from './operatorStatus/plugin';
'./notificationIndicator/plugin', import PerformanceIndicator from './performanceIndicator/plugin';
'./newFolderAction/plugin', import CouchDBPlugin from './persistence/couch/plugin';
'./persistence/couch/plugin', import PlanLayout from './plan/plugin';
'./defaultRootName/plugin', import PlotPlugin from './plot/plugin';
'./plan/plugin', import RemoteClock from './remoteClock/plugin';
'./viewDatumAction/plugin', import StaticRootPlugin from './staticRootPlugin/plugin';
'./viewLargeAction/plugin', import SummaryWidget from './summaryWidget/plugin';
'./interceptors/plugin', import Tabs from './tabs/plugin';
'./performanceIndicator/plugin', import TelemetryMean from './telemetryMean/plugin';
'./CouchDBSearchFolder/plugin', import TelemetryTablePlugin from './telemetryTable/plugin';
'./timeline/plugin', import Espresso from './themes/espresso';
'./hyperlink/plugin', import Snow from './themes/snow';
'./clock/plugin', import TimeConductorPlugin from './timeConductor/plugin';
'./DeviceClassifier/plugin', import Timeline from './timeline/plugin';
'./timer/plugin', import TimeList from './timelist/plugin';
'./userIndicator/plugin', import Timer from './timer/plugin';
'../../example/exampleUser/plugin', import URLIndicatorPlugin from './URLIndicatorPlugin/URLIndicatorPlugin';
'./localStorage/plugin', import URLTimeSettingsSynchronizer from './URLTimeSettingsSynchronizer/plugin';
'./operatorStatus/plugin', import UserIndicator from './userIndicator/plugin';
'./gauge/GaugePlugin', import UTCTimeSystem from './utcTimeSystem/plugin';
'./timelist/plugin', import ViewDatumAction from './viewDatumAction/plugin';
'./faultManagement/FaultManagementPlugin', import ViewLargeAction from './viewLargeAction/plugin';
'../../example/exampleTags/plugin', import WebPagePlugin from './webPage/plugin';
'./inspectorViews/plugin',
'./inspectorDataVisualization/plugin'
], function (
_,
UTCTimeSystem,
RemoteClock,
LocalTimeSystem,
ISOTimeFormat,
MyItems,
GeneratorPlugin,
EventGeneratorPlugin,
ExampleDataVisualizationSourcePlugin,
AutoflowPlugin,
TimeConductorPlugin,
ExampleImagery,
ExampleFaultSource,
ImageryPlugin,
SummaryWidget,
URLIndicatorPlugin,
TelemetryMean,
PlotPlugin,
BarChartPlugin,
ScatterPlotPlugin,
TelemetryTablePlugin,
StaticRootPlugin,
Notebook,
DisplayLayoutPlugin,
FormActions,
FolderView,
FlexibleLayout,
Tabs,
LADTable,
Filters,
ObjectMigration,
GoToOriginalAction,
OpenInNewTabAction,
ClearData,
WebPagePlugin,
ConditionPlugin,
ConditionWidgetPlugin,
Espresso,
Snow,
URLTimeSettingsSynchronizer,
NotificationIndicator,
NewFolderAction,
CouchDBPlugin,
DefaultRootName,
PlanLayout,
ViewDatumAction,
ViewLargeAction,
ObjectInterceptors,
PerformanceIndicator,
CouchDBSearchFolder,
Timeline,
Hyperlink,
Clock,
DeviceClassifier,
Timer,
UserIndicator,
ExampleUser,
LocalStorage,
OperatorStatus,
GaugePlugin,
TimeList,
FaultManagementPlugin,
ExampleTags,
InspectorViews,
InspectorDataVisualization
) {
const plugins = {};
plugins.example = {}; const plugins = {};
plugins.example.ExampleUser = ExampleUser.default;
plugins.example.ExampleImagery = ExampleImagery.default;
plugins.example.ExampleFaultSource = ExampleFaultSource.default;
plugins.example.EventGeneratorPlugin = EventGeneratorPlugin.default;
plugins.example.ExampleDataVisualizationSourcePlugin =
ExampleDataVisualizationSourcePlugin.default;
plugins.example.ExampleTags = ExampleTags.default;
plugins.example.Generator = () => GeneratorPlugin.default;
plugins.UTCTimeSystem = UTCTimeSystem.default; plugins.example = {};
plugins.LocalTimeSystem = LocalTimeSystem; plugins.example.ExampleUser = ExampleUser;
plugins.RemoteClock = RemoteClock.default; plugins.example.ExampleImagery = ExampleImagery;
plugins.example.ExampleFaultSource = ExampleFaultSource;
plugins.example.EventGeneratorPlugin = EventGeneratorPlugin;
plugins.example.ExampleDataVisualizationSourcePlugin = ExampleDataVisualizationSourcePlugin;
plugins.example.ExampleTags = ExampleTags;
plugins.example.Generator = () => GeneratorPlugin;
plugins.MyItems = MyItems.default; plugins.UTCTimeSystem = UTCTimeSystem;
plugins.LocalTimeSystem = LocalTimeSystem;
plugins.RemoteClock = RemoteClock;
plugins.StaticRootPlugin = StaticRootPlugin.default; plugins.MyItems = MyItems;
/** plugins.StaticRootPlugin = StaticRootPlugin;
* A tabular view showing the latest values of multiple telemetry points at
* once. Formatted so that labels and values are aligned.
*
* @param {Object} [options] Optional settings to apply to the autoflow
* tabular view. Currently supports one option, 'type'.
* @param {string} [options.type] The key of an object type to apply this view
* to exclusively.
*/
plugins.AutoflowView = AutoflowPlugin;
plugins.Conductor = TimeConductorPlugin.default; /**
* A tabular view showing the latest values of multiple telemetry points at
* once. Formatted so that labels and values are aligned.
*
* @param {Object} [options] Optional settings to apply to the autoflow
* tabular view. Currently supports one option, 'type'.
* @param {string} [options.type] The key of an object type to apply this view
* to exclusively.
*/
plugins.AutoflowView = AutoflowPlugin;
plugins.CouchDB = CouchDBPlugin.default; plugins.Conductor = TimeConductorPlugin;
plugins.ImageryPlugin = ImageryPlugin; plugins.CouchDB = CouchDBPlugin;
plugins.Plot = PlotPlugin.default;
plugins.BarChart = BarChartPlugin.default;
plugins.ScatterPlot = ScatterPlotPlugin.default;
plugins.TelemetryTable = TelemetryTablePlugin;
plugins.SummaryWidget = SummaryWidget; plugins.ImageryPlugin = ImageryPlugin;
plugins.TelemetryMean = TelemetryMean; plugins.Plot = PlotPlugin;
plugins.URLIndicator = URLIndicatorPlugin; plugins.BarChart = BarChartPlugin;
plugins.Notebook = Notebook.NotebookPlugin; plugins.ScatterPlot = ScatterPlotPlugin;
plugins.RestrictedNotebook = Notebook.RestrictedNotebookPlugin; plugins.TelemetryTable = TelemetryTablePlugin;
plugins.DisplayLayout = DisplayLayoutPlugin.default;
plugins.FaultManagement = FaultManagementPlugin.default;
plugins.FormActions = FormActions;
plugins.FolderView = FolderView.default;
plugins.Tabs = Tabs;
plugins.FlexibleLayout = FlexibleLayout;
plugins.LADTable = LADTable.default;
plugins.Filters = Filters;
plugins.ObjectMigration = ObjectMigration.default;
plugins.GoToOriginalAction = GoToOriginalAction.default;
plugins.OpenInNewTabAction = OpenInNewTabAction.default;
plugins.ClearData = ClearData.default;
plugins.WebPage = WebPagePlugin.default;
plugins.Espresso = Espresso.default;
plugins.Snow = Snow.default;
plugins.Condition = ConditionPlugin.default;
plugins.ConditionWidget = ConditionWidgetPlugin.default;
plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default;
plugins.NotificationIndicator = NotificationIndicator.default;
plugins.NewFolderAction = NewFolderAction.default;
plugins.ISOTimeFormat = ISOTimeFormat.default;
plugins.DefaultRootName = DefaultRootName.default;
plugins.PlanLayout = PlanLayout.default;
plugins.ViewDatumAction = ViewDatumAction.default;
plugins.ViewLargeAction = ViewLargeAction.default;
plugins.ObjectInterceptors = ObjectInterceptors.default;
plugins.PerformanceIndicator = PerformanceIndicator.default;
plugins.CouchDBSearchFolder = CouchDBSearchFolder.default;
plugins.Timeline = Timeline.default;
plugins.Hyperlink = Hyperlink.default;
plugins.Clock = Clock.default;
plugins.Timer = Timer.default;
plugins.DeviceClassifier = DeviceClassifier.default;
plugins.UserIndicator = UserIndicator.default;
plugins.LocalStorage = LocalStorage.default;
plugins.OperatorStatus = OperatorStatus.default;
plugins.Gauge = GaugePlugin.default;
plugins.Timelist = TimeList.default;
plugins.InspectorViews = InspectorViews.default;
plugins.InspectorDataVisualization = InspectorDataVisualization.default;
return plugins; plugins.SummaryWidget = SummaryWidget;
}); plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin;
plugins.Notebook = NotebookPlugin;
plugins.RestrictedNotebook = RestrictedNotebookPlugin;
plugins.DisplayLayout = DisplayLayoutPlugin;
plugins.FaultManagement = FaultManagementPlugin;
plugins.FormActions = FormActions;
plugins.FolderView = FolderView;
plugins.Tabs = Tabs;
plugins.FlexibleLayout = FlexibleLayout;
plugins.LADTable = LADTable;
plugins.Filters = Filters;
plugins.ObjectMigration = ObjectMigration;
plugins.GoToOriginalAction = GoToOriginalAction;
plugins.OpenInNewTabAction = OpenInNewTabAction;
plugins.ClearData = ClearData;
plugins.WebPage = WebPagePlugin;
plugins.Espresso = Espresso;
plugins.Snow = Snow;
plugins.Condition = ConditionPlugin;
plugins.ConditionWidget = ConditionWidgetPlugin;
plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer;
plugins.NotificationIndicator = NotificationIndicator;
plugins.NewFolderAction = NewFolderAction;
plugins.ISOTimeFormat = ISOTimeFormat;
plugins.DefaultRootName = DefaultRootName;
plugins.PlanLayout = PlanLayout;
plugins.ViewDatumAction = ViewDatumAction;
plugins.ViewLargeAction = ViewLargeAction;
plugins.ObjectInterceptors = ObjectInterceptors;
plugins.PerformanceIndicator = PerformanceIndicator;
plugins.CouchDBSearchFolder = CouchDBSearchFolder;
plugins.Timeline = Timeline;
plugins.Hyperlink = Hyperlink;
plugins.Clock = Clock;
plugins.Timer = Timer;
plugins.DeviceClassifier = DeviceClassifier;
plugins.UserIndicator = UserIndicator;
plugins.LocalStorage = LocalStorage;
plugins.OperatorStatus = OperatorStatus;
plugins.Gauge = GaugePlugin;
plugins.Timelist = TimeList;
plugins.InspectorViews = InspectorViews;
plugins.InspectorDataVisualization = InspectorDataVisualization;
export default plugins;

View File

@ -20,20 +20,16 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { /**
/** * Policy determining which views can apply to summary widget. Disables
* Policy determining which views can apply to summary widget. Disables * any view other than normal summary widget view.
* any view other than normal summary widget view. */
*/ export default function SummaryWidgetViewPolicy() {}
function SummaryWidgetViewPolicy() {}
SummaryWidgetViewPolicy.prototype.allow = function (view, domainObject) { SummaryWidgetViewPolicy.prototype.allow = function (view, domainObject) {
if (domainObject.getModel().type === 'summary-widget') { if (domainObject.getModel().type === 'summary-widget') {
return view.key === 'summary-widget-viewer'; return view.key === 'summary-widget-viewer';
} }
return true; return true;
}; };
return SummaryWidgetViewPolicy;
});

View File

@ -20,20 +20,16 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { export default function SummaryWidgetsCompositionPolicy(openmct) {
function SummaryWidgetsCompositionPolicy(openmct) { this.openmct = openmct;
this.openmct = openmct; }
SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) {
const parentType = parent.type;
if (parentType === 'summary-widget' && !this.openmct.telemetry.isTelemetryObject(child)) {
return false;
} }
SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) { return true;
const parentType = parent.type; };
if (parentType === 'summary-widget' && !this.openmct.telemetry.isTelemetryObject(child)) {
return false;
}
return true;
};
return SummaryWidgetsCompositionPolicy;
});

View File

@ -1,98 +1,87 @@
define([ import SummaryWidgetMetadataProvider from './src/telemetry/SummaryWidgetMetadataProvider';
'./SummaryWidgetsCompositionPolicy', import SummaryWidgetTelemetryProvider from './src/telemetry/SummaryWidgetTelemetryProvider';
'./src/telemetry/SummaryWidgetMetadataProvider', import SummaryWidgetViewProvider from './src/views/SummaryWidgetViewProvider';
'./src/telemetry/SummaryWidgetTelemetryProvider', import SummaryWidgetsCompositionPolicy from './SummaryWidgetsCompositionPolicy';
'./src/views/SummaryWidgetViewProvider',
'./SummaryWidgetViewPolicy' export default function plugin() {
], function ( const widgetType = {
SummaryWidgetsCompositionPolicy, name: 'Summary Widget',
SummaryWidgetMetadataProvider, description: 'A compact status update for collections of telemetry-producing items',
SummaryWidgetTelemetryProvider, cssClass: 'icon-summary-widget',
SummaryWidgetViewProvider, initialize: function (domainObject) {
SummaryWidgetViewPolicy domainObject.composition = [];
) { domainObject.configuration = {
function plugin() { ruleOrder: ['default'],
const widgetType = { ruleConfigById: {
name: 'Summary Widget', default: {
description: 'A compact status update for collections of telemetry-producing items', name: 'Default',
cssClass: 'icon-summary-widget', label: 'Unnamed Rule',
initialize: function (domainObject) { message: '',
domainObject.composition = []; id: 'default',
domainObject.configuration = { icon: ' ',
ruleOrder: ['default'], style: {
ruleConfigById: { color: '#ffffff',
default: { 'background-color': '#38761d',
name: 'Default', 'border-color': 'rgba(0,0,0,0)'
label: 'Unnamed Rule',
message: '',
id: 'default',
icon: ' ',
style: {
color: '#ffffff',
'background-color': '#38761d',
'border-color': 'rgba(0,0,0,0)'
},
description: 'Default appearance for the widget',
conditions: [
{
object: '',
key: '',
operation: '',
values: []
}
],
jsCondition: '',
trigger: 'any',
expanded: 'true'
}
},
testDataConfig: [
{
object: '',
key: '',
value: ''
}
]
};
domainObject.openNewTab = 'thisTab';
domainObject.telemetry = {};
},
form: [
{
key: 'url',
name: 'URL',
control: 'textfield',
required: false,
cssClass: 'l-input-lg'
},
{
key: 'openNewTab',
name: 'Tab to Open Hyperlink',
control: 'select',
options: [
{
value: 'thisTab',
name: 'Open in this tab'
}, },
{ description: 'Default appearance for the widget',
value: 'newTab', conditions: [
name: 'Open in a new tab' {
} object: '',
], key: '',
cssClass: 'l-inline' operation: '',
} values: []
] }
}; ],
jsCondition: '',
trigger: 'any',
expanded: 'true'
}
},
testDataConfig: [
{
object: '',
key: '',
value: ''
}
]
};
domainObject.openNewTab = 'thisTab';
domainObject.telemetry = {};
},
form: [
{
key: 'url',
name: 'URL',
control: 'textfield',
required: false,
cssClass: 'l-input-lg'
},
{
key: 'openNewTab',
name: 'Tab to Open Hyperlink',
control: 'select',
options: [
{
value: 'thisTab',
name: 'Open in this tab'
},
{
value: 'newTab',
name: 'Open in a new tab'
}
],
cssClass: 'l-inline'
}
]
};
return function install(openmct) { return function install(openmct) {
openmct.types.addType('summary-widget', widgetType); openmct.types.addType('summary-widget', widgetType);
let compositionPolicy = new SummaryWidgetsCompositionPolicy(openmct); let compositionPolicy = new SummaryWidgetsCompositionPolicy(openmct);
openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy)); openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy));
openmct.telemetry.addProvider(new SummaryWidgetMetadataProvider(openmct)); openmct.telemetry.addProvider(new SummaryWidgetMetadataProvider(openmct));
openmct.telemetry.addProvider(new SummaryWidgetTelemetryProvider(openmct)); openmct.telemetry.addProvider(new SummaryWidgetTelemetryProvider(openmct));
openmct.objectViews.addProvider(new SummaryWidgetViewProvider(openmct)); openmct.objectViews.addProvider(new SummaryWidgetViewProvider(openmct));
}; };
} }
return plugin;
});

View File

@ -19,242 +19,232 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([
'../res/conditionTemplate.html', import EventEmitter from 'EventEmitter';
'./input/ObjectSelect',
'./input/KeySelect', import * as templateHelpers from '../../../utils/template/templateHelpers';
'./input/OperationSelect', import conditionTemplate from '../res/conditionTemplate.html';
'./eventHelpers', import eventHelpers from './eventHelpers';
'../../../utils/template/templateHelpers', import KeySelect from './input/KeySelect';
'EventEmitter' import ObjectSelect from './input/ObjectSelect';
], function ( import OperationSelect from './input/OperationSelect';
conditionTemplate,
ObjectSelect, /**
KeySelect, * Represents an individual condition for a summary widget rule. Manages the
OperationSelect, * associated inputs and view.
eventHelpers, * @param {Object} conditionConfig The configuration for this condition, consisting
templateHelpers, * of object, key, operation, and values fields
EventEmitter * @param {number} index the index of this Condition object in it's parent Rule's data model,
) { * to be injected into callbacks for removes
* @param {ConditionManager} conditionManager A ConditionManager instance for populating
* selects with configuration data
*/
export default function Condition(conditionConfig, index, conditionManager) {
eventHelpers.extend(this);
this.config = conditionConfig;
this.index = index;
this.conditionManager = conditionManager;
this.domElement = templateHelpers.convertTemplateToHTML(conditionTemplate)[0];
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['remove', 'duplicate', 'change'];
this.deleteButton = this.domElement.querySelector('.t-delete');
this.duplicateButton = this.domElement.querySelector('.t-duplicate');
this.selects = {};
this.valueInputs = [];
const self = this;
/** /**
* Represents an individual condition for a summary widget rule. Manages the * Event handler for a change in one of this conditions' custom selects
* associated inputs and view. * @param {string} value The new value of this selects
* @param {Object} conditionConfig The configuration for this condition, consisting * @param {string} property The property of this condition to modify
* of object, key, operation, and values fields * @private
* @param {number} index the index of this Condition object in it's parent Rule's data model,
* to be injected into callbacks for removes
* @param {ConditionManager} conditionManager A ConditionManager instance for populating
* selects with configuration data
*/ */
function Condition(conditionConfig, index, conditionManager) { function onSelectChange(value, property) {
eventHelpers.extend(this); if (property === 'operation') {
this.config = conditionConfig; self.generateValueInputs(value);
this.index = index;
this.conditionManager = conditionManager;
this.domElement = templateHelpers.convertTemplateToHTML(conditionTemplate)[0];
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['remove', 'duplicate', 'change'];
this.deleteButton = this.domElement.querySelector('.t-delete');
this.duplicateButton = this.domElement.querySelector('.t-duplicate');
this.selects = {};
this.valueInputs = [];
const self = this;
/**
* Event handler for a change in one of this conditions' custom selects
* @param {string} value The new value of this selects
* @param {string} property The property of this condition to modify
* @private
*/
function onSelectChange(value, property) {
if (property === 'operation') {
self.generateValueInputs(value);
}
self.eventEmitter.emit('change', {
value: value,
property: property,
index: self.index
});
} }
this.handleObjectChange = (value) => onSelectChange(value, 'object'); self.eventEmitter.emit('change', {
this.handleKeyChange = (value) => onSelectChange(value, 'key'); value: value,
property: property,
/** index: self.index
* Event handler for this conditions value inputs
* @param {Event} event The oninput event that triggered this callback
* @private
*/
function onValueInput(event) {
const elem = event.target;
const value = isNaN(Number(elem.value)) ? elem.value : Number(elem.value);
const inputIndex = self.valueInputs.indexOf(elem);
self.eventEmitter.emit('change', {
value: value,
property: 'values[' + inputIndex + ']',
index: self.index
});
}
this.listenTo(this.deleteButton, 'click', this.remove, this);
this.listenTo(this.duplicateButton, 'click', this.duplicate, this);
this.selects.object = new ObjectSelect(this.config, this.conditionManager, [
['any', 'any telemetry'],
['all', 'all telemetry']
]);
this.selects.key = new KeySelect(this.config, this.selects.object, this.conditionManager);
this.selects.operation = new OperationSelect(
this.config,
this.selects.key,
this.conditionManager,
function (value) {
onSelectChange(value, 'operation');
}
);
this.selects.object.on('change', this.handleObjectChange);
this.selects.key.on('change', this.handleKeyChange);
Object.values(this.selects).forEach(function (select) {
self.domElement.querySelector('.t-configuration').append(select.getDOM());
}); });
this.listenTo(this.domElement.querySelector('.t-value-inputs'), 'input', onValueInput);
} }
Condition.prototype.getDOM = function (container) { this.handleObjectChange = (value) => onSelectChange(value, 'object');
return this.domElement; this.handleKeyChange = (value) => onSelectChange(value, 'key');
};
/** /**
* Register a callback with this condition: supported callbacks are remove, change, * Event handler for this conditions value inputs
* duplicate * @param {Event} event The oninput event that triggered this callback
* @param {string} event The key for the event to listen to * @private
* @param {function} callback The function that this rule will invoke on this event
* @param {Object} context A reference to a scope to use as the context for
* context for the callback function
*/ */
Condition.prototype.on = function (event, callback, context) { function onValueInput(event) {
if (this.supportedCallbacks.includes(event)) { const elem = event.target;
this.eventEmitter.on(event, callback, context || this); const value = isNaN(Number(elem.value)) ? elem.value : Number(elem.value);
const inputIndex = self.valueInputs.indexOf(elem);
self.eventEmitter.emit('change', {
value: value,
property: 'values[' + inputIndex + ']',
index: self.index
});
}
this.listenTo(this.deleteButton, 'click', this.remove, this);
this.listenTo(this.duplicateButton, 'click', this.duplicate, this);
this.selects.object = new ObjectSelect(this.config, this.conditionManager, [
['any', 'any telemetry'],
['all', 'all telemetry']
]);
this.selects.key = new KeySelect(this.config, this.selects.object, this.conditionManager);
this.selects.operation = new OperationSelect(
this.config,
this.selects.key,
this.conditionManager,
function (value) {
onSelectChange(value, 'operation');
} }
}; );
/** this.selects.object.on('change', this.handleObjectChange);
* Hide the appropriate inputs when this is the only condition this.selects.key.on('change', this.handleKeyChange);
*/
Condition.prototype.hideButtons = function () {
this.deleteButton.style.display = 'none';
};
/** Object.values(this.selects).forEach(function (select) {
* Remove this condition from the configuration. Invokes any registered self.domElement.querySelector('.t-configuration').append(select.getDOM());
* remove callbacks });
*/
Condition.prototype.remove = function () {
this.selects.object.off('change', this.handleObjectChange);
this.selects.key.off('change', this.handleKeyChange);
this.eventEmitter.emit('remove', this.index);
this.destroy();
};
Condition.prototype.destroy = function () { this.listenTo(this.domElement.querySelector('.t-value-inputs'), 'input', onValueInput);
this.stopListening(); }
Object.values(this.selects).forEach(function (select) {
select.destroy();
});
};
/** Condition.prototype.getDOM = function (container) {
* Make a deep clone of this condition's configuration and invoke any duplicate return this.domElement;
* callbacks with the cloned configuration and this rule's index };
*/
Condition.prototype.duplicate = function () {
const sourceCondition = JSON.parse(JSON.stringify(this.config));
this.eventEmitter.emit('duplicate', {
sourceCondition: sourceCondition,
index: this.index
});
};
/** /**
* When an operation is selected, create the appropriate value inputs * Register a callback with this condition: supported callbacks are remove, change,
* and add them to the view. If an operation is of type enum, create * duplicate
* a drop-down menu instead. * @param {string} event The key for the event to listen to
* * @param {function} callback The function that this rule will invoke on this event
* @param {string} operation The key of currently selected operation * @param {Object} context A reference to a scope to use as the context for
*/ * context for the callback function
Condition.prototype.generateValueInputs = function (operation) { */
const evaluator = this.conditionManager.getEvaluator(); Condition.prototype.on = function (event, callback, context) {
const inputArea = this.domElement.querySelector('.t-value-inputs'); if (this.supportedCallbacks.includes(event)) {
let inputCount; this.eventEmitter.on(event, callback, context || this);
let inputType; }
let newInput; };
let index = 0;
let emitChange = false;
inputArea.innerHTML = ''; /**
this.valueInputs = []; * Hide the appropriate inputs when this is the only condition
this.config.values = this.config.values || []; */
Condition.prototype.hideButtons = function () {
this.deleteButton.style.display = 'none';
};
if (evaluator.getInputCount(operation)) { /**
inputCount = evaluator.getInputCount(operation); * Remove this condition from the configuration. Invokes any registered
inputType = evaluator.getInputType(operation); * remove callbacks
*/
Condition.prototype.remove = function () {
this.selects.object.off('change', this.handleObjectChange);
this.selects.key.off('change', this.handleKeyChange);
this.eventEmitter.emit('remove', this.index);
this.destroy();
};
while (index < inputCount) { Condition.prototype.destroy = function () {
if (inputType === 'select') { this.stopListening();
const options = this.generateSelectOptions(); Object.values(this.selects).forEach(function (select) {
select.destroy();
});
};
newInput = document.createElement('select'); /**
newInput.appendChild(options); * Make a deep clone of this condition's configuration and invoke any duplicate
* callbacks with the cloned configuration and this rule's index
*/
Condition.prototype.duplicate = function () {
const sourceCondition = JSON.parse(JSON.stringify(this.config));
this.eventEmitter.emit('duplicate', {
sourceCondition: sourceCondition,
index: this.index
});
};
emitChange = true; /**
} else { * When an operation is selected, create the appropriate value inputs
const defaultValue = inputType === 'number' ? 0 : ''; * and add them to the view. If an operation is of type enum, create
const value = this.config.values[index] || defaultValue; * a drop-down menu instead.
this.config.values[index] = value; *
* @param {string} operation The key of currently selected operation
*/
Condition.prototype.generateValueInputs = function (operation) {
const evaluator = this.conditionManager.getEvaluator();
const inputArea = this.domElement.querySelector('.t-value-inputs');
let inputCount;
let inputType;
let newInput;
let index = 0;
let emitChange = false;
newInput = document.createElement('input'); inputArea.innerHTML = '';
newInput.type = `${inputType}`; this.valueInputs = [];
newInput.value = `${value}`; this.config.values = this.config.values || [];
}
this.valueInputs.push(newInput); if (evaluator.getInputCount(operation)) {
inputArea.appendChild(newInput); inputCount = evaluator.getInputCount(operation);
index += 1; inputType = evaluator.getInputType(operation);
while (index < inputCount) {
if (inputType === 'select') {
const options = this.generateSelectOptions();
newInput = document.createElement('select');
newInput.appendChild(options);
emitChange = true;
} else {
const defaultValue = inputType === 'number' ? 0 : '';
const value = this.config.values[index] || defaultValue;
this.config.values[index] = value;
newInput = document.createElement('input');
newInput.type = `${inputType}`;
newInput.value = `${value}`;
} }
if (emitChange) { this.valueInputs.push(newInput);
this.eventEmitter.emit('change', { inputArea.appendChild(newInput);
value: Number(newInput[0].options[0].value), index += 1;
property: 'values[0]',
index: this.index
});
}
} }
};
Condition.prototype.generateSelectOptions = function () { if (emitChange) {
let telemetryMetadata = this.conditionManager.getTelemetryMetadata(this.config.object); this.eventEmitter.emit('change', {
let fragment = document.createDocumentFragment(); value: Number(newInput[0].options[0].value),
property: 'values[0]',
index: this.index
});
}
}
};
telemetryMetadata[this.config.key].enumerations.forEach((enumeration) => { Condition.prototype.generateSelectOptions = function () {
const option = document.createElement('option'); let telemetryMetadata = this.conditionManager.getTelemetryMetadata(this.config.object);
option.value = enumeration.value; let fragment = document.createDocumentFragment();
option.textContent = enumeration.string;
fragment.appendChild(option);
});
return fragment; telemetryMetadata[this.config.key].enumerations.forEach((enumeration) => {
}; const option = document.createElement('option');
option.value = enumeration.value;
option.textContent = enumeration.string;
fragment.appendChild(option);
});
return Condition; return fragment;
}); };

View File

@ -1,486 +1,482 @@
define([], function () { /**
* Responsible for maintaining the possible operations for conditions
* in this widget, and evaluating the boolean value of conditions passed as
* input.
* @constructor
* @param {Object} subscriptionCache A cache consisting of the latest available
* data for any telemetry sources in the widget's
* composition.
* @param {Object} compositionObjs The current set of composition objects to
* evaluate for 'any' and 'all' conditions
*/
export default function ConditionEvaluator(subscriptionCache, compositionObjs) {
this.subscriptionCache = subscriptionCache;
this.compositionObjs = compositionObjs;
this.testCache = {};
this.useTestCache = false;
/** /**
* Responsible for maintaining the possible operations for conditions * Maps value types to HTML input field types. These
* in this widget, and evaluating the boolean value of conditions passed as * type of inputs will be generated by conditions expecting this data type
* input.
* @constructor
* @param {Object} subscriptionCache A cache consisting of the latest available
* data for any telemetry sources in the widget's
* composition.
* @param {Object} compositionObjs The current set of composition objects to
* evaluate for 'any' and 'all' conditions
*/ */
function ConditionEvaluator(subscriptionCache, compositionObjs) { this.inputTypes = {
this.subscriptionCache = subscriptionCache; number: 'number',
this.compositionObjs = compositionObjs; string: 'text',
enum: 'select'
};
this.testCache = {}; /**
this.useTestCache = false; * Functions to validate that the input to an operation is of the type
* that it expects, in order to prevent unexpected behavior. Will be
* invoked before the corresponding operation is executed
*/
this.inputValidators = {
number: this.validateNumberInput,
string: this.validateStringInput,
enum: this.validateNumberInput
};
/** /**
* Maps value types to HTML input field types. These * A library of operations supported by this rule evaluator. Each operation
* type of inputs will be generated by conditions expecting this data type * consists of the following fields:
*/ * operation: a function with boolean return type to be invoked when this
this.inputTypes = { * operation is used. Will be called with an array of inputs
number: 'number', * where input [0] is the telemetry value and input [1..n] are
string: 'text', * any comparison values
enum: 'select' * text: a human-readable description of this operation to populate selects
}; * appliesTo: an array of identifiers for types that operation may be used on
* inputCount: the number of inputs required to get any necessary comparison
/** * values for the operation
* Functions to validate that the input to an operation is of the type * getDescription: A function returning a human-readable shorthand description of
* that it expects, in order to prevent unexpected behavior. Will be * this operation to populate the 'description' field in the rule header.
* invoked before the corresponding operation is executed * Will be invoked with an array of a condition's comparison values.
*/ */
this.inputValidators = { this.operations = {
number: this.validateNumberInput, equalTo: {
string: this.validateStringInput, operation: function (input) {
enum: this.validateNumberInput return input[0] === input[1];
};
/**
* A library of operations supported by this rule evaluator. Each operation
* consists of the following fields:
* operation: a function with boolean return type to be invoked when this
* operation is used. Will be called with an array of inputs
* where input [0] is the telemetry value and input [1..n] are
* any comparison values
* text: a human-readable description of this operation to populate selects
* appliesTo: an array of identifiers for types that operation may be used on
* inputCount: the number of inputs required to get any necessary comparison
* values for the operation
* getDescription: A function returning a human-readable shorthand description of
* this operation to populate the 'description' field in the rule header.
* Will be invoked with an array of a condition's comparison values.
*/
this.operations = {
equalTo: {
operation: function (input) {
return input[0] === input[1];
},
text: 'is equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' == ' + values[0];
}
}, },
notEqualTo: { text: 'is equal to',
operation: function (input) { appliesTo: ['number'],
return input[0] !== input[1]; inputCount: 1,
}, getDescription: function (values) {
text: 'is not equal to', return ' == ' + values[0];
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' != ' + values[0];
}
},
greaterThan: {
operation: function (input) {
return input[0] > input[1];
},
text: 'is greater than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' > ' + values[0];
}
},
lessThan: {
operation: function (input) {
return input[0] < input[1];
},
text: 'is less than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' < ' + values[0];
}
},
greaterThanOrEq: {
operation: function (input) {
return input[0] >= input[1];
},
text: 'is greater than or equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' >= ' + values[0];
}
},
lessThanOrEq: {
operation: function (input) {
return input[0] <= input[1];
},
text: 'is less than or equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' <= ' + values[0];
}
},
between: {
operation: function (input) {
return input[0] > input[1] && input[0] < input[2];
},
text: 'is between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' between ' + values[0] + ' and ' + values[1];
}
},
notBetween: {
operation: function (input) {
return input[0] < input[1] || input[0] > input[2];
},
text: 'is not between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' not between ' + values[0] + ' and ' + values[1];
}
},
textContains: {
operation: function (input) {
return input[0] && input[1] && input[0].includes(input[1]);
},
text: 'text contains',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' contains ' + values[0];
}
},
textDoesNotContain: {
operation: function (input) {
return input[0] && input[1] && !input[0].includes(input[1]);
},
text: 'text does not contain',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' does not contain ' + values[0];
}
},
textStartsWith: {
operation: function (input) {
return input[0].startsWith(input[1]);
},
text: 'text starts with',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' starts with ' + values[0];
}
},
textEndsWith: {
operation: function (input) {
return input[0].endsWith(input[1]);
},
text: 'text ends with',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' ends with ' + values[0];
}
},
textIsExactly: {
operation: function (input) {
return input[0] === input[1];
},
text: 'text is exactly',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' is exactly ' + values[0];
}
},
isUndefined: {
operation: function (input) {
return typeof input[0] === 'undefined';
},
text: 'is undefined',
appliesTo: ['string', 'number', 'enum'],
inputCount: 0,
getDescription: function () {
return ' is undefined';
}
},
isDefined: {
operation: function (input) {
return typeof input[0] !== 'undefined';
},
text: 'is defined',
appliesTo: ['string', 'number', 'enum'],
inputCount: 0,
getDescription: function () {
return ' is defined';
}
},
enumValueIs: {
operation: function (input) {
return input[0] === input[1];
},
text: 'is',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' == ' + values[0];
}
},
enumValueIsNot: {
operation: function (input) {
return input[0] !== input[1];
},
text: 'is not',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' != ' + values[0];
}
} }
}; },
} notEqualTo: {
operation: function (input) {
return input[0] !== input[1];
},
text: 'is not equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' != ' + values[0];
}
},
greaterThan: {
operation: function (input) {
return input[0] > input[1];
},
text: 'is greater than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' > ' + values[0];
}
},
lessThan: {
operation: function (input) {
return input[0] < input[1];
},
text: 'is less than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' < ' + values[0];
}
},
greaterThanOrEq: {
operation: function (input) {
return input[0] >= input[1];
},
text: 'is greater than or equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' >= ' + values[0];
}
},
lessThanOrEq: {
operation: function (input) {
return input[0] <= input[1];
},
text: 'is less than or equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' <= ' + values[0];
}
},
between: {
operation: function (input) {
return input[0] > input[1] && input[0] < input[2];
},
text: 'is between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' between ' + values[0] + ' and ' + values[1];
}
},
notBetween: {
operation: function (input) {
return input[0] < input[1] || input[0] > input[2];
},
text: 'is not between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' not between ' + values[0] + ' and ' + values[1];
}
},
textContains: {
operation: function (input) {
return input[0] && input[1] && input[0].includes(input[1]);
},
text: 'text contains',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' contains ' + values[0];
}
},
textDoesNotContain: {
operation: function (input) {
return input[0] && input[1] && !input[0].includes(input[1]);
},
text: 'text does not contain',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' does not contain ' + values[0];
}
},
textStartsWith: {
operation: function (input) {
return input[0].startsWith(input[1]);
},
text: 'text starts with',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' starts with ' + values[0];
}
},
textEndsWith: {
operation: function (input) {
return input[0].endsWith(input[1]);
},
text: 'text ends with',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' ends with ' + values[0];
}
},
textIsExactly: {
operation: function (input) {
return input[0] === input[1];
},
text: 'text is exactly',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' is exactly ' + values[0];
}
},
isUndefined: {
operation: function (input) {
return typeof input[0] === 'undefined';
},
text: 'is undefined',
appliesTo: ['string', 'number', 'enum'],
inputCount: 0,
getDescription: function () {
return ' is undefined';
}
},
isDefined: {
operation: function (input) {
return typeof input[0] !== 'undefined';
},
text: 'is defined',
appliesTo: ['string', 'number', 'enum'],
inputCount: 0,
getDescription: function () {
return ' is defined';
}
},
enumValueIs: {
operation: function (input) {
return input[0] === input[1];
},
text: 'is',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' == ' + values[0];
}
},
enumValueIsNot: {
operation: function (input) {
return input[0] !== input[1];
},
text: 'is not',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' != ' + values[0];
}
}
};
}
/** /**
* Evaluate the conditions passed in as an argument, and return the boolean * Evaluate the conditions passed in as an argument, and return the boolean
* value of these conditions. Available evaluation modes are 'any', which will * value of these conditions. Available evaluation modes are 'any', which will
* return true if any of the conditions evaluates to true (i.e. logical OR); 'all', * return true if any of the conditions evaluates to true (i.e. logical OR); 'all',
* which returns true only if all conditions evalute to true (i.e. logical AND); * which returns true only if all conditions evalute to true (i.e. logical AND);
* or 'js', which returns the boolean value of a custom JavaScript conditional. * or 'js', which returns the boolean value of a custom JavaScript conditional.
* @param {} conditions Either an array of objects with object, key, operation, * @param {} conditions Either an array of objects with object, key, operation,
* and value fields, or a string representing a JavaScript * and value fields, or a string representing a JavaScript
* condition. * condition.
* @param {string} mode The key of the mode to use when evaluating the conditions. * @param {string} mode The key of the mode to use when evaluating the conditions.
* @return {boolean} The boolean value of the conditions * @return {boolean} The boolean value of the conditions
*/ */
ConditionEvaluator.prototype.execute = function (conditions, mode) { ConditionEvaluator.prototype.execute = function (conditions, mode) {
let active = false; let active = false;
let conditionValue; let conditionValue;
let conditionDefined = false; let conditionDefined = false;
const self = this; const self = this;
let firstRuleEvaluated = false; let firstRuleEvaluated = false;
const compositionObjs = this.compositionObjs; const compositionObjs = this.compositionObjs;
if (mode === 'js') { if (mode === 'js') {
active = this.executeJavaScriptCondition(conditions); active = this.executeJavaScriptCondition(conditions);
} else { } else {
(conditions || []).forEach(function (condition) { (conditions || []).forEach(function (condition) {
conditionDefined = false; conditionDefined = false;
if (condition.object === 'any') { if (condition.object === 'any') {
conditionValue = false; conditionValue = false;
Object.keys(compositionObjs).forEach(function (objId) { Object.keys(compositionObjs).forEach(function (objId) {
try {
conditionValue =
conditionValue ||
self.executeCondition(objId, condition.key, condition.operation, condition.values);
conditionDefined = true;
} catch (e) {
//ignore a malformed condition
}
});
} else if (condition.object === 'all') {
conditionValue = true;
Object.keys(compositionObjs).forEach(function (objId) {
try {
conditionValue =
conditionValue &&
self.executeCondition(objId, condition.key, condition.operation, condition.values);
conditionDefined = true;
} catch (e) {
//ignore a malformed condition
}
});
} else {
try { try {
conditionValue = self.executeCondition( conditionValue =
condition.object, conditionValue ||
condition.key, self.executeCondition(objId, condition.key, condition.operation, condition.values);
condition.operation,
condition.values
);
conditionDefined = true; conditionDefined = true;
} catch (e) { } catch (e) {
//ignore malformed condition //ignore a malformed condition
} }
} });
} else if (condition.object === 'all') {
if (conditionDefined) { conditionValue = true;
active = mode === 'all' && !firstRuleEvaluated ? true : active; Object.keys(compositionObjs).forEach(function (objId) {
firstRuleEvaluated = true; try {
if (mode === 'any') { conditionValue =
active = active || conditionValue; conditionValue &&
} else if (mode === 'all') { self.executeCondition(objId, condition.key, condition.operation, condition.values);
active = active && conditionValue; conditionDefined = true;
} catch (e) {
//ignore a malformed condition
} }
} });
});
}
return active;
};
/**
* Execute a condition defined as an object.
* @param {string} object The identifier of the telemetry object to retrieve data from
* @param {string} key The property of the telemetry object
* @param {string} operation The key of the operation in this ConditionEvaluator to executeCondition
* @param {string} values An array of comparison values to invoke the operation with
* @return {boolean} The value of this condition
*/
ConditionEvaluator.prototype.executeCondition = function (object, key, operation, values) {
const cache = this.useTestCache ? this.testCache : this.subscriptionCache;
let telemetryValue;
let op;
let input;
let validator;
if (cache[object] && typeof cache[object][key] !== 'undefined') {
let value = cache[object][key];
telemetryValue = [isNaN(Number(value)) ? value : Number(value)];
}
op = this.operations[operation] && this.operations[operation].operation;
input = telemetryValue && telemetryValue.concat(values);
validator = op && this.inputValidators[this.operations[operation].appliesTo[0]];
if (op && input && validator) {
if (this.operations[operation].appliesTo.length > 1) {
return (this.validateNumberInput(input) || this.validateStringInput(input)) && op(input);
} else { } else {
return validator(input) && op(input); try {
conditionValue = self.executeCondition(
condition.object,
condition.key,
condition.operation,
condition.values
);
conditionDefined = true;
} catch (e) {
//ignore malformed condition
}
} }
if (conditionDefined) {
active = mode === 'all' && !firstRuleEvaluated ? true : active;
firstRuleEvaluated = true;
if (mode === 'any') {
active = active || conditionValue;
} else if (mode === 'all') {
active = active && conditionValue;
}
}
});
}
return active;
};
/**
* Execute a condition defined as an object.
* @param {string} object The identifier of the telemetry object to retrieve data from
* @param {string} key The property of the telemetry object
* @param {string} operation The key of the operation in this ConditionEvaluator to executeCondition
* @param {string} values An array of comparison values to invoke the operation with
* @return {boolean} The value of this condition
*/
ConditionEvaluator.prototype.executeCondition = function (object, key, operation, values) {
const cache = this.useTestCache ? this.testCache : this.subscriptionCache;
let telemetryValue;
let op;
let input;
let validator;
if (cache[object] && typeof cache[object][key] !== 'undefined') {
let value = cache[object][key];
telemetryValue = [isNaN(Number(value)) ? value : Number(value)];
}
op = this.operations[operation] && this.operations[operation].operation;
input = telemetryValue && telemetryValue.concat(values);
validator = op && this.inputValidators[this.operations[operation].appliesTo[0]];
if (op && input && validator) {
if (this.operations[operation].appliesTo.length > 1) {
return (this.validateNumberInput(input) || this.validateStringInput(input)) && op(input);
} else { } else {
throw new Error('Malformed condition'); return validator(input) && op(input);
} }
}; } else {
throw new Error('Malformed condition');
}
};
/** /**
* A function that returns true only if each value in its input argument is * A function that returns true only if each value in its input argument is
* of a numerical type * of a numerical type
* @param {[]} input An array of values * @param {[]} input An array of values
* @returns {boolean} * @returns {boolean}
*/ */
ConditionEvaluator.prototype.validateNumberInput = function (input) { ConditionEvaluator.prototype.validateNumberInput = function (input) {
let valid = true; let valid = true;
input.forEach(function (value) { input.forEach(function (value) {
valid = valid && typeof value === 'number'; valid = valid && typeof value === 'number';
}); });
return valid; return valid;
}; };
/** /**
* A function that returns true only if each value in its input argument is * A function that returns true only if each value in its input argument is
* a string * a string
* @param {[]} input An array of values * @param {[]} input An array of values
* @returns {boolean} * @returns {boolean}
*/ */
ConditionEvaluator.prototype.validateStringInput = function (input) { ConditionEvaluator.prototype.validateStringInput = function (input) {
let valid = true; let valid = true;
input.forEach(function (value) { input.forEach(function (value) {
valid = valid && typeof value === 'string'; valid = valid && typeof value === 'string';
}); });
return valid; return valid;
}; };
/** /**
* Get the keys of operations supported by this evaluator * Get the keys of operations supported by this evaluator
* @return {string[]} An array of the keys of supported operations * @return {string[]} An array of the keys of supported operations
*/ */
ConditionEvaluator.prototype.getOperationKeys = function () { ConditionEvaluator.prototype.getOperationKeys = function () {
return Object.keys(this.operations); return Object.keys(this.operations);
}; };
/** /**
* Get the human-readable text corresponding to a given operation * Get the human-readable text corresponding to a given operation
* @param {string} key The key of the operation * @param {string} key The key of the operation
* @return {string} The text description of the operation * @return {string} The text description of the operation
*/ */
ConditionEvaluator.prototype.getOperationText = function (key) { ConditionEvaluator.prototype.getOperationText = function (key) {
return this.operations[key].text; return this.operations[key].text;
}; };
/** /**
* Returns true only if the given operation applies to a given type * Returns true only if the given operation applies to a given type
* @param {string} key The key of the operation * @param {string} key The key of the operation
* @param {string} type The value type to query * @param {string} type The value type to query
* @returns {boolean} True if the condition applies, false otherwise * @returns {boolean} True if the condition applies, false otherwise
*/ */
ConditionEvaluator.prototype.operationAppliesTo = function (key, type) { ConditionEvaluator.prototype.operationAppliesTo = function (key, type) {
return this.operations[key].appliesTo.includes(type); return this.operations[key].appliesTo.includes(type);
}; };
/** /**
* Return the number of value inputs required by an operation * Return the number of value inputs required by an operation
* @param {string} key The key of the operation to query * @param {string} key The key of the operation to query
* @return {number} * @return {number}
*/ */
ConditionEvaluator.prototype.getInputCount = function (key) { ConditionEvaluator.prototype.getInputCount = function (key) {
if (this.operations[key]) { if (this.operations[key]) {
return this.operations[key].inputCount; return this.operations[key].inputCount;
} }
}; };
/** /**
* Return the human-readable shorthand description of the operation for a rule header * Return the human-readable shorthand description of the operation for a rule header
* @param {string} key The key of the operation to query * @param {string} key The key of the operation to query
* @param {} values An array of values with which to invoke the getDescription function * @param {} values An array of values with which to invoke the getDescription function
* of the operation * of the operation
* @return {string} A text description of this operation * @return {string} A text description of this operation
*/ */
ConditionEvaluator.prototype.getOperationDescription = function (key, values) { ConditionEvaluator.prototype.getOperationDescription = function (key, values) {
if (this.operations[key]) { if (this.operations[key]) {
return this.operations[key].getDescription(values); return this.operations[key].getDescription(values);
} }
}; };
/** /**
* Return the HTML input type associated with a given operation * Return the HTML input type associated with a given operation
* @param {string} key The key of the operation to query * @param {string} key The key of the operation to query
* @return {string} The key for an HTML5 input type * @return {string} The key for an HTML5 input type
*/ */
ConditionEvaluator.prototype.getInputType = function (key) { ConditionEvaluator.prototype.getInputType = function (key) {
let type; let type;
if (this.operations[key]) { if (this.operations[key]) {
type = this.operations[key].appliesTo[0]; type = this.operations[key].appliesTo[0];
} }
if (this.inputTypes[type]) { if (this.inputTypes[type]) {
return this.inputTypes[type]; return this.inputTypes[type];
} }
}; };
/** /**
* Returns the HTML input type associated with a value type * Returns the HTML input type associated with a value type
* @param {string} dataType The JavaScript value type * @param {string} dataType The JavaScript value type
* @return {string} The key for an HTML5 input type * @return {string} The key for an HTML5 input type
*/ */
ConditionEvaluator.prototype.getInputTypeById = function (dataType) { ConditionEvaluator.prototype.getInputTypeById = function (dataType) {
return this.inputTypes[dataType]; return this.inputTypes[dataType];
}; };
/** /**
* Set the test data cache used by this rule evaluator * Set the test data cache used by this rule evaluator
* @param {object} testCache A mock cache following the format of the real * @param {object} testCache A mock cache following the format of the real
* subscription cache * subscription cache
*/ */
ConditionEvaluator.prototype.setTestDataCache = function (testCache) { ConditionEvaluator.prototype.setTestDataCache = function (testCache) {
this.testCache = testCache; this.testCache = testCache;
}; };
/** /**
* Have this RuleEvaluator pull data values from the provided test cache * Have this RuleEvaluator pull data values from the provided test cache
* instead of its actual subscription cache when evaluating. If invoked with true, * instead of its actual subscription cache when evaluating. If invoked with true,
* will use the test cache; otherwise, will use the subscription cache * will use the test cache; otherwise, will use the subscription cache
* @param {boolean} useTestData Boolean flag * @param {boolean} useTestData Boolean flag
*/ */
ConditionEvaluator.prototype.useTestData = function (useTestCache) { ConditionEvaluator.prototype.useTestData = function (useTestCache) {
this.useTestCache = useTestCache; this.useTestCache = useTestCache;
}; };
return ConditionEvaluator;
});

View File

@ -1,386 +1,383 @@
define(['./ConditionEvaluator', 'objectUtils', 'EventEmitter', 'lodash'], function ( import EventEmitter from 'EventEmitter';
ConditionEvaluator, import _ from 'lodash';
objectUtils, import objectUtils from 'objectUtils';
EventEmitter,
_
) {
/**
* Provides a centralized content manager for conditions in the summary widget.
* Loads and caches composition and telemetry subscriptions, and maintains a
* {ConditionEvaluator} instance to handle evaluation
* @constructor
* @param {Object} domainObject the Summary Widget domain object
* @param {MCT} openmct an MCT instance
*/
function ConditionManager(domainObject, openmct) {
this.domainObject = domainObject;
this.openmct = openmct;
this.composition = this.openmct.composition.get(this.domainObject); import ConditionEvaluator from './ConditionEvaluator';
this.compositionObjs = {};
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['add', 'remove', 'load', 'metadata', 'receiveTelemetry'];
this.keywordLabels = { /**
any: 'any Telemetry', * Provides a centralized content manager for conditions in the summary widget.
all: 'all Telemetry' * Loads and caches composition and telemetry subscriptions, and maintains a
}; * {ConditionEvaluator} instance to handle evaluation
* @constructor
* @param {Object} domainObject the Summary Widget domain object
* @param {MCT} openmct an MCT instance
*/
export default function ConditionManager(domainObject, openmct) {
this.domainObject = domainObject;
this.openmct = openmct;
this.telemetryMetadataById = { this.composition = this.openmct.composition.get(this.domainObject);
any: {}, this.compositionObjs = {};
all: {} this.eventEmitter = new EventEmitter();
}; this.supportedCallbacks = ['add', 'remove', 'load', 'metadata', 'receiveTelemetry'];
this.telemetryTypesById = { this.keywordLabels = {
any: {}, any: 'any Telemetry',
all: {} all: 'all Telemetry'
};
this.subscriptions = {};
this.subscriptionCache = {};
this.loadComplete = false;
this.metadataLoadComplete = false;
this.evaluator = new ConditionEvaluator(this.subscriptionCache, this.compositionObjs);
this.composition.on('add', this.onCompositionAdd, this);
this.composition.on('remove', this.onCompositionRemove, this);
this.composition.on('load', this.onCompositionLoad, this);
this.composition.load();
}
/**
* Register a callback with this ConditionManager: supported callbacks are add
* remove, load, metadata, and receiveTelemetry
* @param {string} event The key for the event to listen to
* @param {function} callback The function that this rule will invoke on this event
* @param {Object} context A reference to a scope to use as the context for
* context for the callback function
*/
ConditionManager.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.on(event, callback, context || this);
} else {
throw (
event + ' is not a supported callback. Supported callbacks are ' + this.supportedCallbacks
);
}
}; };
/** this.telemetryMetadataById = {
* Given a set of rules, execute the conditions associated with each rule any: {},
* and return the id of the last rule whose conditions evaluate to true all: {}
* @param {string[]} ruleOrder An array of rule IDs indicating what order They };
* should be evaluated in
* @param {Object} rules An object mapping rule IDs to rule configurations
* @return {string} The ID of the rule to display on the widget
*/
ConditionManager.prototype.executeRules = function (ruleOrder, rules) {
const self = this;
let activeId = ruleOrder[0];
let rule;
let conditions;
ruleOrder.forEach(function (ruleId) { this.telemetryTypesById = {
rule = rules[ruleId]; any: {},
conditions = rule.getProperty('conditions'); all: {}
if (self.evaluator.execute(conditions, rule.getProperty('trigger'))) { };
activeId = ruleId;
} this.subscriptions = {};
this.subscriptionCache = {};
this.loadComplete = false;
this.metadataLoadComplete = false;
this.evaluator = new ConditionEvaluator(this.subscriptionCache, this.compositionObjs);
this.composition.on('add', this.onCompositionAdd, this);
this.composition.on('remove', this.onCompositionRemove, this);
this.composition.on('load', this.onCompositionLoad, this);
this.composition.load();
}
/**
* Register a callback with this ConditionManager: supported callbacks are add
* remove, load, metadata, and receiveTelemetry
* @param {string} event The key for the event to listen to
* @param {function} callback The function that this rule will invoke on this event
* @param {Object} context A reference to a scope to use as the context for
* context for the callback function
*/
ConditionManager.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.on(event, callback, context || this);
} else {
throw (
event + ' is not a supported callback. Supported callbacks are ' + this.supportedCallbacks
);
}
};
/**
* Given a set of rules, execute the conditions associated with each rule
* and return the id of the last rule whose conditions evaluate to true
* @param {string[]} ruleOrder An array of rule IDs indicating what order They
* should be evaluated in
* @param {Object} rules An object mapping rule IDs to rule configurations
* @return {string} The ID of the rule to display on the widget
*/
ConditionManager.prototype.executeRules = function (ruleOrder, rules) {
const self = this;
let activeId = ruleOrder[0];
let rule;
let conditions;
ruleOrder.forEach(function (ruleId) {
rule = rules[ruleId];
conditions = rule.getProperty('conditions');
if (self.evaluator.execute(conditions, rule.getProperty('trigger'))) {
activeId = ruleId;
}
});
return activeId;
};
/**
* Adds a field to the list of all available metadata fields in the widget
* @param {Object} metadatum An object representing a set of telemetry metadata
*/
ConditionManager.prototype.addGlobalMetadata = function (metadatum) {
this.telemetryMetadataById.any[metadatum.key] = metadatum;
this.telemetryMetadataById.all[metadatum.key] = metadatum;
};
/**
* Adds a field to the list of properties for globally available metadata
* @param {string} key The key for the property this type applies to
* @param {string} type The type that should be associated with this property
*/
ConditionManager.prototype.addGlobalPropertyType = function (key, type) {
this.telemetryTypesById.any[key] = type;
this.telemetryTypesById.all[key] = type;
};
/**
* Given a telemetry-producing domain object, associate each of it's telemetry
* fields with a type, parsing from historical data.
* @param {Object} object a domain object that can produce telemetry
* @return {Promise} A promise that resolves when a telemetry request
* has completed and types have been parsed
*/
ConditionManager.prototype.parsePropertyTypes = function (object) {
const objectId = objectUtils.makeKeyString(object.identifier);
this.telemetryTypesById[objectId] = {};
Object.values(this.telemetryMetadataById[objectId]).forEach(function (valueMetadata) {
let type;
if (valueMetadata.enumerations !== undefined) {
type = 'enum';
} else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {
type = 'number';
} else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) {
type = 'number';
} else if (valueMetadata.key === 'name') {
type = 'string';
} else {
type = 'string';
}
this.telemetryTypesById[objectId][valueMetadata.key] = type;
this.addGlobalPropertyType(valueMetadata.key, type);
}, this);
};
/**
* Parse types of telemetry fields from all composition objects; used internally
* to perform a block types load once initial composition load has completed
* @return {Promise} A promise that resolves when all metadata has been loaded
* and property types parsed
*/
ConditionManager.prototype.parseAllPropertyTypes = function () {
Object.values(this.compositionObjs).forEach(this.parsePropertyTypes, this);
this.metadataLoadComplete = true;
this.eventEmitter.emit('metadata');
};
/**
* Invoked when a telemetry subscription yields new data. Updates the LAD
* cache and invokes any registered receiveTelemetry callbacks
* @param {string} objId The key associated with the telemetry source
* @param {datum} datum The new data from the telemetry source
* @private
*/
ConditionManager.prototype.handleSubscriptionCallback = function (objId, telemetryDatum) {
this.subscriptionCache[objId] = this.createNormalizedDatum(objId, telemetryDatum);
this.eventEmitter.emit('receiveTelemetry');
};
ConditionManager.prototype.createNormalizedDatum = function (objId, telemetryDatum) {
return Object.values(this.telemetryMetadataById[objId]).reduce((normalizedDatum, metadatum) => {
normalizedDatum[metadatum.key] = telemetryDatum[metadatum.source];
return normalizedDatum;
}, {});
};
/**
* Event handler for an add event in this Summary Widget's composition.
* Sets up subscription handlers and parses its property types.
* @param {Object} obj The newly added domain object
* @private
*/
ConditionManager.prototype.onCompositionAdd = function (obj) {
let compositionKeys;
const telemetryAPI = this.openmct.telemetry;
const objId = objectUtils.makeKeyString(obj.identifier);
let telemetryMetadata;
const self = this;
if (telemetryAPI.isTelemetryObject(obj)) {
self.compositionObjs[objId] = obj;
self.telemetryMetadataById[objId] = {};
// FIXME: this should just update based on listener.
compositionKeys = self.domainObject.composition.map(objectUtils.makeKeyString);
if (!compositionKeys.includes(objId)) {
self.domainObject.composition.push(obj.identifier);
}
telemetryMetadata = telemetryAPI.getMetadata(obj).values();
telemetryMetadata.forEach(function (metaDatum) {
self.telemetryMetadataById[objId][metaDatum.key] = metaDatum;
self.addGlobalMetadata(metaDatum);
}); });
return activeId; self.subscriptionCache[objId] = {};
}; self.subscriptions[objId] = telemetryAPI.subscribe(
obj,
/** function (datum) {
* Adds a field to the list of all available metadata fields in the widget self.handleSubscriptionCallback(objId, datum);
* @param {Object} metadatum An object representing a set of telemetry metadata },
*/ {}
ConditionManager.prototype.addGlobalMetadata = function (metadatum) { );
this.telemetryMetadataById.any[metadatum.key] = metadatum; telemetryAPI
this.telemetryMetadataById.all[metadatum.key] = metadatum; .request(obj, {
}; strategy: 'latest',
size: 1
/** })
* Adds a field to the list of properties for globally available metadata .then(function (results) {
* @param {string} key The key for the property this type applies to if (results && results.length) {
* @param {string} type The type that should be associated with this property self.handleSubscriptionCallback(objId, results[results.length - 1]);
*/ }
ConditionManager.prototype.addGlobalPropertyType = function (key, type) {
this.telemetryTypesById.any[key] = type;
this.telemetryTypesById.all[key] = type;
};
/**
* Given a telemetry-producing domain object, associate each of it's telemetry
* fields with a type, parsing from historical data.
* @param {Object} object a domain object that can produce telemetry
* @return {Promise} A promise that resolves when a telemetry request
* has completed and types have been parsed
*/
ConditionManager.prototype.parsePropertyTypes = function (object) {
const objectId = objectUtils.makeKeyString(object.identifier);
this.telemetryTypesById[objectId] = {};
Object.values(this.telemetryMetadataById[objectId]).forEach(function (valueMetadata) {
let type;
if (valueMetadata.enumerations !== undefined) {
type = 'enum';
} else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {
type = 'number';
} else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) {
type = 'number';
} else if (valueMetadata.key === 'name') {
type = 'string';
} else {
type = 'string';
}
this.telemetryTypesById[objectId][valueMetadata.key] = type;
this.addGlobalPropertyType(valueMetadata.key, type);
}, this);
};
/**
* Parse types of telemetry fields from all composition objects; used internally
* to perform a block types load once initial composition load has completed
* @return {Promise} A promise that resolves when all metadata has been loaded
* and property types parsed
*/
ConditionManager.prototype.parseAllPropertyTypes = function () {
Object.values(this.compositionObjs).forEach(this.parsePropertyTypes, this);
this.metadataLoadComplete = true;
this.eventEmitter.emit('metadata');
};
/**
* Invoked when a telemetry subscription yields new data. Updates the LAD
* cache and invokes any registered receiveTelemetry callbacks
* @param {string} objId The key associated with the telemetry source
* @param {datum} datum The new data from the telemetry source
* @private
*/
ConditionManager.prototype.handleSubscriptionCallback = function (objId, telemetryDatum) {
this.subscriptionCache[objId] = this.createNormalizedDatum(objId, telemetryDatum);
this.eventEmitter.emit('receiveTelemetry');
};
ConditionManager.prototype.createNormalizedDatum = function (objId, telemetryDatum) {
return Object.values(this.telemetryMetadataById[objId]).reduce((normalizedDatum, metadatum) => {
normalizedDatum[metadatum.key] = telemetryDatum[metadatum.source];
return normalizedDatum;
}, {});
};
/**
* Event handler for an add event in this Summary Widget's composition.
* Sets up subscription handlers and parses its property types.
* @param {Object} obj The newly added domain object
* @private
*/
ConditionManager.prototype.onCompositionAdd = function (obj) {
let compositionKeys;
const telemetryAPI = this.openmct.telemetry;
const objId = objectUtils.makeKeyString(obj.identifier);
let telemetryMetadata;
const self = this;
if (telemetryAPI.isTelemetryObject(obj)) {
self.compositionObjs[objId] = obj;
self.telemetryMetadataById[objId] = {};
// FIXME: this should just update based on listener.
compositionKeys = self.domainObject.composition.map(objectUtils.makeKeyString);
if (!compositionKeys.includes(objId)) {
self.domainObject.composition.push(obj.identifier);
}
telemetryMetadata = telemetryAPI.getMetadata(obj).values();
telemetryMetadata.forEach(function (metaDatum) {
self.telemetryMetadataById[objId][metaDatum.key] = metaDatum;
self.addGlobalMetadata(metaDatum);
}); });
self.subscriptionCache[objId] = {}; /**
self.subscriptions[objId] = telemetryAPI.subscribe( * if this is the initial load, parsing property types will be postponed
obj, * until all composition objects have been loaded
function (datum) { */
self.handleSubscriptionCallback(objId, datum); if (self.loadComplete) {
}, self.parsePropertyTypes(obj);
{}
);
telemetryAPI
.request(obj, {
strategy: 'latest',
size: 1
})
.then(function (results) {
if (results && results.length) {
self.handleSubscriptionCallback(objId, results[results.length - 1]);
}
});
/**
* if this is the initial load, parsing property types will be postponed
* until all composition objects have been loaded
*/
if (self.loadComplete) {
self.parsePropertyTypes(obj);
}
self.eventEmitter.emit('add', obj);
const summaryWidget = document.querySelector('.w-summary-widget');
if (summaryWidget) {
summaryWidget.classList.remove('s-status-no-data');
}
}
};
/**
* Invoked on a remove event in this Summary Widget's composition. Removes
* the object from the local composition, and untracks it
* @param {object} identifier The identifier of the object to be removed
* @private
*/
ConditionManager.prototype.onCompositionRemove = function (identifier) {
const objectId = objectUtils.makeKeyString(identifier);
// FIXME: this should just update by listener.
_.remove(this.domainObject.composition, function (id) {
return id.key === identifier.key && id.namespace === identifier.namespace;
});
delete this.compositionObjs[objectId];
delete this.subscriptionCache[objectId];
this.subscriptions[objectId](); //unsubscribe from telemetry source
delete this.subscriptions[objectId];
this.eventEmitter.emit('remove', identifier);
if (_.isEmpty(this.compositionObjs)) {
const summaryWidget = document.querySelector('.w-summary-widget');
if (summaryWidget) {
summaryWidget.classList.add('s-status-no-data');
}
}
};
/**
* Invoked when the Summary Widget's composition finishes its initial load.
* Invokes any registered load callbacks, does a block load of all metadata,
* and then invokes any registered metadata load callbacks.
* @private
*/
ConditionManager.prototype.onCompositionLoad = function () {
this.loadComplete = true;
this.eventEmitter.emit('load');
this.parseAllPropertyTypes();
};
/**
* Returns the currently tracked telemetry sources
* @return {Object} An object mapping object keys to domain objects
*/
ConditionManager.prototype.getComposition = function () {
return this.compositionObjs;
};
/**
* Get the human-readable name of a domain object from its key
* @param {string} id The key of the domain object
* @return {string} The human-readable name of the domain object
*/
ConditionManager.prototype.getObjectName = function (id) {
let name;
if (this.keywordLabels[id]) {
name = this.keywordLabels[id];
} else if (this.compositionObjs[id]) {
name = this.compositionObjs[id].name;
} }
return name; self.eventEmitter.emit('add', obj);
};
/** const summaryWidget = document.querySelector('.w-summary-widget');
* Returns the property metadata associated with a given telemetry source if (summaryWidget) {
* @param {string} id The key associated with the domain object summaryWidget.classList.remove('s-status-no-data');
* @return {Object} Returns an object with fields representing each telemetry field
*/
ConditionManager.prototype.getTelemetryMetadata = function (id) {
return this.telemetryMetadataById[id];
};
/**
* Returns the type associated with a telemetry data field of a particular domain
* object
* @param {string} id The key associated with the domain object
* @param {string} property The telemetry field key to retrieve the type of
* @return {string} The type name
*/
ConditionManager.prototype.getTelemetryPropertyType = function (id, property) {
if (this.telemetryTypesById[id]) {
return this.telemetryTypesById[id][property];
} }
}; }
};
/** /**
* Returns the human-readable name of a telemetry data field of a particular domain * Invoked on a remove event in this Summary Widget's composition. Removes
* object * the object from the local composition, and untracks it
* @param {string} id The key associated with the domain object * @param {object} identifier The identifier of the object to be removed
* @param {string} property The telemetry field key to retrieve the type of * @private
* @return {string} The telemetry field name */
*/ ConditionManager.prototype.onCompositionRemove = function (identifier) {
ConditionManager.prototype.getTelemetryPropertyName = function (id, property) { const objectId = objectUtils.makeKeyString(identifier);
if (this.telemetryMetadataById[id] && this.telemetryMetadataById[id][property]) { // FIXME: this should just update by listener.
return this.telemetryMetadataById[id][property].name; _.remove(this.domainObject.composition, function (id) {
return id.key === identifier.key && id.namespace === identifier.namespace;
});
delete this.compositionObjs[objectId];
delete this.subscriptionCache[objectId];
this.subscriptions[objectId](); //unsubscribe from telemetry source
delete this.subscriptions[objectId];
this.eventEmitter.emit('remove', identifier);
if (_.isEmpty(this.compositionObjs)) {
const summaryWidget = document.querySelector('.w-summary-widget');
if (summaryWidget) {
summaryWidget.classList.add('s-status-no-data');
} }
}; }
};
/** /**
* Returns the {ConditionEvaluator} instance associated with this condition * Invoked when the Summary Widget's composition finishes its initial load.
* manager * Invokes any registered load callbacks, does a block load of all metadata,
* @return {ConditionEvaluator} * and then invokes any registered metadata load callbacks.
*/ * @private
ConditionManager.prototype.getEvaluator = function () { */
return this.evaluator; ConditionManager.prototype.onCompositionLoad = function () {
}; this.loadComplete = true;
this.eventEmitter.emit('load');
this.parseAllPropertyTypes();
};
/** /**
* Returns true if the initial composition load has completed * Returns the currently tracked telemetry sources
* @return {boolean} * @return {Object} An object mapping object keys to domain objects
*/ */
ConditionManager.prototype.loadCompleted = function () { ConditionManager.prototype.getComposition = function () {
return this.loadComplete; return this.compositionObjs;
}; };
/** /**
* Returns true if the initial block metadata load has completed * Get the human-readable name of a domain object from its key
*/ * @param {string} id The key of the domain object
ConditionManager.prototype.metadataLoadCompleted = function () { * @return {string} The human-readable name of the domain object
return this.metadataLoadComplete; */
}; ConditionManager.prototype.getObjectName = function (id) {
let name;
/** if (this.keywordLabels[id]) {
* Triggers the telemetryReceive callbacks registered to this ConditionManager, name = this.keywordLabels[id];
* used by the {TestDataManager} to force a rule evaluation when test data is } else if (this.compositionObjs[id]) {
* enabled name = this.compositionObjs[id].name;
*/ }
ConditionManager.prototype.triggerTelemetryCallback = function () {
this.eventEmitter.emit('receiveTelemetry');
};
/** return name;
* Unsubscribe from all registered telemetry sources and unregister all event };
* listeners registered with the Open MCT APIs
*/
ConditionManager.prototype.destroy = function () {
Object.values(this.subscriptions).forEach(function (unsubscribeFunction) {
unsubscribeFunction();
});
this.composition.off('add', this.onCompositionAdd, this);
this.composition.off('remove', this.onCompositionRemove, this);
this.composition.off('load', this.onCompositionLoad, this);
};
return ConditionManager; /**
}); * Returns the property metadata associated with a given telemetry source
* @param {string} id The key associated with the domain object
* @return {Object} Returns an object with fields representing each telemetry field
*/
ConditionManager.prototype.getTelemetryMetadata = function (id) {
return this.telemetryMetadataById[id];
};
/**
* Returns the type associated with a telemetry data field of a particular domain
* object
* @param {string} id The key associated with the domain object
* @param {string} property The telemetry field key to retrieve the type of
* @return {string} The type name
*/
ConditionManager.prototype.getTelemetryPropertyType = function (id, property) {
if (this.telemetryTypesById[id]) {
return this.telemetryTypesById[id][property];
}
};
/**
* Returns the human-readable name of a telemetry data field of a particular domain
* object
* @param {string} id The key associated with the domain object
* @param {string} property The telemetry field key to retrieve the type of
* @return {string} The telemetry field name
*/
ConditionManager.prototype.getTelemetryPropertyName = function (id, property) {
if (this.telemetryMetadataById[id] && this.telemetryMetadataById[id][property]) {
return this.telemetryMetadataById[id][property].name;
}
};
/**
* Returns the {ConditionEvaluator} instance associated with this condition
* manager
* @return {ConditionEvaluator}
*/
ConditionManager.prototype.getEvaluator = function () {
return this.evaluator;
};
/**
* Returns true if the initial composition load has completed
* @return {boolean}
*/
ConditionManager.prototype.loadCompleted = function () {
return this.loadComplete;
};
/**
* Returns true if the initial block metadata load has completed
*/
ConditionManager.prototype.metadataLoadCompleted = function () {
return this.metadataLoadComplete;
};
/**
* Triggers the telemetryReceive callbacks registered to this ConditionManager,
* used by the {TestDataManager} to force a rule evaluation when test data is
* enabled
*/
ConditionManager.prototype.triggerTelemetryCallback = function () {
this.eventEmitter.emit('receiveTelemetry');
};
/**
* Unsubscribe from all registered telemetry sources and unregister all event
* listeners registered with the Open MCT APIs
*/
ConditionManager.prototype.destroy = function () {
Object.values(this.subscriptions).forEach(function (unsubscribeFunction) {
unsubscribeFunction();
});
this.composition.off('add', this.onCompositionAdd, this);
this.composition.off('remove', this.onCompositionRemove, this);
this.composition.off('load', this.onCompositionLoad, this);
};

File diff suppressed because it is too large Load Diff

View File

@ -1,413 +1,393 @@
define([ import * as urlSanitizeLib from '@braintree/sanitize-url';
'../res/widgetTemplate.html',
'./Rule', import * as templateHelpers from '../../../utils/template/templateHelpers';
'./ConditionManager', import widgetTemplate from '../res/widgetTemplate.html';
'./TestDataManager', import ConditionManager from './ConditionManager';
'./WidgetDnD', import eventHelpers from './eventHelpers';
'./eventHelpers', import Rule from './Rule';
'../../../utils/template/templateHelpers', import TestDataManager from './TestDataManager';
'objectUtils', import WidgetDnD from './WidgetDnD';
'lodash',
'@braintree/sanitize-url' //default css configuration for new rules
], function ( const DEFAULT_PROPS = {
widgetTemplate, color: '#cccccc',
Rule, 'background-color': '#666666',
ConditionManager, 'border-color': 'rgba(0,0,0,0)'
TestDataManager, };
WidgetDnD,
eventHelpers, /**
templateHelpers, * A Summary Widget object, which allows a user to configure rules based
objectUtils, * on telemetry producing domain objects, and update a compact display
_, * accordingly.
urlSanitizeLib * @constructor
) { * @param {Object} domainObject The domain Object represented by this Widget
//default css configuration for new rules * @param {MCT} openmct An MCT instance
const DEFAULT_PROPS = { */
color: '#cccccc', export default function SummaryWidget(domainObject, openmct) {
'background-color': '#666666', eventHelpers.extend(this);
'border-color': 'rgba(0,0,0,0)'
}; this.domainObject = domainObject;
this.openmct = openmct;
this.domainObject.configuration = this.domainObject.configuration || {};
this.domainObject.configuration.ruleConfigById =
this.domainObject.configuration.ruleConfigById || {};
this.domainObject.configuration.ruleOrder = this.domainObject.configuration.ruleOrder || [
'default'
];
this.domainObject.configuration.testDataConfig = this.domainObject.configuration
.testDataConfig || [
{
object: '',
key: '',
value: ''
}
];
this.activeId = 'default';
this.rulesById = {};
this.domElement = templateHelpers.convertTemplateToHTML(widgetTemplate)[0];
this.toggleRulesControl = this.domElement.querySelector('.t-view-control-rules');
this.toggleTestDataControl = this.domElement.querySelector('.t-view-control-test-data');
this.widgetButton = this.domElement.querySelector(':scope > #widget');
this.editing = false;
this.container = '';
this.editListenerUnsubscribe = () => {};
this.outerWrapper = this.domElement.querySelector('.widget-edit-holder');
this.ruleArea = this.domElement.querySelector('#ruleArea');
this.configAreaRules = this.domElement.querySelector('.widget-rules-wrapper');
this.testDataArea = this.domElement.querySelector('.widget-test-data');
this.addRuleButton = this.domElement.querySelector('#addRule');
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
this.testDataManager = new TestDataManager(
this.domainObject,
this.conditionManager,
this.openmct
);
this.watchForChanges = this.watchForChanges.bind(this);
this.show = this.show.bind(this);
this.destroy = this.destroy.bind(this);
this.addRule = this.addRule.bind(this);
this.addHyperlink(domainObject.url, domainObject.openNewTab);
this.watchForChanges(openmct, domainObject);
const self = this;
/** /**
* A Summary Widget object, which allows a user to configure rules based * Toggles the configuration area for test data in the view
* on telemetry producing domain objects, and update a compact display * @private
* accordingly.
* @constructor
* @param {Object} domainObject The domain Object represented by this Widget
* @param {MCT} openmct An MCT instance
*/ */
function SummaryWidget(domainObject, openmct) { function toggleTestData() {
eventHelpers.extend(this); if (self.outerWrapper.classList.contains('expanded-widget-test-data')) {
self.outerWrapper.classList.remove('expanded-widget-test-data');
this.domainObject = domainObject; } else {
this.openmct = openmct; self.outerWrapper.classList.add('expanded-widget-test-data');
this.domainObject.configuration = this.domainObject.configuration || {};
this.domainObject.configuration.ruleConfigById =
this.domainObject.configuration.ruleConfigById || {};
this.domainObject.configuration.ruleOrder = this.domainObject.configuration.ruleOrder || [
'default'
];
this.domainObject.configuration.testDataConfig = this.domainObject.configuration
.testDataConfig || [
{
object: '',
key: '',
value: ''
}
];
this.activeId = 'default';
this.rulesById = {};
this.domElement = templateHelpers.convertTemplateToHTML(widgetTemplate)[0];
this.toggleRulesControl = this.domElement.querySelector('.t-view-control-rules');
this.toggleTestDataControl = this.domElement.querySelector('.t-view-control-test-data');
this.widgetButton = this.domElement.querySelector(':scope > #widget');
this.editing = false;
this.container = '';
this.editListenerUnsubscribe = () => {};
this.outerWrapper = this.domElement.querySelector('.widget-edit-holder');
this.ruleArea = this.domElement.querySelector('#ruleArea');
this.configAreaRules = this.domElement.querySelector('.widget-rules-wrapper');
this.testDataArea = this.domElement.querySelector('.widget-test-data');
this.addRuleButton = this.domElement.querySelector('#addRule');
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
this.testDataManager = new TestDataManager(
this.domainObject,
this.conditionManager,
this.openmct
);
this.watchForChanges = this.watchForChanges.bind(this);
this.show = this.show.bind(this);
this.destroy = this.destroy.bind(this);
this.addRule = this.addRule.bind(this);
this.addHyperlink(domainObject.url, domainObject.openNewTab);
this.watchForChanges(openmct, domainObject);
const self = this;
/**
* Toggles the configuration area for test data in the view
* @private
*/
function toggleTestData() {
if (self.outerWrapper.classList.contains('expanded-widget-test-data')) {
self.outerWrapper.classList.remove('expanded-widget-test-data');
} else {
self.outerWrapper.classList.add('expanded-widget-test-data');
}
if (self.toggleTestDataControl.classList.contains('c-disclosure-triangle--expanded')) {
self.toggleTestDataControl.classList.remove('c-disclosure-triangle--expanded');
} else {
self.toggleTestDataControl.classList.add('c-disclosure-triangle--expanded');
}
} }
this.listenTo(this.toggleTestDataControl, 'click', toggleTestData); if (self.toggleTestDataControl.classList.contains('c-disclosure-triangle--expanded')) {
self.toggleTestDataControl.classList.remove('c-disclosure-triangle--expanded');
/** } else {
* Toggles the configuration area for rules in the view self.toggleTestDataControl.classList.add('c-disclosure-triangle--expanded');
* @private
*/
function toggleRules() {
templateHelpers.toggleClass(self.outerWrapper, 'expanded-widget-rules');
templateHelpers.toggleClass(self.toggleRulesControl, 'c-disclosure-triangle--expanded');
} }
this.listenTo(this.toggleRulesControl, 'click', toggleRules);
} }
/** this.listenTo(this.toggleTestDataControl, 'click', toggleTestData);
* adds or removes href to widget button and adds or removes openInNewTab
* @param {string} url String that denotes the url to be opened
* @param {string} openNewTab String that denotes wether to open link in new tab or not
*/
SummaryWidget.prototype.addHyperlink = function (url, openNewTab) {
if (url) {
this.widgetButton.href = urlSanitizeLib.sanitizeUrl(url);
} else {
this.widgetButton.removeAttribute('href');
}
if (openNewTab === 'newTab') {
this.widgetButton.target = '_blank';
} else {
this.widgetButton.removeAttribute('target');
}
};
/** /**
* adds a listener to the object to watch for any changes made by user * Toggles the configuration area for rules in the view
* only executes if changes are observed * @private
* @param {openmct} Object Instance of OpenMCT
* @param {domainObject} Object instance of this object
*/ */
SummaryWidget.prototype.watchForChanges = function (openmct, domainObject) { function toggleRules() {
this.watchForChangesUnsubscribe = openmct.objects.observe( templateHelpers.toggleClass(self.outerWrapper, 'expanded-widget-rules');
domainObject, templateHelpers.toggleClass(self.toggleRulesControl, 'c-disclosure-triangle--expanded');
'*', }
function (newDomainObject) {
if ( this.listenTo(this.toggleRulesControl, 'click', toggleRules);
newDomainObject.url !== this.domainObject.url || }
newDomainObject.openNewTab !== this.domainObject.openNewTab
) { /**
this.addHyperlink(newDomainObject.url, newDomainObject.openNewTab); * adds or removes href to widget button and adds or removes openInNewTab
* @param {string} url String that denotes the url to be opened
* @param {string} openNewTab String that denotes wether to open link in new tab or not
*/
SummaryWidget.prototype.addHyperlink = function (url, openNewTab) {
if (url) {
this.widgetButton.href = urlSanitizeLib.sanitizeUrl(url);
} else {
this.widgetButton.removeAttribute('href');
}
if (openNewTab === 'newTab') {
this.widgetButton.target = '_blank';
} else {
this.widgetButton.removeAttribute('target');
}
};
/**
* adds a listener to the object to watch for any changes made by user
* only executes if changes are observed
* @param {openmct} Object Instance of OpenMCT
* @param {domainObject} Object instance of this object
*/
SummaryWidget.prototype.watchForChanges = function (openmct, domainObject) {
this.watchForChangesUnsubscribe = openmct.objects.observe(
domainObject,
'*',
function (newDomainObject) {
if (
newDomainObject.url !== this.domainObject.url ||
newDomainObject.openNewTab !== this.domainObject.openNewTab
) {
this.addHyperlink(newDomainObject.url, newDomainObject.openNewTab);
}
}.bind(this)
);
};
/**
* Builds the Summary Widget's DOM, performs other necessary setup, and attaches
* this Summary Widget's view to the supplied container.
* @param {element} container The DOM element that will contain this Summary
* Widget's view.
*/
SummaryWidget.prototype.show = function (container) {
const self = this;
this.container = container;
this.container.append(this.domElement);
this.domElement.querySelector('.widget-test-data').append(this.testDataManager.getDOM());
this.widgetDnD = new WidgetDnD(
this.domElement,
this.domainObject.configuration.ruleOrder,
this.rulesById
);
this.initRule('default', 'Default');
this.domainObject.configuration.ruleOrder.forEach(function (ruleId) {
if (ruleId !== 'default') {
self.initRule(ruleId);
}
});
this.refreshRules();
this.updateWidget();
this.listenTo(this.addRuleButton, 'click', this.addRule);
this.conditionManager.on('receiveTelemetry', this.executeRules, this);
this.widgetDnD.on('drop', this.reorder, this);
};
/**
* Unregister event listeners with the Open MCT APIs, unsubscribe from telemetry,
* and clean up event handlers
*/
SummaryWidget.prototype.destroy = function (container) {
this.editListenerUnsubscribe();
this.conditionManager.destroy();
this.testDataManager.destroy();
this.widgetDnD.destroy();
this.watchForChangesUnsubscribe();
Object.values(this.rulesById).forEach(function (rule) {
rule.destroy();
});
this.stopListening();
};
/**
* Update the view from the current rule configuration and order
*/
SummaryWidget.prototype.refreshRules = function () {
const self = this;
const ruleOrder = self.domainObject.configuration.ruleOrder;
const rules = self.rulesById;
self.ruleArea.innerHTML = '';
Object.values(ruleOrder).forEach(function (ruleId) {
self.ruleArea.append(rules[ruleId].getDOM());
});
this.executeRules();
this.addOrRemoveDragIndicator();
};
SummaryWidget.prototype.addOrRemoveDragIndicator = function () {
const rules = this.domainObject.configuration.ruleOrder;
const rulesById = this.rulesById;
rules.forEach(function (ruleKey, index, array) {
if (array.length > 2 && index > 0) {
rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = '';
} else {
rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = 'none';
}
});
};
/**
* Update the widget's appearance from the configuration of the active rule
*/
SummaryWidget.prototype.updateWidget = function () {
const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';
const activeRule = this.rulesById[this.activeId];
this.applyStyle(this.domElement.querySelector('#widget'), activeRule.getProperty('style'));
this.domElement.querySelector('#widget').title = activeRule.getProperty('message');
this.domElement.querySelector('#widgetLabel').textContent = activeRule.getProperty('label');
this.domElement.querySelector('#widgetIcon').classList =
WIDGET_ICON_CLASS + ' ' + activeRule.getProperty('icon');
};
/**
* Get the active rule and update the Widget's appearance.
*/
SummaryWidget.prototype.executeRules = function () {
this.activeId = this.conditionManager.executeRules(
this.domainObject.configuration.ruleOrder,
this.rulesById
);
this.updateWidget();
};
/**
* Add a new rule to this widget
*/
SummaryWidget.prototype.addRule = function () {
let ruleCount = 0;
let ruleId;
const ruleOrder = this.domainObject.configuration.ruleOrder;
while (Object.keys(this.rulesById).includes('rule' + ruleCount)) {
ruleCount++;
}
ruleId = 'rule' + ruleCount;
ruleOrder.push(ruleId);
this.domainObject.configuration.ruleOrder = ruleOrder;
this.initRule(ruleId, 'Rule');
this.updateDomainObject();
this.refreshRules();
};
/**
* Duplicate an existing widget rule from its configuration and splice it in
* after the rule it duplicates
* @param {Object} sourceConfig The configuration properties of the rule to be
* instantiated
*/
SummaryWidget.prototype.duplicateRule = function (sourceConfig) {
let ruleCount = 0;
let ruleId;
const sourceRuleId = sourceConfig.id;
const ruleOrder = this.domainObject.configuration.ruleOrder;
const ruleIds = Object.keys(this.rulesById);
while (ruleIds.includes('rule' + ruleCount)) {
ruleCount = ++ruleCount;
}
ruleId = 'rule' + ruleCount;
sourceConfig.id = ruleId;
sourceConfig.name += ' Copy';
ruleOrder.splice(ruleOrder.indexOf(sourceRuleId) + 1, 0, ruleId);
this.domainObject.configuration.ruleOrder = ruleOrder;
this.domainObject.configuration.ruleConfigById[ruleId] = sourceConfig;
this.initRule(ruleId, sourceConfig.name);
this.updateDomainObject();
this.refreshRules();
};
/**
* Initialize a new rule from a default configuration, or build a {Rule} object
* from it if already exists
* @param {string} ruleId An key to be used to identify this ruleId, or the key
of the rule to be instantiated
* @param {string} ruleName The initial human-readable name of this rule
*/
SummaryWidget.prototype.initRule = function (ruleId, ruleName) {
let ruleConfig;
const styleObj = {};
Object.assign(styleObj, DEFAULT_PROPS);
if (!this.domainObject.configuration.ruleConfigById[ruleId]) {
this.domainObject.configuration.ruleConfigById[ruleId] = {
name: ruleName || 'Rule',
label: 'Unnamed Rule',
message: '',
id: ruleId,
icon: ' ',
style: styleObj,
description: ruleId === 'default' ? 'Default appearance for the widget' : 'A new rule',
conditions: [
{
object: '',
key: '',
operation: '',
values: []
} }
}.bind(this) ],
); jsCondition: '',
}; trigger: 'any',
expanded: 'true'
};
}
/** ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId];
* Builds the Summary Widget's DOM, performs other necessary setup, and attaches this.rulesById[ruleId] = new Rule(
* this Summary Widget's view to the supplied container. ruleConfig,
* @param {element} container The DOM element that will contain this Summary this.domainObject,
* Widget's view. this.openmct,
*/ this.conditionManager,
SummaryWidget.prototype.show = function (container) { this.widgetDnD,
const self = this; this.container
this.container = container; );
this.container.append(this.domElement); this.rulesById[ruleId].on('remove', this.refreshRules, this);
this.domElement.querySelector('.widget-test-data').append(this.testDataManager.getDOM()); this.rulesById[ruleId].on('duplicate', this.duplicateRule, this);
this.widgetDnD = new WidgetDnD( this.rulesById[ruleId].on('change', this.updateWidget, this);
this.domElement, this.rulesById[ruleId].on('conditionChange', this.executeRules, this);
this.domainObject.configuration.ruleOrder, };
this.rulesById
);
this.initRule('default', 'Default');
this.domainObject.configuration.ruleOrder.forEach(function (ruleId) {
if (ruleId !== 'default') {
self.initRule(ruleId);
}
});
this.refreshRules();
this.updateWidget();
this.listenTo(this.addRuleButton, 'click', this.addRule); /**
this.conditionManager.on('receiveTelemetry', this.executeRules, this); * Given two ruleIds, move the source rule after the target rule and update
this.widgetDnD.on('drop', this.reorder, this); * the view.
}; * @param {Object} event An event object representing this drop with draggingId
* and dropTarget fields
*/
SummaryWidget.prototype.reorder = function (event) {
const ruleOrder = this.domainObject.configuration.ruleOrder;
const sourceIndex = ruleOrder.indexOf(event.draggingId);
let targetIndex;
/** if (event.draggingId !== event.dropTarget) {
* Unregister event listeners with the Open MCT APIs, unsubscribe from telemetry, ruleOrder.splice(sourceIndex, 1);
* and clean up event handlers targetIndex = ruleOrder.indexOf(event.dropTarget);
*/ ruleOrder.splice(targetIndex + 1, 0, event.draggingId);
SummaryWidget.prototype.destroy = function (container) {
this.editListenerUnsubscribe();
this.conditionManager.destroy();
this.testDataManager.destroy();
this.widgetDnD.destroy();
this.watchForChangesUnsubscribe();
Object.values(this.rulesById).forEach(function (rule) {
rule.destroy();
});
this.stopListening();
};
/**
* Update the view from the current rule configuration and order
*/
SummaryWidget.prototype.refreshRules = function () {
const self = this;
const ruleOrder = self.domainObject.configuration.ruleOrder;
const rules = self.rulesById;
self.ruleArea.innerHTML = '';
Object.values(ruleOrder).forEach(function (ruleId) {
self.ruleArea.append(rules[ruleId].getDOM());
});
this.executeRules();
this.addOrRemoveDragIndicator();
};
SummaryWidget.prototype.addOrRemoveDragIndicator = function () {
const rules = this.domainObject.configuration.ruleOrder;
const rulesById = this.rulesById;
rules.forEach(function (ruleKey, index, array) {
if (array.length > 2 && index > 0) {
rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = '';
} else {
rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = 'none';
}
});
};
/**
* Update the widget's appearance from the configuration of the active rule
*/
SummaryWidget.prototype.updateWidget = function () {
const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';
const activeRule = this.rulesById[this.activeId];
this.applyStyle(this.domElement.querySelector('#widget'), activeRule.getProperty('style'));
this.domElement.querySelector('#widget').title = activeRule.getProperty('message');
this.domElement.querySelector('#widgetLabel').textContent = activeRule.getProperty('label');
this.domElement.querySelector('#widgetIcon').classList =
WIDGET_ICON_CLASS + ' ' + activeRule.getProperty('icon');
};
/**
* Get the active rule and update the Widget's appearance.
*/
SummaryWidget.prototype.executeRules = function () {
this.activeId = this.conditionManager.executeRules(
this.domainObject.configuration.ruleOrder,
this.rulesById
);
this.updateWidget();
};
/**
* Add a new rule to this widget
*/
SummaryWidget.prototype.addRule = function () {
let ruleCount = 0;
let ruleId;
const ruleOrder = this.domainObject.configuration.ruleOrder;
while (Object.keys(this.rulesById).includes('rule' + ruleCount)) {
ruleCount++;
}
ruleId = 'rule' + ruleCount;
ruleOrder.push(ruleId);
this.domainObject.configuration.ruleOrder = ruleOrder; this.domainObject.configuration.ruleOrder = ruleOrder;
this.initRule(ruleId, 'Rule');
this.updateDomainObject(); this.updateDomainObject();
this.refreshRules(); }
};
/** this.refreshRules();
* Duplicate an existing widget rule from its configuration and splice it in };
* after the rule it duplicates
* @param {Object} sourceConfig The configuration properties of the rule to be
* instantiated
*/
SummaryWidget.prototype.duplicateRule = function (sourceConfig) {
let ruleCount = 0;
let ruleId;
const sourceRuleId = sourceConfig.id;
const ruleOrder = this.domainObject.configuration.ruleOrder;
const ruleIds = Object.keys(this.rulesById);
while (ruleIds.includes('rule' + ruleCount)) { /**
ruleCount = ++ruleCount; * Apply a list of css properties to an element
} * @param {element} elem The DOM element to which the rules will be applied
* @param {object} style an object representing the style
*/
SummaryWidget.prototype.applyStyle = function (elem, style) {
Object.keys(style).forEach(function (propId) {
elem.style[propId] = style[propId];
});
};
ruleId = 'rule' + ruleCount; /**
sourceConfig.id = ruleId; * Mutate this domain object's configuration with the current local configuration
sourceConfig.name += ' Copy'; */
ruleOrder.splice(ruleOrder.indexOf(sourceRuleId) + 1, 0, ruleId); SummaryWidget.prototype.updateDomainObject = function () {
this.domainObject.configuration.ruleOrder = ruleOrder; this.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration);
this.domainObject.configuration.ruleConfigById[ruleId] = sourceConfig; };
this.initRule(ruleId, sourceConfig.name);
this.updateDomainObject();
this.refreshRules();
};
/**
* Initialize a new rule from a default configuration, or build a {Rule} object
* from it if already exists
* @param {string} ruleId An key to be used to identify this ruleId, or the key
of the rule to be instantiated
* @param {string} ruleName The initial human-readable name of this rule
*/
SummaryWidget.prototype.initRule = function (ruleId, ruleName) {
let ruleConfig;
const styleObj = {};
Object.assign(styleObj, DEFAULT_PROPS);
if (!this.domainObject.configuration.ruleConfigById[ruleId]) {
this.domainObject.configuration.ruleConfigById[ruleId] = {
name: ruleName || 'Rule',
label: 'Unnamed Rule',
message: '',
id: ruleId,
icon: ' ',
style: styleObj,
description: ruleId === 'default' ? 'Default appearance for the widget' : 'A new rule',
conditions: [
{
object: '',
key: '',
operation: '',
values: []
}
],
jsCondition: '',
trigger: 'any',
expanded: 'true'
};
}
ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId];
this.rulesById[ruleId] = new Rule(
ruleConfig,
this.domainObject,
this.openmct,
this.conditionManager,
this.widgetDnD,
this.container
);
this.rulesById[ruleId].on('remove', this.refreshRules, this);
this.rulesById[ruleId].on('duplicate', this.duplicateRule, this);
this.rulesById[ruleId].on('change', this.updateWidget, this);
this.rulesById[ruleId].on('conditionChange', this.executeRules, this);
};
/**
* Given two ruleIds, move the source rule after the target rule and update
* the view.
* @param {Object} event An event object representing this drop with draggingId
* and dropTarget fields
*/
SummaryWidget.prototype.reorder = function (event) {
const ruleOrder = this.domainObject.configuration.ruleOrder;
const sourceIndex = ruleOrder.indexOf(event.draggingId);
let targetIndex;
if (event.draggingId !== event.dropTarget) {
ruleOrder.splice(sourceIndex, 1);
targetIndex = ruleOrder.indexOf(event.dropTarget);
ruleOrder.splice(targetIndex + 1, 0, event.draggingId);
this.domainObject.configuration.ruleOrder = ruleOrder;
this.updateDomainObject();
}
this.refreshRules();
};
/**
* Apply a list of css properties to an element
* @param {element} elem The DOM element to which the rules will be applied
* @param {object} style an object representing the style
*/
SummaryWidget.prototype.applyStyle = function (elem, style) {
Object.keys(style).forEach(function (propId) {
elem.style[propId] = style[propId];
});
};
/**
* Mutate this domain object's configuration with the current local configuration
*/
SummaryWidget.prototype.updateDomainObject = function () {
this.openmct.objects.mutate(
this.domainObject,
'configuration',
this.domainObject.configuration
);
};
return SummaryWidget;
});

View File

@ -1,193 +1,190 @@
define([ import EventEmitter from 'EventEmitter';
'../res/testDataItemTemplate.html',
'./input/ObjectSelect', import * as templateHelpers from '../../../utils/template/templateHelpers';
'./input/KeySelect', import itemTemplate from '../res/testDataItemTemplate.html';
'./eventHelpers', import eventHelpers from './eventHelpers';
'../../../utils/template/templateHelpers', import KeySelect from './input/KeySelect';
'EventEmitter' import ObjectSelect from './input/ObjectSelect';
], function (itemTemplate, ObjectSelect, KeySelect, eventHelpers, templateHelpers, EventEmitter) {
/**
* An object representing a single mock telemetry value
* @param {object} itemConfig the configuration for this item, consisting of
* object, key, and value fields
* @param {number} index the index of this TestDataItem object in the data
* model of its parent {TestDataManager} o be injected into callbacks
* for removes
* @param {ConditionManager} conditionManager a conditionManager instance
* for populating selects with configuration data
* @constructor
*/
export default function TestDataItem(itemConfig, index, conditionManager) {
eventHelpers.extend(this);
this.config = itemConfig;
this.index = index;
this.conditionManager = conditionManager;
this.domElement = templateHelpers.convertTemplateToHTML(itemTemplate)[0];
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['remove', 'duplicate', 'change'];
this.deleteButton = this.domElement.querySelector('.t-delete');
this.duplicateButton = this.domElement.querySelector('.t-duplicate');
this.selects = {};
this.valueInputs = [];
this.remove = this.remove.bind(this);
this.duplicate = this.duplicate.bind(this);
const self = this;
/** /**
* An object representing a single mock telemetry value * A change event handler for this item's select inputs, which also invokes
* @param {object} itemConfig the configuration for this item, consisting of * change callbacks registered with this item
* object, key, and value fields * @param {string} value The new value of this select item
* @param {number} index the index of this TestDataItem object in the data * @param {string} property The property of this item to modify
* model of its parent {TestDataManager} o be injected into callbacks * @private
* for removes
* @param {ConditionManager} conditionManager a conditionManager instance
* for populating selects with configuration data
* @constructor
*/ */
function TestDataItem(itemConfig, index, conditionManager) { function onSelectChange(value, property) {
eventHelpers.extend(this); if (property === 'key') {
this.config = itemConfig; self.generateValueInput(value);
this.index = index;
this.conditionManager = conditionManager;
this.domElement = templateHelpers.convertTemplateToHTML(itemTemplate)[0];
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['remove', 'duplicate', 'change'];
this.deleteButton = this.domElement.querySelector('.t-delete');
this.duplicateButton = this.domElement.querySelector('.t-duplicate');
this.selects = {};
this.valueInputs = [];
this.remove = this.remove.bind(this);
this.duplicate = this.duplicate.bind(this);
const self = this;
/**
* A change event handler for this item's select inputs, which also invokes
* change callbacks registered with this item
* @param {string} value The new value of this select item
* @param {string} property The property of this item to modify
* @private
*/
function onSelectChange(value, property) {
if (property === 'key') {
self.generateValueInput(value);
}
self.eventEmitter.emit('change', {
value: value,
property: property,
index: self.index
});
} }
/** self.eventEmitter.emit('change', {
* An input event handler for this item's value field. Invokes any change value: value,
* callbacks associated with this item property: property,
* @param {Event} event The input event that initiated this callback index: self.index
* @private
*/
function onValueInput(event) {
const elem = event.target;
const value = isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber;
if (elem.tagName.toUpperCase() === 'INPUT') {
self.eventEmitter.emit('change', {
value: value,
property: 'value',
index: self.index
});
}
}
this.listenTo(this.deleteButton, 'click', this.remove);
this.listenTo(this.duplicateButton, 'click', this.duplicate);
this.selects.object = new ObjectSelect(this.config, this.conditionManager);
this.selects.key = new KeySelect(
this.config,
this.selects.object,
this.conditionManager,
function (value) {
onSelectChange(value, 'key');
}
);
this.selects.object.on('change', function (value) {
onSelectChange(value, 'object');
}); });
Object.values(this.selects).forEach(function (select) {
self.domElement.querySelector('.t-configuration').append(select.getDOM());
});
this.listenTo(this.domElement, 'input', onValueInput);
} }
/** /**
* Gets the DOM associated with this element's view * An input event handler for this item's value field. Invokes any change
* @return {Element} * callbacks associated with this item
* @param {Event} event The input event that initiated this callback
* @private
*/ */
TestDataItem.prototype.getDOM = function (container) { function onValueInput(event) {
return this.domElement; const elem = event.target;
}; const value = isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber;
/** if (elem.tagName.toUpperCase() === 'INPUT') {
* Register a callback with this item: supported callbacks are remove, change, self.eventEmitter.emit('change', {
* and duplicate value: value,
* @param {string} event The key for the event to listen to property: 'value',
* @param {function} callback The function that this rule will invoke on this event index: self.index
* @param {Object} context A reference to a scope to use as the context for });
* context for the callback function
*/
TestDataItem.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.on(event, callback, context || this);
} }
}; }
/** this.listenTo(this.deleteButton, 'click', this.remove);
* Implement "off" to complete event emitter interface. this.listenTo(this.duplicateButton, 'click', this.duplicate);
*/
TestDataItem.prototype.off = function (event, callback, context) {
this.eventEmitter.off(event, callback, context);
};
/** this.selects.object = new ObjectSelect(this.config, this.conditionManager);
* Hide the appropriate inputs when this is the only item this.selects.key = new KeySelect(
*/ this.config,
TestDataItem.prototype.hideButtons = function () { this.selects.object,
this.deleteButton.style.display = 'none'; this.conditionManager,
}; function (value) {
onSelectChange(value, 'key');
/**
* Remove this item from the configuration. Invokes any registered
* remove callbacks
*/
TestDataItem.prototype.remove = function () {
const self = this;
this.eventEmitter.emit('remove', self.index);
this.stopListening();
Object.values(this.selects).forEach(function (select) {
select.destroy();
});
};
/**
* Makes a deep clone of this item's configuration, and invokes any registered
* duplicate callbacks with the cloned configuration as an argument
*/
TestDataItem.prototype.duplicate = function () {
const sourceItem = JSON.parse(JSON.stringify(this.config));
const self = this;
this.eventEmitter.emit('duplicate', {
sourceItem: sourceItem,
index: self.index
});
};
/**
* When a telemetry property key is selected, create the appropriate value input
* and add it to the view
* @param {string} key The key of currently selected telemetry property
*/
TestDataItem.prototype.generateValueInput = function (key) {
const evaluator = this.conditionManager.getEvaluator();
const inputArea = this.domElement.querySelector('.t-value-inputs');
const dataType = this.conditionManager.getTelemetryPropertyType(this.config.object, key);
const inputType = evaluator.getInputTypeById(dataType);
inputArea.innerHTML = '';
if (inputType) {
if (!this.config.value) {
this.config.value = inputType === 'number' ? 0 : '';
}
const newInput = document.createElement('input');
newInput.type = `${inputType}`;
newInput.value = `${this.config.value}`;
this.valueInput = newInput;
inputArea.append(this.valueInput);
} }
}; );
return TestDataItem; this.selects.object.on('change', function (value) {
}); onSelectChange(value, 'object');
});
Object.values(this.selects).forEach(function (select) {
self.domElement.querySelector('.t-configuration').append(select.getDOM());
});
this.listenTo(this.domElement, 'input', onValueInput);
}
/**
* Gets the DOM associated with this element's view
* @return {Element}
*/
TestDataItem.prototype.getDOM = function (container) {
return this.domElement;
};
/**
* Register a callback with this item: supported callbacks are remove, change,
* and duplicate
* @param {string} event The key for the event to listen to
* @param {function} callback The function that this rule will invoke on this event
* @param {Object} context A reference to a scope to use as the context for
* context for the callback function
*/
TestDataItem.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.on(event, callback, context || this);
}
};
/**
* Implement "off" to complete event emitter interface.
*/
TestDataItem.prototype.off = function (event, callback, context) {
this.eventEmitter.off(event, callback, context);
};
/**
* Hide the appropriate inputs when this is the only item
*/
TestDataItem.prototype.hideButtons = function () {
this.deleteButton.style.display = 'none';
};
/**
* Remove this item from the configuration. Invokes any registered
* remove callbacks
*/
TestDataItem.prototype.remove = function () {
const self = this;
this.eventEmitter.emit('remove', self.index);
this.stopListening();
Object.values(this.selects).forEach(function (select) {
select.destroy();
});
};
/**
* Makes a deep clone of this item's configuration, and invokes any registered
* duplicate callbacks with the cloned configuration as an argument
*/
TestDataItem.prototype.duplicate = function () {
const sourceItem = JSON.parse(JSON.stringify(this.config));
const self = this;
this.eventEmitter.emit('duplicate', {
sourceItem: sourceItem,
index: self.index
});
};
/**
* When a telemetry property key is selected, create the appropriate value input
* and add it to the view
* @param {string} key The key of currently selected telemetry property
*/
TestDataItem.prototype.generateValueInput = function (key) {
const evaluator = this.conditionManager.getEvaluator();
const inputArea = this.domElement.querySelector('.t-value-inputs');
const dataType = this.conditionManager.getTelemetryPropertyType(this.config.object, key);
const inputType = evaluator.getInputTypeById(dataType);
inputArea.innerHTML = '';
if (inputType) {
if (!this.config.value) {
this.config.value = inputType === 'number' ? 0 : '';
}
const newInput = document.createElement('input');
newInput.type = `${inputType}`;
newInput.value = `${this.config.value}`;
this.valueInput = newInput;
inputArea.append(this.valueInput);
}
};

View File

@ -1,201 +1,198 @@
define([ import _ from 'lodash';
'./eventHelpers',
'../res/testDataTemplate.html', import * as templateHelpers from '../../../utils/template/templateHelpers';
'./TestDataItem', import testDataTemplate from '../res/testDataTemplate.html';
'../../../utils/template/templateHelpers', import eventHelpers from './eventHelpers';
'lodash' import TestDataItem from './TestDataItem';
], function (eventHelpers, testDataTemplate, TestDataItem, templateHelpers, _) {
/**
* Controls the input and usage of test data in the summary widget.
* @constructor
* @param {Object} domainObject The summary widget domain object
* @param {ConditionManager} conditionManager A conditionManager instance
* @param {MCT} openmct and MCT instance
*/
export default function TestDataManager(domainObject, conditionManager, openmct) {
eventHelpers.extend(this);
const self = this;
this.domainObject = domainObject;
this.manager = conditionManager;
this.openmct = openmct;
this.evaluator = this.manager.getEvaluator();
this.domElement = templateHelpers.convertTemplateToHTML(testDataTemplate)[0];
this.config = this.domainObject.configuration.testDataConfig;
this.testCache = {};
this.itemArea = this.domElement.querySelector('.t-test-data-config');
this.addItemButton = this.domElement.querySelector('.add-test-condition');
this.testDataInput = this.domElement.querySelector('.t-test-data-checkbox');
/** /**
* Controls the input and usage of test data in the summary widget. * Toggles whether the associated {ConditionEvaluator} uses the actual
* @constructor * subscription cache or the test data cache
* @param {Object} domainObject The summary widget domain object * @param {Event} event The change event that triggered this callback
* @param {ConditionManager} conditionManager A conditionManager instance * @private
* @param {MCT} openmct and MCT instance
*/ */
function TestDataManager(domainObject, conditionManager, openmct) { function toggleTestData(event) {
eventHelpers.extend(this); const elem = event.target;
const self = this; self.evaluator.useTestData(elem.checked);
self.updateTestCache();
this.domainObject = domainObject;
this.manager = conditionManager;
this.openmct = openmct;
this.evaluator = this.manager.getEvaluator();
this.domElement = templateHelpers.convertTemplateToHTML(testDataTemplate)[0];
this.config = this.domainObject.configuration.testDataConfig;
this.testCache = {};
this.itemArea = this.domElement.querySelector('.t-test-data-config');
this.addItemButton = this.domElement.querySelector('.add-test-condition');
this.testDataInput = this.domElement.querySelector('.t-test-data-checkbox');
/**
* Toggles whether the associated {ConditionEvaluator} uses the actual
* subscription cache or the test data cache
* @param {Event} event The change event that triggered this callback
* @private
*/
function toggleTestData(event) {
const elem = event.target;
self.evaluator.useTestData(elem.checked);
self.updateTestCache();
}
this.listenTo(this.addItemButton, 'click', function () {
self.initItem();
});
this.listenTo(this.testDataInput, 'change', toggleTestData);
this.evaluator.setTestDataCache(this.testCache);
this.evaluator.useTestData(false);
this.refreshItems();
} }
/** this.listenTo(this.addItemButton, 'click', function () {
* Get the DOM element representing this test data manager in the view self.initItem();
*/ });
TestDataManager.prototype.getDOM = function () { this.listenTo(this.testDataInput, 'change', toggleTestData);
return this.domElement;
this.evaluator.setTestDataCache(this.testCache);
this.evaluator.useTestData(false);
this.refreshItems();
}
/**
* Get the DOM element representing this test data manager in the view
*/
TestDataManager.prototype.getDOM = function () {
return this.domElement;
};
/**
* Initialize a new test data item, either from a source configuration, or with
* the default empty configuration
* @param {Object} [config] An object with sourceItem and index fields to instantiate
* this rule from, optional
*/
TestDataManager.prototype.initItem = function (config) {
const sourceIndex = config && config.index;
const defaultItem = {
object: '',
key: '',
value: ''
}; };
let newItem;
/** newItem = config !== undefined ? config.sourceItem : defaultItem;
* Initialize a new test data item, either from a source configuration, or with if (sourceIndex !== undefined) {
* the default empty configuration this.config.splice(sourceIndex + 1, 0, newItem);
* @param {Object} [config] An object with sourceItem and index fields to instantiate } else {
* this rule from, optional this.config.push(newItem);
*/ }
TestDataManager.prototype.initItem = function (config) {
const sourceIndex = config && config.index;
const defaultItem = {
object: '',
key: '',
value: ''
};
let newItem;
newItem = config !== undefined ? config.sourceItem : defaultItem; this.updateDomainObject();
if (sourceIndex !== undefined) { this.refreshItems();
this.config.splice(sourceIndex + 1, 0, newItem); };
} else {
this.config.push(newItem);
}
this.updateDomainObject(); /**
this.refreshItems(); * Remove an item from this TestDataManager at the given index
}; * @param {number} removeIndex The index of the item to remove
*/
TestDataManager.prototype.removeItem = function (removeIndex) {
_.remove(this.config, function (item, index) {
return index === removeIndex;
});
this.updateDomainObject();
this.refreshItems();
};
/** /**
* Remove an item from this TestDataManager at the given index * Change event handler for the test data items which compose this
* @param {number} removeIndex The index of the item to remove * test data generator
*/ * @param {Object} event An object representing this event, with value, property,
TestDataManager.prototype.removeItem = function (removeIndex) { * and index fields
_.remove(this.config, function (item, index) { */
return index === removeIndex; TestDataManager.prototype.onItemChange = function (event) {
}); this.config[event.index][event.property] = event.value;
this.updateDomainObject(); this.updateDomainObject();
this.refreshItems(); this.updateTestCache();
}; };
/** /**
* Change event handler for the test data items which compose this * Builds the test cache from the current item configuration, and passes
* test data generator * the new test cache to the associated {ConditionEvaluator} instance
* @param {Object} event An object representing this event, with value, property, */
* and index fields TestDataManager.prototype.updateTestCache = function () {
*/ this.generateTestCache();
TestDataManager.prototype.onItemChange = function (event) { this.evaluator.setTestDataCache(this.testCache);
this.config[event.index][event.property] = event.value; this.manager.triggerTelemetryCallback();
this.updateDomainObject(); };
this.updateTestCache();
};
/** /**
* Builds the test cache from the current item configuration, and passes * Instantiate {TestDataItem} objects from the current configuration, and
* the new test cache to the associated {ConditionEvaluator} instance * update the view accordingly
*/ */
TestDataManager.prototype.updateTestCache = function () { TestDataManager.prototype.refreshItems = function () {
this.generateTestCache(); const self = this;
this.evaluator.setTestDataCache(this.testCache); if (this.items) {
this.manager.triggerTelemetryCallback();
};
/**
* Instantiate {TestDataItem} objects from the current configuration, and
* update the view accordingly
*/
TestDataManager.prototype.refreshItems = function () {
const self = this;
if (this.items) {
this.items.forEach(function (item) {
this.stopListening(item);
}, this);
}
self.items = [];
this.domElement.querySelectorAll('.t-test-data-item').forEach((item) => {
item.remove();
});
this.config.forEach(function (item, index) {
const newItem = new TestDataItem(item, index, self.manager);
self.listenTo(newItem, 'remove', self.removeItem, self);
self.listenTo(newItem, 'duplicate', self.initItem, self);
self.listenTo(newItem, 'change', self.onItemChange, self);
self.items.push(newItem);
});
self.items.forEach(function (item) {
self.itemArea.prepend(item.getDOM());
});
if (self.items.length === 1) {
self.items[0].hideButtons();
}
this.updateTestCache();
};
/**
* Builds a test data cache in the format of a telemetry subscription cache
* as expected by a {ConditionEvaluator}
*/
TestDataManager.prototype.generateTestCache = function () {
let testCache = this.testCache;
const manager = this.manager;
const compositionObjs = manager.getComposition();
let metadata;
testCache = {};
Object.keys(compositionObjs).forEach(function (id) {
testCache[id] = {};
metadata = manager.getTelemetryMetadata(id);
Object.keys(metadata).forEach(function (key) {
testCache[id][key] = '';
});
});
this.config.forEach(function (item) {
if (testCache[item.object]) {
testCache[item.object][item.key] = item.value;
}
});
this.testCache = testCache;
};
/**
* Update the domain object configuration associated with this test data manager
*/
TestDataManager.prototype.updateDomainObject = function () {
this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config);
};
TestDataManager.prototype.destroy = function () {
this.stopListening();
this.items.forEach(function (item) { this.items.forEach(function (item) {
item.remove(); this.stopListening(item);
}); }, this);
}; }
return TestDataManager; self.items = [];
});
this.domElement.querySelectorAll('.t-test-data-item').forEach((item) => {
item.remove();
});
this.config.forEach(function (item, index) {
const newItem = new TestDataItem(item, index, self.manager);
self.listenTo(newItem, 'remove', self.removeItem, self);
self.listenTo(newItem, 'duplicate', self.initItem, self);
self.listenTo(newItem, 'change', self.onItemChange, self);
self.items.push(newItem);
});
self.items.forEach(function (item) {
self.itemArea.prepend(item.getDOM());
});
if (self.items.length === 1) {
self.items[0].hideButtons();
}
this.updateTestCache();
};
/**
* Builds a test data cache in the format of a telemetry subscription cache
* as expected by a {ConditionEvaluator}
*/
TestDataManager.prototype.generateTestCache = function () {
let testCache = this.testCache;
const manager = this.manager;
const compositionObjs = manager.getComposition();
let metadata;
testCache = {};
Object.keys(compositionObjs).forEach(function (id) {
testCache[id] = {};
metadata = manager.getTelemetryMetadata(id);
Object.keys(metadata).forEach(function (key) {
testCache[id][key] = '';
});
});
this.config.forEach(function (item) {
if (testCache[item.object]) {
testCache[item.object][item.key] = item.value;
}
});
this.testCache = testCache;
};
/**
* Update the domain object configuration associated with this test data manager
*/
TestDataManager.prototype.updateDomainObject = function () {
this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config);
};
TestDataManager.prototype.destroy = function () {
this.stopListening();
this.items.forEach(function (item) {
item.remove();
});
};

View File

@ -1,165 +1,162 @@
define([ import EventEmitter from 'EventEmitter';
'../res/ruleImageTemplate.html',
'EventEmitter',
'../../../utils/template/templateHelpers'
], function (ruleImageTemplate, EventEmitter, templateHelpers) {
/**
* Manages the Sortable List interface for reordering rules by drag and drop
* @param {Element} container The DOM element that contains this Summary Widget's view
* @param {string[]} ruleOrder An array of rule IDs representing the current rule order
* @param {Object} rulesById An object mapping rule IDs to rule configurations
*/
function WidgetDnD(container, ruleOrder, rulesById) {
this.container = container;
this.ruleOrder = ruleOrder;
this.rulesById = rulesById;
this.imageContainer = templateHelpers.convertTemplateToHTML(ruleImageTemplate)[0]; import * as templateHelpers from '../../../utils/template/templateHelpers';
this.image = this.imageContainer.querySelector('.t-drag-rule-image'); import ruleImageTemplate from '../res/ruleImageTemplate.html';
this.draggingId = '';
this.draggingRulePrevious = '';
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['drop'];
this.drag = this.drag.bind(this); /**
this.drop = this.drop.bind(this); * Manages the Sortable List interface for reordering rules by drag and drop
* @param {Element} container The DOM element that contains this Summary Widget's view
* @param {string[]} ruleOrder An array of rule IDs representing the current rule order
* @param {Object} rulesById An object mapping rule IDs to rule configurations
*/
export default function WidgetDnD(container, ruleOrder, rulesById) {
this.container = container;
this.ruleOrder = ruleOrder;
this.rulesById = rulesById;
this.container.addEventListener('mousemove', this.drag); this.imageContainer = templateHelpers.convertTemplateToHTML(ruleImageTemplate)[0];
document.addEventListener('mouseup', this.drop); this.image = this.imageContainer.querySelector('.t-drag-rule-image');
this.container.parentNode.insertBefore(this.imageContainer, this.container); this.draggingId = '';
this.imageContainer.style.display = 'none'; this.draggingRulePrevious = '';
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['drop'];
this.drag = this.drag.bind(this);
this.drop = this.drop.bind(this);
this.container.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.drop);
this.container.parentNode.insertBefore(this.imageContainer, this.container);
this.imageContainer.style.display = 'none';
}
/**
* Remove event listeners registered to elements external to the widget
*/
WidgetDnD.prototype.destroy = function () {
this.container.removeEventListener('mousemove', this.drag);
document.removeEventListener('mouseup', this.drop);
};
/**
* Register a callback with this WidgetDnD: supported callback is drop
* @param {string} event The key for the event to listen to
* @param {function} callback The function that this rule will invoke on this event
* @param {Object} context A reference to a scope to use as the context for
* context for the callback function
*/
WidgetDnD.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.on(event, callback, context || this);
} }
};
/** /**
* Remove event listeners registered to elements external to the widget * Sets the image for the dragged element to the given DOM element
*/ * @param {Element} image The HTML element to set as the drap image
WidgetDnD.prototype.destroy = function () { */
this.container.removeEventListener('mousemove', this.drag); WidgetDnD.prototype.setDragImage = function (image) {
document.removeEventListener('mouseup', this.drop); this.image.html(image);
}; };
/** /**
* Register a callback with this WidgetDnD: supported callback is drop * Calculate where this rule has been dragged relative to the other rules
* @param {string} event The key for the event to listen to * @param {Event} event The mousemove or mouseup event that triggered this
* @param {function} callback The function that this rule will invoke on this event event handler
* @param {Object} context A reference to a scope to use as the context for * @return {string} The ID of the rule whose drag indicator should be displayed
* context for the callback function */
*/ WidgetDnD.prototype.getDropLocation = function (event) {
WidgetDnD.prototype.on = function (event, callback, context) { const ruleOrder = this.ruleOrder;
if (this.supportedCallbacks.includes(event)) { const rulesById = this.rulesById;
this.eventEmitter.on(event, callback, context || this); const draggingId = this.draggingId;
} let offset;
}; let y;
let height;
const dropY = event.pageY;
let target = '';
/** ruleOrder.forEach(function (ruleId, index) {
* Sets the image for the dragged element to the given DOM element const ruleDOM = rulesById[ruleId].getDOM();
* @param {Element} image The HTML element to set as the drap image offset = window.innerWidth - (ruleDOM.offsetLeft + ruleDOM.offsetWidth);
*/ y = offset.top;
WidgetDnD.prototype.setDragImage = function (image) { height = offset.height;
this.image.html(image); if (index === 0) {
}; if (dropY < y + (7 * height) / 3) {
target = ruleId;
/**
* Calculate where this rule has been dragged relative to the other rules
* @param {Event} event The mousemove or mouseup event that triggered this
event handler
* @return {string} The ID of the rule whose drag indicator should be displayed
*/
WidgetDnD.prototype.getDropLocation = function (event) {
const ruleOrder = this.ruleOrder;
const rulesById = this.rulesById;
const draggingId = this.draggingId;
let offset;
let y;
let height;
const dropY = event.pageY;
let target = '';
ruleOrder.forEach(function (ruleId, index) {
const ruleDOM = rulesById[ruleId].getDOM();
offset = window.innerWidth - (ruleDOM.offsetLeft + ruleDOM.offsetWidth);
y = offset.top;
height = offset.height;
if (index === 0) {
if (dropY < y + (7 * height) / 3) {
target = ruleId;
}
} else if (index === ruleOrder.length - 1 && ruleId !== draggingId) {
if (y + height / 3 < dropY) {
target = ruleId;
}
} else {
if (y + height / 3 < dropY && dropY < y + (7 * height) / 3) {
target = ruleId;
}
} }
}); } else if (index === ruleOrder.length - 1 && ruleId !== draggingId) {
if (y + height / 3 < dropY) {
target = ruleId;
}
} else {
if (y + height / 3 < dropY && dropY < y + (7 * height) / 3) {
target = ruleId;
}
}
});
return target; return target;
}; };
/** /**
* Called by a {Rule} instance that initiates a drag gesture * Called by a {Rule} instance that initiates a drag gesture
* @param {string} ruleId The identifier of the rule which is being dragged * @param {string} ruleId The identifier of the rule which is being dragged
*/ */
WidgetDnD.prototype.dragStart = function (ruleId) { WidgetDnD.prototype.dragStart = function (ruleId) {
const ruleOrder = this.ruleOrder; const ruleOrder = this.ruleOrder;
this.draggingId = ruleId; this.draggingId = ruleId;
this.draggingRulePrevious = ruleOrder[ruleOrder.indexOf(ruleId) - 1]; this.draggingRulePrevious = ruleOrder[ruleOrder.indexOf(ruleId) - 1];
this.rulesById[this.draggingRulePrevious].showDragIndicator(); this.rulesById[this.draggingRulePrevious].showDragIndicator();
this.imageContainer.show(); this.imageContainer.show();
this.imageContainer.offset({
top: event.pageY - this.image.height() / 2,
left: event.pageX - this.image.querySelector('.t-grippy').style.width
});
};
/**
* An event handler for a mousemove event, once a rule has begun a drag gesture
* @param {Event} event The mousemove event that triggered this callback
*/
WidgetDnD.prototype.drag = function (event) {
let dragTarget;
if (this.draggingId && this.draggingId !== '') {
event.preventDefault();
dragTarget = this.getDropLocation(event);
this.imageContainer.offset({ this.imageContainer.offset({
top: event.pageY - this.image.height() / 2, top: event.pageY - this.image.height() / 2,
left: event.pageX - this.image.querySelector('.t-grippy').style.width left: event.pageX - this.image.querySelector('.t-grippy').style.width
}); });
}; if (this.rulesById[dragTarget]) {
this.rulesById[dragTarget].showDragIndicator();
/** } else {
* An event handler for a mousemove event, once a rule has begun a drag gesture this.rulesById[this.draggingRulePrevious].showDragIndicator();
* @param {Event} event The mousemove event that triggered this callback
*/
WidgetDnD.prototype.drag = function (event) {
let dragTarget;
if (this.draggingId && this.draggingId !== '') {
event.preventDefault();
dragTarget = this.getDropLocation(event);
this.imageContainer.offset({
top: event.pageY - this.image.height() / 2,
left: event.pageX - this.image.querySelector('.t-grippy').style.width
});
if (this.rulesById[dragTarget]) {
this.rulesById[dragTarget].showDragIndicator();
} else {
this.rulesById[this.draggingRulePrevious].showDragIndicator();
}
} }
}; }
};
/** /**
* Handles the mouseup event that corresponds to the user dropping the rule * Handles the mouseup event that corresponds to the user dropping the rule
* in its final location. Invokes any registered drop callbacks with the dragged * in its final location. Invokes any registered drop callbacks with the dragged
* rule's ID and the ID of the target rule that the dragged rule should be * rule's ID and the ID of the target rule that the dragged rule should be
* inserted after * inserted after
* @param {Event} event The mouseup event that triggered this callback * @param {Event} event The mouseup event that triggered this callback
*/ */
WidgetDnD.prototype.drop = function (event) { WidgetDnD.prototype.drop = function (event) {
let dropTarget = this.getDropLocation(event); let dropTarget = this.getDropLocation(event);
const draggingId = this.draggingId; const draggingId = this.draggingId;
if (this.draggingId && this.draggingId !== '') { if (this.draggingId && this.draggingId !== '') {
if (!this.rulesById[dropTarget]) { if (!this.rulesById[dropTarget]) {
dropTarget = this.draggingId; dropTarget = this.draggingId;
}
this.eventEmitter.emit('drop', {
draggingId: draggingId,
dropTarget: dropTarget
});
this.draggingId = '';
this.draggingRulePrevious = '';
this.imageContainer.hide();
} }
};
return WidgetDnD; this.eventEmitter.emit('drop', {
}); draggingId: draggingId,
dropTarget: dropTarget
});
this.draggingId = '';
this.draggingRulePrevious = '';
this.imageContainer.hide();
}
};

View File

@ -20,80 +20,78 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { const helperFunctions = {
const helperFunctions = { listenTo(object, event, callback, context) {
listenTo: function (object, event, callback, context) { if (!this._listeningTo) {
if (!this._listeningTo) { this._listeningTo = [];
this._listeningTo = [];
}
const listener = {
object: object,
event: event,
callback: callback,
context: context,
_cb: context ? callback.bind(context) : callback
};
if (object.$watch && event.indexOf('change:') === 0) {
const scopePath = event.replace('change:', '');
listener.unlisten = object.$watch(scopePath, listener._cb, true);
} else if (object.$on) {
listener.unlisten = object.$on(event, listener._cb);
} else if (object.addEventListener) {
object.addEventListener(event, listener._cb);
} else {
object.on(event, listener._cb);
}
this._listeningTo.push(listener);
},
stopListening: function (object, event, callback, context) {
if (!this._listeningTo) {
this._listeningTo = [];
}
this._listeningTo
.filter(function (listener) {
if (object && object !== listener.object) {
return false;
}
if (event && event !== listener.event) {
return false;
}
if (callback && callback !== listener.callback) {
return false;
}
if (context && context !== listener.context) {
return false;
}
return true;
})
.map(function (listener) {
if (listener.unlisten) {
listener.unlisten();
} else if (listener.object.removeEventListener) {
listener.object.removeEventListener(listener.event, listener._cb);
} else {
listener.object.off(listener.event, listener._cb);
}
return listener;
})
.forEach(function (listener) {
this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);
}, this);
},
extend: function (object) {
object.listenTo = helperFunctions.listenTo;
object.stopListening = helperFunctions.stopListening;
} }
};
return helperFunctions; const listener = {
}); object: object,
event: event,
callback: callback,
context: context,
_cb: context ? callback.bind(context) : callback
};
if (object.$watch && event.indexOf('change:') === 0) {
const scopePath = event.replace('change:', '');
listener.unlisten = object.$watch(scopePath, listener._cb, true);
} else if (object.$on) {
listener.unlisten = object.$on(event, listener._cb);
} else if (object.addEventListener) {
object.addEventListener(event, listener._cb);
} else {
object.on(event, listener._cb);
}
this._listeningTo.push(listener);
},
stopListening(object, event, callback, context) {
if (!this._listeningTo) {
this._listeningTo = [];
}
this._listeningTo
.filter(function (listener) {
if (object && object !== listener.object) {
return false;
}
if (event && event !== listener.event) {
return false;
}
if (callback && callback !== listener.callback) {
return false;
}
if (context && context !== listener.context) {
return false;
}
return true;
})
.map(function (listener) {
if (listener.unlisten) {
listener.unlisten();
} else if (listener.object.removeEventListener) {
listener.object.removeEventListener(listener.event, listener._cb);
} else {
listener.object.off(listener.event, listener._cb);
}
return listener;
})
.forEach(function (listener) {
this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);
}, this);
},
extend: function (object) {
object.listenTo = helperFunctions.listenTo;
object.stopListening = helperFunctions.stopListening;
}
};
export default helperFunctions;

View File

@ -1,128 +1,126 @@
define(['./Palette'], function (Palette) { import Palette from './Palette';
//The colors that will be used to instantiate this palette if none are provided
const DEFAULT_COLORS = [ // The colors that will be used to instantiate this palette if none are provided
'#000000', const DEFAULT_COLORS = [
'#434343', '#000000',
'#666666', '#434343',
'#999999', '#666666',
'#b7b7b7', '#999999',
'#cccccc', '#b7b7b7',
'#d9d9d9', '#cccccc',
'#efefef', '#d9d9d9',
'#f3f3f3', '#efefef',
'#ffffff', '#f3f3f3',
'#980000', '#ffffff',
'#ff0000', '#980000',
'#ff9900', '#ff0000',
'#ffff00', '#ff9900',
'#00ff00', '#ffff00',
'#00ffff', '#00ff00',
'#4a86e8', '#00ffff',
'#0000ff', '#4a86e8',
'#9900ff', '#0000ff',
'#ff00ff', '#9900ff',
'#e6b8af', '#ff00ff',
'#f4cccc', '#e6b8af',
'#fce5cd', '#f4cccc',
'#fff2cc', '#fce5cd',
'#d9ead3', '#fff2cc',
'#d0e0e3', '#d9ead3',
'#c9daf8', '#d0e0e3',
'#cfe2f3', '#c9daf8',
'#d9d2e9', '#cfe2f3',
'#ead1dc', '#d9d2e9',
'#dd7e6b', '#ead1dc',
'#dd7e6b', '#dd7e6b',
'#f9cb9c', '#dd7e6b',
'#ffe599', '#f9cb9c',
'#b6d7a8', '#ffe599',
'#a2c4c9', '#b6d7a8',
'#a4c2f4', '#a2c4c9',
'#9fc5e8', '#a4c2f4',
'#b4a7d6', '#9fc5e8',
'#d5a6bd', '#b4a7d6',
'#cc4125', '#d5a6bd',
'#e06666', '#cc4125',
'#f6b26b', '#e06666',
'#ffd966', '#f6b26b',
'#93c47d', '#ffd966',
'#76a5af', '#93c47d',
'#6d9eeb', '#76a5af',
'#6fa8dc', '#6d9eeb',
'#8e7cc3', '#6fa8dc',
'#c27ba0', '#8e7cc3',
'#a61c00', '#c27ba0',
'#cc0000', '#a61c00',
'#e69138', '#cc0000',
'#f1c232', '#e69138',
'#6aa84f', '#f1c232',
'#45818e', '#6aa84f',
'#3c78d8', '#45818e',
'#3d85c6', '#3c78d8',
'#674ea7', '#3d85c6',
'#a64d79', '#674ea7',
'#85200c', '#a64d79',
'#990000', '#85200c',
'#b45f06', '#990000',
'#bf9000', '#b45f06',
'#38761d', '#bf9000',
'#134f5c', '#38761d',
'#1155cc', '#134f5c',
'#0b5394', '#1155cc',
'#351c75', '#0b5394',
'#741b47', '#351c75',
'#5b0f00', '#741b47',
'#660000', '#5b0f00',
'#783f04', '#660000',
'#7f6000', '#783f04',
'#274e13', '#7f6000',
'#0c343d', '#274e13',
'#1c4587', '#0c343d',
'#073763', '#1c4587',
'#20124d', '#073763',
'#4c1130' '#20124d',
]; '#4c1130'
];
/**
* Instantiates a new Open MCT Color Palette input
* @constructor
* @param {string} cssClass The class name of the icon which should be applied
* to this palette
* @param {Element} container The view that contains this palette
* @param {string[]} colors (optional) A list of colors that should be used to instantiate this palette
*/
export default function ColorPalette(cssClass, container, colors) {
this.colors = colors || DEFAULT_COLORS;
this.palette = new Palette(cssClass, container, this.colors);
this.palette.setNullOption('rgba(0,0,0,0)');
const domElement = this.palette.getDOM();
const self = this;
domElement.querySelector('.c-button--menu').classList.add('c-button--swatched');
domElement.querySelector('.t-swatch').classList.add('color-swatch');
domElement.querySelector('.c-palette').classList.add('c-palette--color');
domElement.querySelectorAll('.c-palette__item').forEach((item) => {
// eslint-disable-next-line no-invalid-this
item.style.backgroundColor = item.dataset.item;
});
/** /**
* Instantiates a new Open MCT Color Palette input * Update this palette's current selection indicator with the style
* @constructor * of the currently selected item
* @param {string} cssClass The class name of the icon which should be applied * @private
* to this palette
* @param {Element} container The view that contains this palette
* @param {string[]} colors (optional) A list of colors that should be used to instantiate this palette
*/ */
function ColorPalette(cssClass, container, colors) { function updateSwatch() {
this.colors = colors || DEFAULT_COLORS; const color = self.palette.getCurrent();
this.palette = new Palette(cssClass, container, this.colors); domElement.querySelector('.color-swatch').style.backgroundColor = color;
this.palette.setNullOption('rgba(0,0,0,0)');
const domElement = this.palette.getDOM();
const self = this;
domElement.querySelector('.c-button--menu').classList.add('c-button--swatched');
domElement.querySelector('.t-swatch').classList.add('color-swatch');
domElement.querySelector('.c-palette').classList.add('c-palette--color');
domElement.querySelectorAll('.c-palette__item').forEach((item) => {
// eslint-disable-next-line no-invalid-this
item.style.backgroundColor = item.dataset.item;
});
/**
* Update this palette's current selection indicator with the style
* of the currently selected item
* @private
*/
function updateSwatch() {
const color = self.palette.getCurrent();
domElement.querySelector('.color-swatch').style.backgroundColor = color;
}
this.palette.on('change', updateSwatch);
return this.palette;
} }
return ColorPalette; this.palette.on('change', updateSwatch);
});
return this.palette;
}

View File

@ -1,77 +1,75 @@
define(['./Palette'], function (Palette) { import Palette from './Palette';
//The icons that will be used to instantiate this palette if none are provided
const DEFAULT_ICONS = [ //The icons that will be used to instantiate this palette if none are provided
'icon-alert-rect', const DEFAULT_ICONS = [
'icon-alert-triangle', 'icon-alert-rect',
'icon-arrow-down', 'icon-alert-triangle',
'icon-arrow-left', 'icon-arrow-down',
'icon-arrow-right', 'icon-arrow-left',
'icon-arrow-double-up', 'icon-arrow-right',
'icon-arrow-tall-up', 'icon-arrow-double-up',
'icon-arrow-tall-down', 'icon-arrow-tall-up',
'icon-arrow-double-down', 'icon-arrow-tall-down',
'icon-arrow-up', 'icon-arrow-double-down',
'icon-asterisk', 'icon-arrow-up',
'icon-bell', 'icon-asterisk',
'icon-check', 'icon-bell',
'icon-eye-open', 'icon-check',
'icon-gear', 'icon-eye-open',
'icon-hourglass', 'icon-gear',
'icon-info', 'icon-hourglass',
'icon-link', 'icon-info',
'icon-lock', 'icon-link',
'icon-people', 'icon-lock',
'icon-person', 'icon-people',
'icon-plus', 'icon-person',
'icon-trash', 'icon-plus',
'icon-x' 'icon-trash',
]; 'icon-x'
];
/**
* Instantiates a new Open MCT Icon Palette input
* @constructor
* @param {string} cssClass The class name of the icon which should be applied
* to this palette
* @param {Element} container The view that contains this palette
* @param {string[]} icons (optional) A list of icons that should be used to instantiate this palette
*/
export default function IconPalette(cssClass, container, icons) {
this.icons = icons || DEFAULT_ICONS;
this.palette = new Palette(cssClass, container, this.icons);
this.palette.setNullOption('');
this.oldIcon = this.palette.current || '';
const domElement = this.palette.getDOM();
const self = this;
domElement.querySelector('.c-button--menu').classList.add('c-button--swatched');
domElement.querySelector('.t-swatch').classList.add('icon-swatch');
domElement.querySelector('.c-palette').classList.add('c-palette--icon');
domElement.querySelectorAll('.c-palette-item').forEach((item) => {
// eslint-disable-next-line no-invalid-this
item.classList.add(item.dataset.item);
});
/** /**
* Instantiates a new Open MCT Icon Palette input * Update this palette's current selection indicator with the style
* @constructor * of the currently selected item
* @param {string} cssClass The class name of the icon which should be applied * @private
* to this palette
* @param {Element} container The view that contains this palette
* @param {string[]} icons (optional) A list of icons that should be used to instantiate this palette
*/ */
function IconPalette(cssClass, container, icons) { function updateSwatch() {
this.icons = icons || DEFAULT_ICONS; if (self.oldIcon) {
this.palette = new Palette(cssClass, container, this.icons); domElement.querySelector('.icon-swatch').classList.remove(self.oldIcon);
this.palette.setNullOption('');
this.oldIcon = this.palette.current || '';
const domElement = this.palette.getDOM();
const self = this;
domElement.querySelector('.c-button--menu').classList.add('c-button--swatched');
domElement.querySelector('.t-swatch').classList.add('icon-swatch');
domElement.querySelector('.c-palette').classList.add('c-palette--icon');
domElement.querySelectorAll('.c-palette-item').forEach((item) => {
// eslint-disable-next-line no-invalid-this
item.classList.add(item.dataset.item);
});
/**
* Update this palette's current selection indicator with the style
* of the currently selected item
* @private
*/
function updateSwatch() {
if (self.oldIcon) {
domElement.querySelector('.icon-swatch').classList.remove(self.oldIcon);
}
domElement.querySelector('.icon-swatch').classList.add(self.palette.getCurrent());
self.oldIcon = self.palette.getCurrent();
} }
this.palette.on('change', updateSwatch); domElement.querySelector('.icon-swatch').classList.add(self.palette.getCurrent());
self.oldIcon = self.palette.getCurrent();
return this.palette;
} }
return IconPalette; this.palette.on('change', updateSwatch);
});
return this.palette;
}

View File

@ -1,95 +1,93 @@
define(['./Select'], function (Select) { import Select from './Select';
/**
* Create a {Select} element whose composition is dynamically updated with
* the telemetry fields of a particular domain object
* @constructor
* @param {Object} config The current state of this select. Must have object
* and key fields
* @param {ObjectSelect} objectSelect The linked ObjectSelect instance to which
* this KeySelect should listen to for change
* events
* @param {ConditionManager} manager A ConditionManager instance from which
* to receive telemetry metadata
* @param {function} changeCallback A change event callback to register with this
* select on initialization
*/
const NULLVALUE = '- Select Field -';
function KeySelect(config, objectSelect, manager, changeCallback) { /**
const self = this; * Create a {Select} element whose composition is dynamically updated with
* the telemetry fields of a particular domain object
* @constructor
* @param {Object} config The current state of this select. Must have object
* and key fields
* @param {ObjectSelect} objectSelect The linked ObjectSelect instance to which
* this KeySelect should listen to for change
* events
* @param {ConditionManager} manager A ConditionManager instance from which
* to receive telemetry metadata
* @param {function} changeCallback A change event callback to register with this
* select on initialization
*/
const NULLVALUE = '- Select Field -';
this.config = config; export default function KeySelect(config, objectSelect, manager, changeCallback) {
this.objectSelect = objectSelect; const self = this;
this.manager = manager;
this.select = new Select(); this.config = config;
this.select.hide(); this.objectSelect = objectSelect;
this.select.addOption('', NULLVALUE); this.manager = manager;
if (changeCallback) {
this.select.on('change', changeCallback);
}
/** this.select = new Select();
* Change event handler for the {ObjectSelect} to which this KeySelect instance this.select.hide();
* is linked. Loads the new object's metadata and updates its select element's this.select.addOption('', NULLVALUE);
* composition. if (changeCallback) {
* @param {Object} key The key identifying the newly selected domain object this.select.on('change', changeCallback);
* @private
*/
function onObjectChange(key) {
const selected = self.manager.metadataLoadCompleted()
? self.select.getSelected()
: self.config.key;
self.telemetryMetadata = self.manager.getTelemetryMetadata(key) || {};
self.generateOptions();
self.select.setSelected(selected);
}
/**
* Event handler for the initial metadata load event from the associated
* ConditionManager. Retrieves metadata from the manager and populates
* the select element.
* @private
*/
function onMetadataLoad() {
if (self.manager.getTelemetryMetadata(self.config.object)) {
self.telemetryMetadata = self.manager.getTelemetryMetadata(self.config.object);
self.generateOptions();
}
self.select.setSelected(self.config.key);
}
if (self.manager.metadataLoadCompleted()) {
onMetadataLoad();
}
this.objectSelect.on('change', onObjectChange, this);
this.manager.on('metadata', onMetadataLoad);
return this.select;
} }
/** /**
* Populate this select with options based on its current composition * Change event handler for the {ObjectSelect} to which this KeySelect instance
* is linked. Loads the new object's metadata and updates its select element's
* composition.
* @param {Object} key The key identifying the newly selected domain object
* @private
*/ */
KeySelect.prototype.generateOptions = function () { function onObjectChange(key) {
const items = Object.entries(this.telemetryMetadata).map(function (metaDatum) { const selected = self.manager.metadataLoadCompleted()
return [metaDatum[0], metaDatum[1].name]; ? self.select.getSelected()
}); : self.config.key;
items.splice(0, 0, ['', NULLVALUE]); self.telemetryMetadata = self.manager.getTelemetryMetadata(key) || {};
this.select.setOptions(items); self.generateOptions();
self.select.setSelected(selected);
}
if (this.select.options.length < 2) { /**
this.select.hide(); * Event handler for the initial metadata load event from the associated
} else if (this.select.options.length > 1) { * ConditionManager. Retrieves metadata from the manager and populates
this.select.show(); * the select element.
* @private
*/
function onMetadataLoad() {
if (self.manager.getTelemetryMetadata(self.config.object)) {
self.telemetryMetadata = self.manager.getTelemetryMetadata(self.config.object);
self.generateOptions();
} }
};
KeySelect.prototype.destroy = function () { self.select.setSelected(self.config.key);
this.objectSelect.destroy(); }
};
return KeySelect; if (self.manager.metadataLoadCompleted()) {
}); onMetadataLoad();
}
this.objectSelect.on('change', onObjectChange, this);
this.manager.on('metadata', onMetadataLoad);
return this.select;
}
/**
* Populate this select with options based on its current composition
*/
KeySelect.prototype.generateOptions = function () {
const items = Object.entries(this.telemetryMetadata).map(function (metaDatum) {
return [metaDatum[0], metaDatum[1].name];
});
items.splice(0, 0, ['', NULLVALUE]);
this.select.setOptions(items);
if (this.select.options.length < 2) {
this.select.hide();
} else if (this.select.options.length > 1) {
this.select.show();
}
};
KeySelect.prototype.destroy = function () {
this.objectSelect.destroy();
};

View File

@ -1,86 +1,86 @@
define(['./Select', 'objectUtils'], function (Select, objectUtils) { import objectUtils from 'objectUtils';
import Select from './Select';
/**
* Create a {Select} element whose composition is dynamically updated with
* the current composition of the Summary Widget
* @constructor
* @param {Object} config The current state of this select. Must have an
* object field
* @param {ConditionManager} manager A ConditionManager instance from which
* to receive the current composition status
* @param {string[][]} baseOptions A set of [value, label] keyword pairs to
* display regardless of the composition state
*/
export default function ObjectSelect(config, manager, baseOptions) {
const self = this;
this.config = config;
this.manager = manager;
this.select = new Select();
this.baseOptions = [['', '- Select Telemetry -']];
if (baseOptions) {
this.baseOptions = this.baseOptions.concat(baseOptions);
}
this.baseOptions.forEach(function (option) {
self.select.addOption(option[0], option[1]);
});
this.compositionObjs = this.manager.getComposition();
self.generateOptions();
/** /**
* Create a {Select} element whose composition is dynamically updated with * Add a new composition object to this select when a composition added
* the current composition of the Summary Widget * is detected on the Summary Widget
* @constructor * @param {Object} obj The newly added domain object
* @param {Object} config The current state of this select. Must have an * @private
* object field
* @param {ConditionManager} manager A ConditionManager instance from which
* to receive the current composition status
* @param {string[][]} baseOptions A set of [value, label] keyword pairs to
* display regardless of the composition state
*/ */
function ObjectSelect(config, manager, baseOptions) { function onCompositionAdd(obj) {
const self = this; self.select.addOption(objectUtils.makeKeyString(obj.identifier), obj.name);
this.config = config;
this.manager = manager;
this.select = new Select();
this.baseOptions = [['', '- Select Telemetry -']];
if (baseOptions) {
this.baseOptions = this.baseOptions.concat(baseOptions);
}
this.baseOptions.forEach(function (option) {
self.select.addOption(option[0], option[1]);
});
this.compositionObjs = this.manager.getComposition();
self.generateOptions();
/**
* Add a new composition object to this select when a composition added
* is detected on the Summary Widget
* @param {Object} obj The newly added domain object
* @private
*/
function onCompositionAdd(obj) {
self.select.addOption(objectUtils.makeKeyString(obj.identifier), obj.name);
}
/**
* Refresh the composition of this select when a domain object is removed
* from the Summary Widget's composition
* @private
*/
function onCompositionRemove() {
const selected = self.select.getSelected();
self.generateOptions();
self.select.setSelected(selected);
}
/**
* Defer setting the selected state on initial load until load is complete
* @private
*/
function onCompositionLoad() {
self.select.setSelected(self.config.object);
}
this.manager.on('add', onCompositionAdd);
this.manager.on('remove', onCompositionRemove);
this.manager.on('load', onCompositionLoad);
if (this.manager.loadCompleted()) {
onCompositionLoad();
}
return this.select;
} }
/** /**
* Populate this select with options based on its current composition * Refresh the composition of this select when a domain object is removed
* from the Summary Widget's composition
* @private
*/ */
ObjectSelect.prototype.generateOptions = function () { function onCompositionRemove() {
const items = Object.values(this.compositionObjs).map(function (obj) { const selected = self.select.getSelected();
return [objectUtils.makeKeyString(obj.identifier), obj.name]; self.generateOptions();
}); self.select.setSelected(selected);
this.baseOptions.forEach(function (option, index) { }
items.splice(index, 0, option);
});
this.select.setOptions(items);
};
return ObjectSelect; /**
}); * Defer setting the selected state on initial load until load is complete
* @private
*/
function onCompositionLoad() {
self.select.setSelected(self.config.object);
}
this.manager.on('add', onCompositionAdd);
this.manager.on('remove', onCompositionRemove);
this.manager.on('load', onCompositionLoad);
if (this.manager.loadCompleted()) {
onCompositionLoad();
}
return this.select;
}
/**
* Populate this select with options based on its current composition
*/
ObjectSelect.prototype.generateOptions = function () {
const items = Object.values(this.compositionObjs).map(function (obj) {
return [objectUtils.makeKeyString(obj.identifier), obj.name];
});
this.baseOptions.forEach(function (option, index) {
items.splice(index, 0, option);
});
this.select.setOptions(items);
};

View File

@ -1,120 +1,119 @@
define(['./Select', '../eventHelpers'], function (Select, eventHelpers) { import eventHelpers from '../eventHelpers';
/** import Select from './Select';
* Create a {Select} element whose composition is dynamically updated with
* the operations applying to a particular telemetry property
* @constructor
* @param {Object} config The current state of this select. Must have object,
* key, and operation fields
* @param {KeySelect} keySelect The linked Key Select instance to which
* this OperationSelect should listen to for change
* events
* @param {ConditionManager} manager A ConditionManager instance from which
* to receive telemetry metadata
* @param {function} changeCallback A change event callback to register with this
* select on initialization
*/
const NULLVALUE = '- Select Comparison -';
function OperationSelect(config, keySelect, manager, changeCallback) { /**
eventHelpers.extend(this); * Create a {Select} element whose composition is dynamically updated with
const self = this; * the operations applying to a particular telemetry property
* @constructor
* @param {Object} config The current state of this select. Must have object,
* key, and operation fields
* @param {KeySelect} keySelect The linked Key Select instance to which
* this OperationSelect should listen to for change
* events
* @param {ConditionManager} manager A ConditionManager instance from which
* to receive telemetry metadata
* @param {function} changeCallback A change event callback to register with this
* select on initialization
*/
const NULLVALUE = '- Select Comparison -';
this.config = config; export default function OperationSelect(config, keySelect, manager, changeCallback) {
this.keySelect = keySelect; eventHelpers.extend(this);
this.manager = manager; const self = this;
this.operationKeys = []; this.config = config;
this.evaluator = this.manager.getEvaluator(); this.keySelect = keySelect;
this.loadComplete = false; this.manager = manager;
this.select = new Select(); this.operationKeys = [];
this.select.hide(); this.evaluator = this.manager.getEvaluator();
this.select.addOption('', NULLVALUE); this.loadComplete = false;
if (changeCallback) {
this.listenTo(this.select, 'change', changeCallback);
}
/** this.select = new Select();
* Change event handler for the {KeySelect} to which this OperationSelect instance this.select.hide();
* is linked. Loads the operations applicable to the given telemetry property and updates this.select.addOption('', NULLVALUE);
* its select element's composition if (changeCallback) {
* @param {Object} key The key identifying the newly selected property this.listenTo(this.select, 'change', changeCallback);
* @private
*/
function onKeyChange(key) {
const selected = self.config.operation;
if (self.manager.metadataLoadCompleted()) {
self.loadOptions(key);
self.generateOptions();
self.select.setSelected(selected);
}
}
/**
* Event handler for the initial metadata load event from the associated
* ConditionManager. Retrieves telemetry property types and updates the
* select
* @private
*/
function onMetadataLoad() {
if (self.manager.getTelemetryPropertyType(self.config.object, self.config.key)) {
self.loadOptions(self.config.key);
self.generateOptions();
}
self.select.setSelected(self.config.operation);
}
this.keySelect.on('change', onKeyChange);
this.manager.on('metadata', onMetadataLoad);
if (this.manager.metadataLoadCompleted()) {
onMetadataLoad();
}
return this.select;
} }
/** /**
* Populate this select with options based on its current composition * Change event handler for the {KeySelect} to which this OperationSelect instance
* is linked. Loads the operations applicable to the given telemetry property and updates
* its select element's composition
* @param {Object} key The key identifying the newly selected property
* @private
*/ */
OperationSelect.prototype.generateOptions = function () { function onKeyChange(key) {
const self = this; const selected = self.config.operation;
const items = this.operationKeys.map(function (operation) { if (self.manager.metadataLoadCompleted()) {
return [operation, self.evaluator.getOperationText(operation)]; self.loadOptions(key);
}); self.generateOptions();
items.splice(0, 0, ['', NULLVALUE]); self.select.setSelected(selected);
this.select.setOptions(items);
if (this.select.options.length < 2) {
this.select.hide();
} else {
this.select.show();
} }
}; }
/** /**
* Retrieve the data type associated with a given telemetry property and * Event handler for the initial metadata load event from the associated
* the applicable operations from the {ConditionEvaluator} * ConditionManager. Retrieves telemetry property types and updates the
* @param {string} key The telemetry property to load operations for * select
* @private
*/ */
OperationSelect.prototype.loadOptions = function (key) { function onMetadataLoad() {
const self = this; if (self.manager.getTelemetryPropertyType(self.config.object, self.config.key)) {
const operations = self.evaluator.getOperationKeys(); self.loadOptions(self.config.key);
let type; self.generateOptions();
type = self.manager.getTelemetryPropertyType(self.config.object, key);
if (type !== undefined) {
self.operationKeys = operations.filter(function (operation) {
return self.evaluator.operationAppliesTo(operation, type);
});
} }
};
OperationSelect.prototype.destroy = function () { self.select.setSelected(self.config.operation);
this.stopListening(); }
};
return OperationSelect; this.keySelect.on('change', onKeyChange);
}); this.manager.on('metadata', onMetadataLoad);
if (this.manager.metadataLoadCompleted()) {
onMetadataLoad();
}
return this.select;
}
/**
* Populate this select with options based on its current composition
*/
OperationSelect.prototype.generateOptions = function () {
const self = this;
const items = this.operationKeys.map(function (operation) {
return [operation, self.evaluator.getOperationText(operation)];
});
items.splice(0, 0, ['', NULLVALUE]);
this.select.setOptions(items);
if (this.select.options.length < 2) {
this.select.hide();
} else {
this.select.show();
}
};
/**
* Retrieve the data type associated with a given telemetry property and
* the applicable operations from the {ConditionEvaluator}
* @param {string} key The telemetry property to load operations for
*/
OperationSelect.prototype.loadOptions = function (key) {
const self = this;
const operations = self.evaluator.getOperationKeys();
let type;
type = self.manager.getTelemetryPropertyType(self.config.object, key);
if (type !== undefined) {
self.operationKeys = operations.filter(function (operation) {
return self.evaluator.operationAppliesTo(operation, type);
});
}
};
OperationSelect.prototype.destroy = function () {
this.stopListening();
};

View File

@ -1,184 +1,181 @@
define([ import EventEmitter from 'EventEmitter';
'../eventHelpers',
'../../res/input/paletteTemplate.html',
'../../../../utils/template/templateHelpers',
'EventEmitter'
], function (eventHelpers, paletteTemplate, templateHelpers, EventEmitter) {
/**
* Instantiates a new Open MCT Color Palette input
* @constructor
* @param {string} cssClass The class name of the icon which should be applied
* to this palette
* @param {Element} container The view that contains this palette
* @param {string[]} items A list of data items that will be associated with each
* palette item in the view; how this data is represented is
* up to the descendent class
*/
function Palette(cssClass, container, items) {
eventHelpers.extend(this);
const self = this; import * as templateHelpers from '../../../../utils/template/templateHelpers';
import paletteTemplate from '../../res/input/paletteTemplate.html';
import eventHelpers from '../eventHelpers';
this.cssClass = cssClass; /**
this.items = items; * Instantiates a new Open MCT Color Palette input
this.container = container; * @constructor
* @param {string} cssClass The class name of the icon which should be applied
* to this palette
* @param {Element} container The view that contains this palette
* @param {string[]} items A list of data items that will be associated with each
* palette item in the view; how this data is represented is
* up to the descendent class
*/
export default function Palette(cssClass, container, items) {
eventHelpers.extend(this);
this.domElement = templateHelpers.convertTemplateToHTML(paletteTemplate)[0]; const self = this;
this.itemElements = { this.cssClass = cssClass;
nullOption: this.domElement.querySelector('.c-palette__item-none .c-palette__item') this.items = items;
}; this.container = container;
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['change'];
this.value = this.items[0];
this.nullOption = ' ';
this.button = this.domElement.querySelector('.js-button');
this.menu = this.domElement.querySelector('.c-menu');
this.hideMenu = this.hideMenu.bind(this); this.domElement = templateHelpers.convertTemplateToHTML(paletteTemplate)[0];
if (this.cssClass) { this.itemElements = {
self.button.classList.add(this.cssClass); nullOption: this.domElement.querySelector('.c-palette__item-none .c-palette__item')
} };
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['change'];
this.value = this.items[0];
this.nullOption = ' ';
this.button = this.domElement.querySelector('.js-button');
this.menu = this.domElement.querySelector('.c-menu');
self.setNullOption(this.nullOption); this.hideMenu = this.hideMenu.bind(this);
self.items.forEach(function (item) { if (this.cssClass) {
const itemElement = document.createElement('div'); self.button.classList.add(this.cssClass);
itemElement.className = 'c-palette__item ' + item;
itemElement.setAttribute('data-item', item);
self.itemElements[item] = itemElement;
self.domElement.querySelector('.c-palette__items').appendChild(itemElement);
});
self.domElement.querySelector('.c-menu').style.display = 'none';
this.listenTo(window.document, 'click', this.hideMenu);
this.listenTo(self.domElement.querySelector('.js-button'), 'click', function (event) {
event.stopPropagation();
self.container.querySelector('.c-menu').style.display = 'none';
self.domElement.querySelector('.c-menu').style.display = '';
});
/**
* Event handler for selection of an individual palette item. Sets the
* currently selected element to be the one associated with that item's data
* @param {Event} event the click event that initiated this callback
* @private
*/
function handleItemClick(event) {
const elem = event.currentTarget;
const item = elem.dataset.item;
self.set(item);
self.domElement.querySelector('.c-menu').style.display = 'none';
}
self.domElement.querySelectorAll('.c-palette__item').forEach((item) => {
this.listenTo(item, 'click', handleItemClick);
});
} }
/** self.setNullOption(this.nullOption);
* Get the DOM element representing this palette in the view
*/ self.items.forEach(function (item) {
Palette.prototype.getDOM = function () { const itemElement = document.createElement('div');
return this.domElement; itemElement.className = 'c-palette__item ' + item;
}; itemElement.setAttribute('data-item', item);
self.itemElements[item] = itemElement;
self.domElement.querySelector('.c-palette__items').appendChild(itemElement);
});
self.domElement.querySelector('.c-menu').style.display = 'none';
this.listenTo(window.document, 'click', this.hideMenu);
this.listenTo(self.domElement.querySelector('.js-button'), 'click', function (event) {
event.stopPropagation();
self.container.querySelector('.c-menu').style.display = 'none';
self.domElement.querySelector('.c-menu').style.display = '';
});
/** /**
* Clean up any event listeners registered to DOM elements external to the widget * Event handler for selection of an individual palette item. Sets the
* currently selected element to be the one associated with that item's data
* @param {Event} event the click event that initiated this callback
* @private
*/ */
Palette.prototype.destroy = function () { function handleItemClick(event) {
this.stopListening(); const elem = event.currentTarget;
}; const item = elem.dataset.item;
self.set(item);
self.domElement.querySelector('.c-menu').style.display = 'none';
}
Palette.prototype.hideMenu = function () { self.domElement.querySelectorAll('.c-palette__item').forEach((item) => {
this.domElement.querySelector('.c-menu').style.display = 'none'; this.listenTo(item, 'click', handleItemClick);
}; });
}
/** /**
* Register a callback with this palette: supported callback is change * Get the DOM element representing this palette in the view
* @param {string} event The key for the event to listen to */
* @param {function} callback The function that this rule will invoke on this event Palette.prototype.getDOM = function () {
* @param {Object} context A reference to a scope to use as the context for return this.domElement;
* context for the callback function };
*/
Palette.prototype.on = function (event, callback, context) { /**
if (this.supportedCallbacks.includes(event)) { * Clean up any event listeners registered to DOM elements external to the widget
this.eventEmitter.on(event, callback, context || this); */
Palette.prototype.destroy = function () {
this.stopListening();
};
Palette.prototype.hideMenu = function () {
this.domElement.querySelector('.c-menu').style.display = 'none';
};
/**
* Register a callback with this palette: supported callback is change
* @param {string} event The key for the event to listen to
* @param {function} callback The function that this rule will invoke on this event
* @param {Object} context A reference to a scope to use as the context for
* context for the callback function
*/
Palette.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.on(event, callback, context || this);
} else {
throw new Error('Unsupported event type: ' + event);
}
};
/**
* Get the currently selected value of this palette
* @return {string} The selected value
*/
Palette.prototype.getCurrent = function () {
return this.value;
};
/**
* Set the selected value of this palette; if the item doesn't exist in the
* palette's data model, the selected value will not change. Invokes any
* change callbacks associated with this palette.
* @param {string} item The key of the item to set as selected
*/
Palette.prototype.set = function (item) {
const self = this;
if (this.items.includes(item) || item === this.nullOption) {
this.value = item;
if (item === this.nullOption) {
this.updateSelected('nullOption');
} else { } else {
throw new Error('Unsupported event type: ' + event); this.updateSelected(item);
} }
}; }
/** this.eventEmitter.emit('change', self.value);
* Get the currently selected value of this palette };
* @return {string} The selected value
*/
Palette.prototype.getCurrent = function () {
return this.value;
};
/** /**
* Set the selected value of this palette; if the item doesn't exist in the * Update the view associated with the currently selected item
* palette's data model, the selected value will not change. Invokes any */
* change callbacks associated with this palette. Palette.prototype.updateSelected = function (item) {
* @param {string} item The key of the item to set as selected this.domElement.querySelectorAll('.c-palette__item').forEach((paletteItem) => {
*/ if (paletteItem.classList.contains('is-selected')) {
Palette.prototype.set = function (item) { paletteItem.classList.remove('is-selected');
const self = this;
if (this.items.includes(item) || item === this.nullOption) {
this.value = item;
if (item === this.nullOption) {
this.updateSelected('nullOption');
} else {
this.updateSelected(item);
}
} }
});
this.itemElements[item].classList.add('is-selected');
if (item === 'nullOption') {
this.domElement.querySelector('.t-swatch').classList.add('no-selection');
} else {
this.domElement.querySelector('.t-swatch').classList.remove('no-selection');
}
};
this.eventEmitter.emit('change', self.value); /**
}; * set the property to be used for the 'no selection' item. If not set, this
* defaults to a single space
* @param {string} item The key to use as the 'no selection' item
*/
Palette.prototype.setNullOption = function (item) {
this.nullOption = item;
this.itemElements.nullOption.data = { item: item };
};
/** /**
* Update the view associated with the currently selected item * Hides the 'no selection' option to be hidden in the view if it doesn't apply
*/ */
Palette.prototype.updateSelected = function (item) { Palette.prototype.toggleNullOption = function () {
this.domElement.querySelectorAll('.c-palette__item').forEach((paletteItem) => { const elem = this.domElement.querySelector('.c-palette__item-none');
if (paletteItem.classList.contains('is-selected')) {
paletteItem.classList.remove('is-selected');
}
});
this.itemElements[item].classList.add('is-selected');
if (item === 'nullOption') {
this.domElement.querySelector('.t-swatch').classList.add('no-selection');
} else {
this.domElement.querySelector('.t-swatch').classList.remove('no-selection');
}
};
/** if (elem.style.display === 'none') {
* set the property to be used for the 'no selection' item. If not set, this this.domElement.querySelector('.c-palette__item-none').style.display = 'flex';
* defaults to a single space } else {
* @param {string} item The key to use as the 'no selection' item this.domElement.querySelector('.c-palette__item-none').style.display = 'none';
*/ }
Palette.prototype.setNullOption = function (item) { };
this.nullOption = item;
this.itemElements.nullOption.data = { item: item };
};
/**
* Hides the 'no selection' option to be hidden in the view if it doesn't apply
*/
Palette.prototype.toggleNullOption = function () {
const elem = this.domElement.querySelector('.c-palette__item-none');
if (elem.style.display === 'none') {
this.domElement.querySelector('.c-palette__item-none').style.display = 'flex';
} else {
this.domElement.querySelector('.c-palette__item-none').style.display = 'none';
}
};
return Palette;
});

View File

@ -1,168 +1,165 @@
define([ import EventEmitter from 'EventEmitter';
'../eventHelpers',
'../../res/input/selectTemplate.html', import * as templateHelpers from '../../../../utils/template/templateHelpers';
'../../../../utils/template/templateHelpers', import selectTemplate from '../../res/input/selectTemplate.html';
'EventEmitter' import eventHelpers from '../eventHelpers';
], function (eventHelpers, selectTemplate, templateHelpers, EventEmitter) {
/**
* Wraps an HTML select element, and provides methods for dynamically altering
* its composition from the data model
* @constructor
*/
export default function Select() {
eventHelpers.extend(this);
const self = this;
this.domElement = templateHelpers.convertTemplateToHTML(selectTemplate)[0];
this.options = [];
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['change'];
this.populate();
/** /**
* Wraps an HTML select element, and provides methods for dynamically altering * Event handler for the wrapped select element. Also invokes any change
* its composition from the data model * callbacks registered with this select with the new value
* @constructor * @param {Event} event The change event that triggered this callback
* @private
*/ */
function Select() { function onChange(event) {
eventHelpers.extend(this); const elem = event.target;
const value = self.options[elem.selectedIndex];
const self = this; self.eventEmitter.emit('change', value[0]);
this.domElement = templateHelpers.convertTemplateToHTML(selectTemplate)[0];
this.options = [];
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['change'];
this.populate();
/**
* Event handler for the wrapped select element. Also invokes any change
* callbacks registered with this select with the new value
* @param {Event} event The change event that triggered this callback
* @private
*/
function onChange(event) {
const elem = event.target;
const value = self.options[elem.selectedIndex];
self.eventEmitter.emit('change', value[0]);
}
this.listenTo(this.domElement.querySelector('select'), 'change', onChange, this);
} }
/** this.listenTo(this.domElement.querySelector('select'), 'change', onChange, this);
* Get the DOM element representing this Select in the view }
* @return {Element}
*/
Select.prototype.getDOM = function () {
return this.domElement;
};
/** /**
* Register a callback with this select: supported callback is change * Get the DOM element representing this Select in the view
* @param {string} event The key for the event to listen to * @return {Element}
* @param {function} callback The function that this rule will invoke on this event */
* @param {Object} context A reference to a scope to use as the context for Select.prototype.getDOM = function () {
* context for the callback function return this.domElement;
*/ };
Select.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) { /**
this.eventEmitter.on(event, callback, context || this); * Register a callback with this select: supported callback is change
} else { * @param {string} event The key for the event to listen to
throw new Error('Unsupported event type' + event); * @param {function} callback The function that this rule will invoke on this event
* @param {Object} context A reference to a scope to use as the context for
* context for the callback function
*/
Select.prototype.on = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.on(event, callback, context || this);
} else {
throw new Error('Unsupported event type' + event);
}
};
/**
* Unregister a callback from this select.
* @param {string} event The key for the event to stop listening to
* @param {function} callback The function to unregister
* @param {Object} context A reference to a scope to use as the context for the callback function
*/
Select.prototype.off = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.off(event, callback, context || this);
} else {
throw new Error('Unsupported event type: ' + event);
}
};
/**
* Update the select element in the view from the current state of the data
* model
*/
Select.prototype.populate = function () {
const self = this;
let selectedIndex = 0;
selectedIndex = this.domElement.querySelector('select').selectedIndex;
this.domElement.querySelector('select').innerHTML = '';
self.options.forEach(function (option) {
const optionElement = document.createElement('option');
optionElement.value = option[0];
optionElement.innerText = `+ ${option[1]}`;
self.domElement.querySelector('select').appendChild(optionElement);
});
this.domElement.querySelector('select').selectedIndex = selectedIndex;
};
/**
* Add a single option to this select
* @param {string} value The value for the new option
* @param {string} label The human-readable text for the new option
*/
Select.prototype.addOption = function (value, label) {
this.options.push([value, label]);
this.populate();
};
/**
* Set the available options for this select. Replaces any existing options
* @param {string[][]} options An array of [value, label] pairs to display
*/
Select.prototype.setOptions = function (options) {
this.options = options;
this.populate();
};
/**
* Sets the currently selected element an invokes any registered change
* callbacks with the new value. If the value doesn't exist in this select's
* model, its state will not change.
* @param {string} value The value to set as the selected option
*/
Select.prototype.setSelected = function (value) {
let selectedIndex = 0;
let selectedOption;
this.options.forEach(function (option, index) {
if (option[0] === value) {
selectedIndex = index;
} }
}; });
this.domElement.querySelector('select').selectedIndex = selectedIndex;
/** selectedOption = this.options[selectedIndex];
* Unregister a callback from this select. this.eventEmitter.emit('change', selectedOption[0]);
* @param {string} event The key for the event to stop listening to };
* @param {function} callback The function to unregister
* @param {Object} context A reference to a scope to use as the context for the callback function
*/
Select.prototype.off = function (event, callback, context) {
if (this.supportedCallbacks.includes(event)) {
this.eventEmitter.off(event, callback, context || this);
} else {
throw new Error('Unsupported event type: ' + event);
}
};
/** /**
* Update the select element in the view from the current state of the data * Get the value of the currently selected item
* model * @return {string}
*/ */
Select.prototype.populate = function () { Select.prototype.getSelected = function () {
const self = this; return this.domElement.querySelector('select').value;
let selectedIndex = 0; };
selectedIndex = this.domElement.querySelector('select').selectedIndex; Select.prototype.hide = function () {
this.domElement.classList.add('hidden');
if (this.domElement.querySelector('.equal-to')) {
this.domElement.querySelector('.equal-to').classList.add('hidden');
}
};
this.domElement.querySelector('select').innerHTML = ''; Select.prototype.show = function () {
this.domElement.classList.remove('hidden');
if (this.domElement.querySelector('.equal-to')) {
this.domElement.querySelector('.equal-to').classList.remove('hidden');
}
};
self.options.forEach(function (option) { Select.prototype.destroy = function () {
const optionElement = document.createElement('option'); this.stopListening();
optionElement.value = option[0]; };
optionElement.innerText = `+ ${option[1]}`;
self.domElement.querySelector('select').appendChild(optionElement);
});
this.domElement.querySelector('select').selectedIndex = selectedIndex;
};
/**
* Add a single option to this select
* @param {string} value The value for the new option
* @param {string} label The human-readable text for the new option
*/
Select.prototype.addOption = function (value, label) {
this.options.push([value, label]);
this.populate();
};
/**
* Set the available options for this select. Replaces any existing options
* @param {string[][]} options An array of [value, label] pairs to display
*/
Select.prototype.setOptions = function (options) {
this.options = options;
this.populate();
};
/**
* Sets the currently selected element an invokes any registered change
* callbacks with the new value. If the value doesn't exist in this select's
* model, its state will not change.
* @param {string} value The value to set as the selected option
*/
Select.prototype.setSelected = function (value) {
let selectedIndex = 0;
let selectedOption;
this.options.forEach(function (option, index) {
if (option[0] === value) {
selectedIndex = index;
}
});
this.domElement.querySelector('select').selectedIndex = selectedIndex;
selectedOption = this.options[selectedIndex];
this.eventEmitter.emit('change', selectedOption[0]);
};
/**
* Get the value of the currently selected item
* @return {string}
*/
Select.prototype.getSelected = function () {
return this.domElement.querySelector('select').value;
};
Select.prototype.hide = function () {
this.domElement.classList.add('hidden');
if (this.domElement.querySelector('.equal-to')) {
this.domElement.querySelector('.equal-to').classList.add('hidden');
}
};
Select.prototype.show = function () {
this.domElement.classList.remove('hidden');
if (this.domElement.querySelector('.equal-to')) {
this.domElement.querySelector('.equal-to').classList.remove('hidden');
}
};
Select.prototype.destroy = function () {
this.stopListening();
};
return Select;
});

View File

@ -20,40 +20,40 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./SummaryWidgetEvaluator', 'objectUtils'], function (SummaryWidgetEvaluator, objectUtils) { import objectUtils from 'objectUtils';
function EvaluatorPool(openmct) {
this.openmct = openmct; import SummaryWidgetEvaluator from './SummaryWidgetEvaluator';
this.byObjectId = {};
this.byEvaluator = new WeakMap(); export default function EvaluatorPool(openmct) {
this.openmct = openmct;
this.byObjectId = {};
this.byEvaluator = new WeakMap();
}
EvaluatorPool.prototype.get = function (domainObject) {
const objectId = objectUtils.makeKeyString(domainObject.identifier);
let poolEntry = this.byObjectId[objectId];
if (!poolEntry) {
poolEntry = {
leases: 0,
objectId: objectId,
evaluator: new SummaryWidgetEvaluator(domainObject, this.openmct)
};
this.byEvaluator.set(poolEntry.evaluator, poolEntry);
this.byObjectId[objectId] = poolEntry;
} }
EvaluatorPool.prototype.get = function (domainObject) { poolEntry.leases += 1;
const objectId = objectUtils.makeKeyString(domainObject.identifier);
let poolEntry = this.byObjectId[objectId];
if (!poolEntry) {
poolEntry = {
leases: 0,
objectId: objectId,
evaluator: new SummaryWidgetEvaluator(domainObject, this.openmct)
};
this.byEvaluator.set(poolEntry.evaluator, poolEntry);
this.byObjectId[objectId] = poolEntry;
}
poolEntry.leases += 1; return poolEntry.evaluator;
};
return poolEntry.evaluator; EvaluatorPool.prototype.release = function (evaluator) {
}; const poolEntry = this.byEvaluator.get(evaluator);
poolEntry.leases -= 1;
EvaluatorPool.prototype.release = function (evaluator) { if (poolEntry.leases === 0) {
const poolEntry = this.byEvaluator.get(evaluator); evaluator.destroy();
poolEntry.leases -= 1; this.byEvaluator.delete(evaluator);
if (poolEntry.leases === 0) { delete this.byObjectId[poolEntry.objectId];
evaluator.destroy(); }
this.byEvaluator.delete(evaluator); };
delete this.byObjectId[poolEntry.objectId];
}
};
return EvaluatorPool;
});

View File

@ -20,78 +20,75 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./EvaluatorPool', './SummaryWidgetEvaluator'], function ( import EvaluatorPool from './EvaluatorPool';
EvaluatorPool,
SummaryWidgetEvaluator
) {
describe('EvaluatorPool', function () {
let pool;
let openmct;
let objectA;
let objectB;
beforeEach(function () { describe('EvaluatorPool', function () {
openmct = { let pool;
composition: jasmine.createSpyObj('compositionAPI', ['get']), let openmct;
objects: jasmine.createSpyObj('objectAPI', ['observe']) let objectA;
}; let objectB;
openmct.composition.get.and.callFake(function () {
const compositionCollection = jasmine.createSpyObj('compositionCollection', [
'load',
'on',
'off'
]);
compositionCollection.load.and.returnValue(Promise.resolve());
return compositionCollection; beforeEach(function () {
}); openmct = {
openmct.objects.observe.and.callFake(function () { composition: jasmine.createSpyObj('compositionAPI', ['get']),
return function () {}; objects: jasmine.createSpyObj('objectAPI', ['observe'])
}); };
pool = new EvaluatorPool(openmct); openmct.composition.get.and.callFake(function () {
objectA = { const compositionCollection = jasmine.createSpyObj('compositionCollection', [
identifier: { 'load',
namespace: 'someNamespace', 'on',
key: 'someKey' 'off'
}, ]);
configuration: { compositionCollection.load.and.returnValue(Promise.resolve());
ruleOrder: []
} return compositionCollection;
};
objectB = {
identifier: {
namespace: 'otherNamespace',
key: 'otherKey'
},
configuration: {
ruleOrder: []
}
};
}); });
openmct.objects.observe.and.callFake(function () {
it('returns new evaluators for different objects', function () { return function () {};
const evaluatorA = pool.get(objectA);
const evaluatorB = pool.get(objectB);
expect(evaluatorA).not.toBe(evaluatorB);
}); });
pool = new EvaluatorPool(openmct);
objectA = {
identifier: {
namespace: 'someNamespace',
key: 'someKey'
},
configuration: {
ruleOrder: []
}
};
objectB = {
identifier: {
namespace: 'otherNamespace',
key: 'otherKey'
},
configuration: {
ruleOrder: []
}
};
});
it('returns the same evaluator for the same object', function () { it('returns new evaluators for different objects', function () {
const evaluatorA = pool.get(objectA); const evaluatorA = pool.get(objectA);
const evaluatorB = pool.get(objectA); const evaluatorB = pool.get(objectB);
expect(evaluatorA).toBe(evaluatorB); expect(evaluatorA).not.toBe(evaluatorB);
});
const evaluatorC = pool.get(JSON.parse(JSON.stringify(objectA))); it('returns the same evaluator for the same object', function () {
expect(evaluatorA).toBe(evaluatorC); const evaluatorA = pool.get(objectA);
}); const evaluatorB = pool.get(objectA);
expect(evaluatorA).toBe(evaluatorB);
it('returns new evaluator when old is released', function () { const evaluatorC = pool.get(JSON.parse(JSON.stringify(objectA)));
const evaluatorA = pool.get(objectA); expect(evaluatorA).toBe(evaluatorC);
const evaluatorB = pool.get(objectA); });
expect(evaluatorA).toBe(evaluatorB);
pool.release(evaluatorA); it('returns new evaluator when old is released', function () {
pool.release(evaluatorB); const evaluatorA = pool.get(objectA);
const evaluatorC = pool.get(objectA); const evaluatorB = pool.get(objectA);
expect(evaluatorA).not.toBe(evaluatorC); expect(evaluatorA).toBe(evaluatorB);
}); pool.release(evaluatorA);
pool.release(evaluatorB);
const evaluatorC = pool.get(objectA);
expect(evaluatorA).not.toBe(evaluatorC);
}); });
}); });

View File

@ -20,57 +20,55 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./operations'], function (OPERATIONS) { import OPERATIONS from './operations';
function SummaryWidgetCondition(definition) {
this.object = definition.object;
this.key = definition.key;
this.values = definition.values;
if (!definition.operation) {
// TODO: better handling for default rule.
this.evaluate = function () {
return true;
};
} else {
this.comparator = OPERATIONS[definition.operation].operation;
}
}
SummaryWidgetCondition.prototype.evaluate = function (telemetryState) {
const stateKeys = Object.keys(telemetryState);
let state;
let result;
let i;
if (this.object === 'any') {
for (i = 0; i < stateKeys.length; i++) {
state = telemetryState[stateKeys[i]];
result = this.evaluateState(state);
if (result) {
return true;
}
}
return false;
} else if (this.object === 'all') {
for (i = 0; i < stateKeys.length; i++) {
state = telemetryState[stateKeys[i]];
result = this.evaluateState(state);
if (!result) {
return false;
}
}
export default function SummaryWidgetCondition(definition) {
this.object = definition.object;
this.key = definition.key;
this.values = definition.values;
if (!definition.operation) {
// TODO: better handling for default rule.
this.evaluate = function () {
return true; return true;
} else { };
return this.evaluateState(telemetryState[this.object]); } else {
this.comparator = OPERATIONS[definition.operation].operation;
}
}
SummaryWidgetCondition.prototype.evaluate = function (telemetryState) {
const stateKeys = Object.keys(telemetryState);
let state;
let result;
let i;
if (this.object === 'any') {
for (i = 0; i < stateKeys.length; i++) {
state = telemetryState[stateKeys[i]];
result = this.evaluateState(state);
if (result) {
return true;
}
} }
};
SummaryWidgetCondition.prototype.evaluateState = function (state) { return false;
const testValues = [state.formats[this.key].parse(state.lastDatum)].concat(this.values); } else if (this.object === 'all') {
for (i = 0; i < stateKeys.length; i++) {
state = telemetryState[stateKeys[i]];
result = this.evaluateState(state);
if (!result) {
return false;
}
}
return this.comparator(testValues); return true;
}; } else {
return this.evaluateState(telemetryState[this.object]);
}
};
return SummaryWidgetCondition; SummaryWidgetCondition.prototype.evaluateState = function (state) {
}); const testValues = [state.formats[this.key].parse(state.lastDatum)].concat(this.values);
return this.comparator(testValues);
};

View File

@ -20,106 +20,106 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./SummaryWidgetCondition'], function (SummaryWidgetCondition) { import SummaryWidgetCondition from './SummaryWidgetCondition';
describe('SummaryWidgetCondition', function () {
let condition;
let telemetryState;
beforeEach(function () { describe('SummaryWidgetCondition', function () {
// Format map intentionally uses different keys than those present let condition;
// in datum, which serves to verify conditions use format map to get let telemetryState;
// data.
const formatMap = { beforeEach(function () {
adjusted: { // Format map intentionally uses different keys than those present
parse: function (datum) { // in datum, which serves to verify conditions use format map to get
return datum.value + 10; // data.
} const formatMap = {
}, adjusted: {
raw: { parse: function (datum) {
parse: function (datum) { return datum.value + 10;
return datum.value;
}
} }
}; },
raw: {
telemetryState = { parse: function (datum) {
objectId: { return datum.value;
formats: formatMap,
lastDatum: {}
},
otherObjectId: {
formats: formatMap,
lastDatum: {}
} }
}; }
}); };
it('can evaluate if a single object matches', function () { telemetryState = {
condition = new SummaryWidgetCondition({ objectId: {
object: 'objectId', formats: formatMap,
key: 'raw', lastDatum: {}
operation: 'greaterThan', },
values: [10] otherObjectId: {
}); formats: formatMap,
telemetryState.objectId.lastDatum.value = 5; lastDatum: {}
expect(condition.evaluate(telemetryState)).toBe(false); }
telemetryState.objectId.lastDatum.value = 15; };
expect(condition.evaluate(telemetryState)).toBe(true); });
});
it('can evaluate if a single object matches (alternate keys)', function () { it('can evaluate if a single object matches', function () {
condition = new SummaryWidgetCondition({ condition = new SummaryWidgetCondition({
object: 'objectId', object: 'objectId',
key: 'adjusted', key: 'raw',
operation: 'greaterThan', operation: 'greaterThan',
values: [10] values: [10]
});
telemetryState.objectId.lastDatum.value = -5;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 5;
expect(condition.evaluate(telemetryState)).toBe(true);
}); });
telemetryState.objectId.lastDatum.value = 5;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 15;
expect(condition.evaluate(telemetryState)).toBe(true);
});
it('can evaluate "if all objects match"', function () { it('can evaluate if a single object matches (alternate keys)', function () {
condition = new SummaryWidgetCondition({ condition = new SummaryWidgetCondition({
object: 'all', object: 'objectId',
key: 'raw', key: 'adjusted',
operation: 'greaterThan', operation: 'greaterThan',
values: [10] values: [10]
});
telemetryState.objectId.lastDatum.value = 0;
telemetryState.otherObjectId.lastDatum.value = 0;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 0;
telemetryState.otherObjectId.lastDatum.value = 15;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 0;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 15;
expect(condition.evaluate(telemetryState)).toBe(true);
}); });
telemetryState.objectId.lastDatum.value = -5;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 5;
expect(condition.evaluate(telemetryState)).toBe(true);
});
it('can evaluate "if any object matches"', function () { it('can evaluate "if all objects match"', function () {
condition = new SummaryWidgetCondition({ condition = new SummaryWidgetCondition({
object: 'any', object: 'all',
key: 'raw', key: 'raw',
operation: 'greaterThan', operation: 'greaterThan',
values: [10] values: [10]
});
telemetryState.objectId.lastDatum.value = 0;
telemetryState.otherObjectId.lastDatum.value = 0;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 0;
telemetryState.otherObjectId.lastDatum.value = 15;
expect(condition.evaluate(telemetryState)).toBe(true);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 0;
expect(condition.evaluate(telemetryState)).toBe(true);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 15;
expect(condition.evaluate(telemetryState)).toBe(true);
}); });
telemetryState.objectId.lastDatum.value = 0;
telemetryState.otherObjectId.lastDatum.value = 0;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 0;
telemetryState.otherObjectId.lastDatum.value = 15;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 0;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 15;
expect(condition.evaluate(telemetryState)).toBe(true);
});
it('can evaluate "if any object matches"', function () {
condition = new SummaryWidgetCondition({
object: 'any',
key: 'raw',
operation: 'greaterThan',
values: [10]
});
telemetryState.objectId.lastDatum.value = 0;
telemetryState.otherObjectId.lastDatum.value = 0;
expect(condition.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 0;
telemetryState.otherObjectId.lastDatum.value = 15;
expect(condition.evaluate(telemetryState)).toBe(true);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 0;
expect(condition.evaluate(telemetryState)).toBe(true);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 15;
expect(condition.evaluate(telemetryState)).toBe(true);
}); });
}); });

View File

@ -20,251 +20,244 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./SummaryWidgetRule', '../eventHelpers', 'objectUtils', 'lodash'], function ( import _ from 'lodash';
SummaryWidgetRule, import objectUtils from 'objectUtils';
eventHelpers,
objectUtils,
_
) {
/**
* evaluates rules defined in a summary widget against either lad or
* realtime state.
*
*/
function SummaryWidgetEvaluator(domainObject, openmct) {
this.openmct = openmct;
this.baseState = {};
this.updateRules(domainObject); import eventHelpers from '../eventHelpers';
this.removeObserver = openmct.objects.observe(domainObject, '*', this.updateRules.bind(this)); import SummaryWidgetRule from './SummaryWidgetRule';
const composition = openmct.composition.get(domainObject); /**
* evaluates rules defined in a summary widget against either lad or
* realtime state.
*
*/
export default function SummaryWidgetEvaluator(domainObject, openmct) {
this.openmct = openmct;
this.baseState = {};
this.listenTo(composition, 'add', this.addChild, this); this.updateRules(domainObject);
this.listenTo(composition, 'remove', this.removeChild, this); this.removeObserver = openmct.objects.observe(domainObject, '*', this.updateRules.bind(this));
this.loadPromise = composition.load(); const composition = openmct.composition.get(domainObject);
this.listenTo(composition, 'add', this.addChild, this);
this.listenTo(composition, 'remove', this.removeChild, this);
this.loadPromise = composition.load();
}
eventHelpers.extend(SummaryWidgetEvaluator.prototype);
/**
* Subscribes to realtime telemetry for the given summary widget.
*/
SummaryWidgetEvaluator.prototype.subscribe = function (callback) {
let active = true;
let unsubscribes = [];
this.getBaseStateClone().then(
function (realtimeStates) {
if (!active) {
return;
}
const updateCallback = function () {
const datum = this.evaluateState(realtimeStates, this.openmct.time.timeSystem().key);
if (datum) {
callback(datum);
}
}.bind(this);
/* eslint-disable you-dont-need-lodash-underscore/map */
unsubscribes = _.map(realtimeStates, this.subscribeToObjectState.bind(this, updateCallback));
/* eslint-enable you-dont-need-lodash-underscore/map */
}.bind(this)
);
return function () {
active = false;
unsubscribes.forEach(function (unsubscribe) {
unsubscribe();
});
};
};
/**
* Returns a promise for a telemetry datum obtained by evaluating the
* current lad data.
*/
SummaryWidgetEvaluator.prototype.requestLatest = function (options) {
return this.getBaseStateClone()
.then(
function (ladState) {
const promises = Object.values(ladState).map(
this.updateObjectStateFromLAD.bind(this, options)
);
return Promise.all(promises).then(function () {
return ladState;
});
}.bind(this)
)
.then(
function (ladStates) {
return this.evaluateState(ladStates, options.domain);
}.bind(this)
);
};
SummaryWidgetEvaluator.prototype.updateRules = function (domainObject) {
this.rules = domainObject.configuration.ruleOrder.map(function (ruleId) {
return new SummaryWidgetRule(domainObject.configuration.ruleConfigById[ruleId]);
});
};
SummaryWidgetEvaluator.prototype.addChild = function (childObject) {
const childId = objectUtils.makeKeyString(childObject.identifier);
const metadata = this.openmct.telemetry.getMetadata(childObject);
const formats = this.openmct.telemetry.getFormatMap(metadata);
this.baseState[childId] = {
id: childId,
domainObject: childObject,
metadata: metadata,
formats: formats
};
};
SummaryWidgetEvaluator.prototype.removeChild = function (childObject) {
const childId = objectUtils.makeKeyString(childObject.identifier);
delete this.baseState[childId];
};
SummaryWidgetEvaluator.prototype.load = function () {
return this.loadPromise;
};
/**
* Return a promise for a 2-deep clone of the base state object: object
* states are shallow cloned, and then assembled and returned as a new base
* state. Allows object states to be mutated while sharing telemetry
* metadata and formats.
*/
SummaryWidgetEvaluator.prototype.getBaseStateClone = function () {
return this.load().then(
function () {
/* eslint-disable you-dont-need-lodash-underscore/values */
return _(this.baseState).values().map(_.clone).keyBy('id').value();
/* eslint-enable you-dont-need-lodash-underscore/values */
}.bind(this)
);
};
/**
* Subscribes to realtime updates for a given objectState, and invokes
* the supplied callback when objectState has been updated. Returns
* a function to unsubscribe.
* @private.
*/
SummaryWidgetEvaluator.prototype.subscribeToObjectState = function (callback, objectState) {
return this.openmct.telemetry.subscribe(
objectState.domainObject,
function (datum) {
objectState.lastDatum = datum;
objectState.timestamps = this.getTimestamps(objectState.id, datum);
callback();
}.bind(this)
);
};
/**
* Given an object state, will return a promise that is resolved when the
* object state has been updated from the LAD.
* @private.
*/
SummaryWidgetEvaluator.prototype.updateObjectStateFromLAD = function (options, objectState) {
options = Object.assign({}, options, {
strategy: 'latest',
size: 1
});
return this.openmct.telemetry.request(objectState.domainObject, options).then(
function (results) {
objectState.lastDatum = results[results.length - 1];
objectState.timestamps = this.getTimestamps(objectState.id, objectState.lastDatum);
}.bind(this)
);
};
/**
* Returns an object containing all domain values in a datum.
* @private.
*/
SummaryWidgetEvaluator.prototype.getTimestamps = function (childId, datum) {
const timestampedDatum = {};
this.openmct.time.getAllTimeSystems().forEach(function (timeSystem) {
timestampedDatum[timeSystem.key] = this.baseState[childId].formats[timeSystem.key].parse(datum);
}, this);
return timestampedDatum;
};
/**
* Given a base datum(containing timestamps) and rule index, adds values
* from the matching rule.
* @private
*/
SummaryWidgetEvaluator.prototype.makeDatumFromRule = function (ruleIndex, baseDatum) {
const rule = this.rules[ruleIndex];
baseDatum.ruleLabel = rule.label;
baseDatum.ruleName = rule.name;
baseDatum.message = rule.message;
baseDatum.ruleIndex = ruleIndex;
baseDatum.backgroundColor = rule.style['background-color'];
baseDatum.textColor = rule.style.color;
baseDatum.borderColor = rule.style['border-color'];
baseDatum.icon = rule.icon;
return baseDatum;
};
/**
* Evaluate a `state` object and return a summary widget telemetry datum.
* Datum timestamps will be taken from the "latest" datum in the `state`
* where "latest" is the datum with the largest value for the given
* `timestampKey`.
* @private.
*/
SummaryWidgetEvaluator.prototype.evaluateState = function (state, timestampKey) {
const hasRequiredData = Object.keys(state).reduce(function (itDoes, k) {
return itDoes && state[k].lastDatum;
}, true);
if (!hasRequiredData) {
return;
} }
eventHelpers.extend(SummaryWidgetEvaluator.prototype); let i;
for (i = this.rules.length - 1; i > 0; i--) {
/** if (this.rules[i].evaluate(state, false)) {
* Subscribes to realtime telemetry for the given summary widget. break;
*/
SummaryWidgetEvaluator.prototype.subscribe = function (callback) {
let active = true;
let unsubscribes = [];
this.getBaseStateClone().then(
function (realtimeStates) {
if (!active) {
return;
}
const updateCallback = function () {
const datum = this.evaluateState(realtimeStates, this.openmct.time.timeSystem().key);
if (datum) {
callback(datum);
}
}.bind(this);
/* eslint-disable you-dont-need-lodash-underscore/map */
unsubscribes = _.map(
realtimeStates,
this.subscribeToObjectState.bind(this, updateCallback)
);
/* eslint-enable you-dont-need-lodash-underscore/map */
}.bind(this)
);
return function () {
active = false;
unsubscribes.forEach(function (unsubscribe) {
unsubscribe();
});
};
};
/**
* Returns a promise for a telemetry datum obtained by evaluating the
* current lad data.
*/
SummaryWidgetEvaluator.prototype.requestLatest = function (options) {
return this.getBaseStateClone()
.then(
function (ladState) {
const promises = Object.values(ladState).map(
this.updateObjectStateFromLAD.bind(this, options)
);
return Promise.all(promises).then(function () {
return ladState;
});
}.bind(this)
)
.then(
function (ladStates) {
return this.evaluateState(ladStates, options.domain);
}.bind(this)
);
};
SummaryWidgetEvaluator.prototype.updateRules = function (domainObject) {
this.rules = domainObject.configuration.ruleOrder.map(function (ruleId) {
return new SummaryWidgetRule(domainObject.configuration.ruleConfigById[ruleId]);
});
};
SummaryWidgetEvaluator.prototype.addChild = function (childObject) {
const childId = objectUtils.makeKeyString(childObject.identifier);
const metadata = this.openmct.telemetry.getMetadata(childObject);
const formats = this.openmct.telemetry.getFormatMap(metadata);
this.baseState[childId] = {
id: childId,
domainObject: childObject,
metadata: metadata,
formats: formats
};
};
SummaryWidgetEvaluator.prototype.removeChild = function (childObject) {
const childId = objectUtils.makeKeyString(childObject.identifier);
delete this.baseState[childId];
};
SummaryWidgetEvaluator.prototype.load = function () {
return this.loadPromise;
};
/**
* Return a promise for a 2-deep clone of the base state object: object
* states are shallow cloned, and then assembled and returned as a new base
* state. Allows object states to be mutated while sharing telemetry
* metadata and formats.
*/
SummaryWidgetEvaluator.prototype.getBaseStateClone = function () {
return this.load().then(
function () {
/* eslint-disable you-dont-need-lodash-underscore/values */
return _(this.baseState).values().map(_.clone).keyBy('id').value();
/* eslint-enable you-dont-need-lodash-underscore/values */
}.bind(this)
);
};
/**
* Subscribes to realtime updates for a given objectState, and invokes
* the supplied callback when objectState has been updated. Returns
* a function to unsubscribe.
* @private.
*/
SummaryWidgetEvaluator.prototype.subscribeToObjectState = function (callback, objectState) {
return this.openmct.telemetry.subscribe(
objectState.domainObject,
function (datum) {
objectState.lastDatum = datum;
objectState.timestamps = this.getTimestamps(objectState.id, datum);
callback();
}.bind(this)
);
};
/**
* Given an object state, will return a promise that is resolved when the
* object state has been updated from the LAD.
* @private.
*/
SummaryWidgetEvaluator.prototype.updateObjectStateFromLAD = function (options, objectState) {
options = Object.assign({}, options, {
strategy: 'latest',
size: 1
});
return this.openmct.telemetry.request(objectState.domainObject, options).then(
function (results) {
objectState.lastDatum = results[results.length - 1];
objectState.timestamps = this.getTimestamps(objectState.id, objectState.lastDatum);
}.bind(this)
);
};
/**
* Returns an object containing all domain values in a datum.
* @private.
*/
SummaryWidgetEvaluator.prototype.getTimestamps = function (childId, datum) {
const timestampedDatum = {};
this.openmct.time.getAllTimeSystems().forEach(function (timeSystem) {
timestampedDatum[timeSystem.key] =
this.baseState[childId].formats[timeSystem.key].parse(datum);
}, this);
return timestampedDatum;
};
/**
* Given a base datum(containing timestamps) and rule index, adds values
* from the matching rule.
* @private
*/
SummaryWidgetEvaluator.prototype.makeDatumFromRule = function (ruleIndex, baseDatum) {
const rule = this.rules[ruleIndex];
baseDatum.ruleLabel = rule.label;
baseDatum.ruleName = rule.name;
baseDatum.message = rule.message;
baseDatum.ruleIndex = ruleIndex;
baseDatum.backgroundColor = rule.style['background-color'];
baseDatum.textColor = rule.style.color;
baseDatum.borderColor = rule.style['border-color'];
baseDatum.icon = rule.icon;
return baseDatum;
};
/**
* Evaluate a `state` object and return a summary widget telemetry datum.
* Datum timestamps will be taken from the "latest" datum in the `state`
* where "latest" is the datum with the largest value for the given
* `timestampKey`.
* @private.
*/
SummaryWidgetEvaluator.prototype.evaluateState = function (state, timestampKey) {
const hasRequiredData = Object.keys(state).reduce(function (itDoes, k) {
return itDoes && state[k].lastDatum;
}, true);
if (!hasRequiredData) {
return;
} }
}
let i; /* eslint-disable you-dont-need-lodash-underscore/map */
for (i = this.rules.length - 1; i > 0; i--) { let latestTimestamp = _(state).map('timestamps').sortBy(timestampKey).last();
if (this.rules[i].evaluate(state, false)) { /* eslint-enable you-dont-need-lodash-underscore/map */
break;
}
}
/* eslint-disable you-dont-need-lodash-underscore/map */ if (!latestTimestamp) {
let latestTimestamp = _(state).map('timestamps').sortBy(timestampKey).last(); latestTimestamp = {};
/* eslint-enable you-dont-need-lodash-underscore/map */ }
if (!latestTimestamp) { const baseDatum = _.clone(latestTimestamp);
latestTimestamp = {};
}
const baseDatum = _.clone(latestTimestamp); return this.makeDatumFromRule(i, baseDatum);
};
return this.makeDatumFromRule(i, baseDatum); /**
}; * remove all listeners and clean up any resources.
*/
/** SummaryWidgetEvaluator.prototype.destroy = function () {
* remove all listeners and clean up any resources. this.stopListening();
*/ this.removeObserver();
SummaryWidgetEvaluator.prototype.destroy = function () { };
this.stopListening();
this.removeObserver();
};
return SummaryWidgetEvaluator;
});

View File

@ -20,94 +20,90 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { export default function SummaryWidgetMetadataProvider(openmct) {
function SummaryWidgetMetadataProvider(openmct) { this.openmct = openmct;
this.openmct = openmct; }
}
SummaryWidgetMetadataProvider.prototype.supportsMetadata = function (domainObject) { SummaryWidgetMetadataProvider.prototype.supportsMetadata = function (domainObject) {
return domainObject.type === 'summary-widget'; return domainObject.type === 'summary-widget';
}; };
SummaryWidgetMetadataProvider.prototype.getDomains = function (domainObject) { SummaryWidgetMetadataProvider.prototype.getDomains = function (domainObject) {
return this.openmct.time.getAllTimeSystems().map(function (ts, i) { return this.openmct.time.getAllTimeSystems().map(function (ts, i) {
return {
key: ts.key,
name: ts.name,
format: ts.timeFormat,
hints: {
domain: i
}
};
});
};
SummaryWidgetMetadataProvider.prototype.getMetadata = function (domainObject) {
const ruleOrder = domainObject.configuration.ruleOrder || [];
const enumerations = ruleOrder
.filter(function (ruleId) {
return Boolean(domainObject.configuration.ruleConfigById[ruleId]);
})
.map(function (ruleId, ruleIndex) {
return { return {
key: ts.key, string: domainObject.configuration.ruleConfigById[ruleId].label,
name: ts.name, value: ruleIndex
format: ts.timeFormat,
hints: {
domain: i
}
}; };
}); });
};
SummaryWidgetMetadataProvider.prototype.getMetadata = function (domainObject) { const metadata = {
const ruleOrder = domainObject.configuration.ruleOrder || []; // Generally safe assumption is that we have one domain per timeSystem.
const enumerations = ruleOrder values: this.getDomains().concat([
.filter(function (ruleId) { {
return Boolean(domainObject.configuration.ruleConfigById[ruleId]); name: 'State',
}) key: 'state',
.map(function (ruleId, ruleIndex) { source: 'ruleIndex',
return { format: 'enum',
string: domainObject.configuration.ruleConfigById[ruleId].label, enumerations: enumerations,
value: ruleIndex hints: {
}; range: 1
});
const metadata = {
// Generally safe assumption is that we have one domain per timeSystem.
values: this.getDomains().concat([
{
name: 'State',
key: 'state',
source: 'ruleIndex',
format: 'enum',
enumerations: enumerations,
hints: {
range: 1
}
},
{
name: 'Rule Label',
key: 'ruleLabel',
format: 'string'
},
{
name: 'Rule Name',
key: 'ruleName',
format: 'string'
},
{
name: 'Message',
key: 'message',
format: 'string'
},
{
name: 'Background Color',
key: 'backgroundColor',
format: 'string'
},
{
name: 'Text Color',
key: 'textColor',
format: 'string'
},
{
name: 'Border Color',
key: 'borderColor',
format: 'string'
},
{
name: 'Display Icon',
key: 'icon',
format: 'string'
} }
]) },
}; {
name: 'Rule Label',
return metadata; key: 'ruleLabel',
format: 'string'
},
{
name: 'Rule Name',
key: 'ruleName',
format: 'string'
},
{
name: 'Message',
key: 'message',
format: 'string'
},
{
name: 'Background Color',
key: 'backgroundColor',
format: 'string'
},
{
name: 'Text Color',
key: 'textColor',
format: 'string'
},
{
name: 'Border Color',
key: 'borderColor',
format: 'string'
},
{
name: 'Display Icon',
key: 'icon',
format: 'string'
}
])
}; };
return SummaryWidgetMetadataProvider; return metadata;
}); };

View File

@ -20,51 +20,49 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./SummaryWidgetCondition'], function (SummaryWidgetCondition) { import SummaryWidgetCondition from './SummaryWidgetCondition';
function SummaryWidgetRule(definition) {
this.name = definition.name;
this.label = definition.label;
this.id = definition.id;
this.icon = definition.icon;
this.style = definition.style;
this.message = definition.message;
this.description = definition.description;
this.conditions = definition.conditions.map(function (cDefinition) {
return new SummaryWidgetCondition(cDefinition);
});
this.trigger = definition.trigger;
}
/** export default function SummaryWidgetRule(definition) {
* Evaluate the given rule against a telemetryState and return true if it this.name = definition.name;
* matches. this.label = definition.label;
*/ this.id = definition.id;
SummaryWidgetRule.prototype.evaluate = function (telemetryState) { this.icon = definition.icon;
let i; this.style = definition.style;
let result; this.message = definition.message;
this.description = definition.description;
this.conditions = definition.conditions.map(function (cDefinition) {
return new SummaryWidgetCondition(cDefinition);
});
this.trigger = definition.trigger;
}
if (this.trigger === 'all') { /**
for (i = 0; i < this.conditions.length; i++) { * Evaluate the given rule against a telemetryState and return true if it
result = this.conditions[i].evaluate(telemetryState); * matches.
if (!result) { */
return false; SummaryWidgetRule.prototype.evaluate = function (telemetryState) {
} let i;
let result;
if (this.trigger === 'all') {
for (i = 0; i < this.conditions.length; i++) {
result = this.conditions[i].evaluate(telemetryState);
if (!result) {
return false;
} }
return true;
} else if (this.trigger === 'any') {
for (i = 0; i < this.conditions.length; i++) {
result = this.conditions[i].evaluate(telemetryState);
if (result) {
return true;
}
}
return false;
} else {
throw new Error('Invalid rule trigger: ' + this.trigger);
} }
};
return SummaryWidgetRule; return true;
}); } else if (this.trigger === 'any') {
for (i = 0; i < this.conditions.length; i++) {
result = this.conditions[i].evaluate(telemetryState);
if (result) {
return true;
}
}
return false;
} else {
throw new Error('Invalid rule trigger: ' + this.trigger);
}
};

View File

@ -20,134 +20,134 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./SummaryWidgetRule'], function (SummaryWidgetRule) { import SummaryWidgetRule from './SummaryWidgetRule';
describe('SummaryWidgetRule', function () {
let rule;
let telemetryState;
beforeEach(function () { describe('SummaryWidgetRule', function () {
const formatMap = { let rule;
raw: { let telemetryState;
parse: function (datum) {
return datum.value; beforeEach(function () {
} const formatMap = {
raw: {
parse: function (datum) {
return datum.value;
} }
}; }
};
telemetryState = { telemetryState = {
objectId: { objectId: {
formats: formatMap, formats: formatMap,
lastDatum: {} lastDatum: {}
},
otherObjectId: {
formats: formatMap,
lastDatum: {}
}
};
});
it('allows single condition rules with any', function () {
rule = new SummaryWidgetRule({
trigger: 'any',
conditions: [
{
object: 'objectId',
key: 'raw',
operation: 'greaterThan',
values: [10]
}
]
});
telemetryState.objectId.lastDatum.value = 5;
expect(rule.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 15;
expect(rule.evaluate(telemetryState)).toBe(true);
});
it('allows single condition rules with all', function () {
rule = new SummaryWidgetRule({
trigger: 'all',
conditions: [
{
object: 'objectId',
key: 'raw',
operation: 'greaterThan',
values: [10]
}
]
});
telemetryState.objectId.lastDatum.value = 5;
expect(rule.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 15;
expect(rule.evaluate(telemetryState)).toBe(true);
});
it('can combine multiple conditions with all', function () {
rule = new SummaryWidgetRule({
trigger: 'all',
conditions: [
{
object: 'objectId',
key: 'raw',
operation: 'greaterThan',
values: [10]
}, },
otherObjectId: { {
formats: formatMap, object: 'otherObjectId',
lastDatum: {} key: 'raw',
operation: 'greaterThan',
values: [20]
} }
}; ]
}); });
it('allows single condition rules with any', function () { telemetryState.objectId.lastDatum.value = 5;
rule = new SummaryWidgetRule({ telemetryState.otherObjectId.lastDatum.value = 5;
trigger: 'any', expect(rule.evaluate(telemetryState)).toBe(false);
conditions: [ telemetryState.objectId.lastDatum.value = 5;
{ telemetryState.otherObjectId.lastDatum.value = 25;
object: 'objectId', expect(rule.evaluate(telemetryState)).toBe(false);
key: 'raw', telemetryState.objectId.lastDatum.value = 15;
operation: 'greaterThan', telemetryState.otherObjectId.lastDatum.value = 5;
values: [10] expect(rule.evaluate(telemetryState)).toBe(false);
} telemetryState.objectId.lastDatum.value = 15;
] telemetryState.otherObjectId.lastDatum.value = 25;
}); expect(rule.evaluate(telemetryState)).toBe(true);
});
telemetryState.objectId.lastDatum.value = 5; it('can combine multiple conditions with any', function () {
expect(rule.evaluate(telemetryState)).toBe(false); rule = new SummaryWidgetRule({
telemetryState.objectId.lastDatum.value = 15; trigger: 'any',
expect(rule.evaluate(telemetryState)).toBe(true); conditions: [
{
object: 'objectId',
key: 'raw',
operation: 'greaterThan',
values: [10]
},
{
object: 'otherObjectId',
key: 'raw',
operation: 'greaterThan',
values: [20]
}
]
}); });
it('allows single condition rules with all', function () { telemetryState.objectId.lastDatum.value = 5;
rule = new SummaryWidgetRule({ telemetryState.otherObjectId.lastDatum.value = 5;
trigger: 'all', expect(rule.evaluate(telemetryState)).toBe(false);
conditions: [ telemetryState.objectId.lastDatum.value = 5;
{ telemetryState.otherObjectId.lastDatum.value = 25;
object: 'objectId', expect(rule.evaluate(telemetryState)).toBe(true);
key: 'raw', telemetryState.objectId.lastDatum.value = 15;
operation: 'greaterThan', telemetryState.otherObjectId.lastDatum.value = 5;
values: [10] expect(rule.evaluate(telemetryState)).toBe(true);
} telemetryState.objectId.lastDatum.value = 15;
] telemetryState.otherObjectId.lastDatum.value = 25;
}); expect(rule.evaluate(telemetryState)).toBe(true);
telemetryState.objectId.lastDatum.value = 5;
expect(rule.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 15;
expect(rule.evaluate(telemetryState)).toBe(true);
});
it('can combine multiple conditions with all', function () {
rule = new SummaryWidgetRule({
trigger: 'all',
conditions: [
{
object: 'objectId',
key: 'raw',
operation: 'greaterThan',
values: [10]
},
{
object: 'otherObjectId',
key: 'raw',
operation: 'greaterThan',
values: [20]
}
]
});
telemetryState.objectId.lastDatum.value = 5;
telemetryState.otherObjectId.lastDatum.value = 5;
expect(rule.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 5;
telemetryState.otherObjectId.lastDatum.value = 25;
expect(rule.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 5;
expect(rule.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 25;
expect(rule.evaluate(telemetryState)).toBe(true);
});
it('can combine multiple conditions with any', function () {
rule = new SummaryWidgetRule({
trigger: 'any',
conditions: [
{
object: 'objectId',
key: 'raw',
operation: 'greaterThan',
values: [10]
},
{
object: 'otherObjectId',
key: 'raw',
operation: 'greaterThan',
values: [20]
}
]
});
telemetryState.objectId.lastDatum.value = 5;
telemetryState.otherObjectId.lastDatum.value = 5;
expect(rule.evaluate(telemetryState)).toBe(false);
telemetryState.objectId.lastDatum.value = 5;
telemetryState.otherObjectId.lastDatum.value = 25;
expect(rule.evaluate(telemetryState)).toBe(true);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 5;
expect(rule.evaluate(telemetryState)).toBe(true);
telemetryState.objectId.lastDatum.value = 15;
telemetryState.otherObjectId.lastDatum.value = 25;
expect(rule.evaluate(telemetryState)).toBe(true);
});
}); });
}); });

View File

@ -20,44 +20,42 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./EvaluatorPool'], function (EvaluatorPool) { import EvaluatorPool from './EvaluatorPool';
function SummaryWidgetTelemetryProvider(openmct) {
this.pool = new EvaluatorPool(openmct); export default function SummaryWidgetTelemetryProvider(openmct) {
this.pool = new EvaluatorPool(openmct);
}
SummaryWidgetTelemetryProvider.prototype.supportsRequest = function (domainObject, options) {
return domainObject.type === 'summary-widget';
};
SummaryWidgetTelemetryProvider.prototype.request = function (domainObject, options) {
if (options.strategy !== 'latest' && options.size !== 1) {
return Promise.resolve([]);
} }
SummaryWidgetTelemetryProvider.prototype.supportsRequest = function (domainObject, options) { const evaluator = this.pool.get(domainObject);
return domainObject.type === 'summary-widget';
};
SummaryWidgetTelemetryProvider.prototype.request = function (domainObject, options) { return evaluator.requestLatest(options).then(
if (options.strategy !== 'latest' && options.size !== 1) { function (latestDatum) {
return Promise.resolve([]);
}
const evaluator = this.pool.get(domainObject);
return evaluator.requestLatest(options).then(
function (latestDatum) {
this.pool.release(evaluator);
return latestDatum ? [latestDatum] : [];
}.bind(this)
);
};
SummaryWidgetTelemetryProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'summary-widget';
};
SummaryWidgetTelemetryProvider.prototype.subscribe = function (domainObject, callback) {
const evaluator = this.pool.get(domainObject);
const unsubscribe = evaluator.subscribe(callback);
return function () {
this.pool.release(evaluator); this.pool.release(evaluator);
unsubscribe();
}.bind(this);
};
return SummaryWidgetTelemetryProvider; return latestDatum ? [latestDatum] : [];
}); }.bind(this)
);
};
SummaryWidgetTelemetryProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'summary-widget';
};
SummaryWidgetTelemetryProvider.prototype.subscribe = function (domainObject, callback) {
const evaluator = this.pool.get(domainObject);
const unsubscribe = evaluator.subscribe(callback);
return function () {
this.pool.release(evaluator);
unsubscribe();
}.bind(this);
};

View File

@ -20,444 +20,444 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./SummaryWidgetTelemetryProvider'], function (SummaryWidgetTelemetryProvider) { import SummaryWidgetTelemetryProvider from './SummaryWidgetTelemetryProvider';
xdescribe('SummaryWidgetTelemetryProvider', function () {
let telemObjectA; xdescribe('SummaryWidgetTelemetryProvider', function () {
let telemObjectB; let telemObjectA;
let summaryWidgetObject; let telemObjectB;
let openmct; let summaryWidgetObject;
let telemUnsubscribes; let openmct;
let unobserver; let telemUnsubscribes;
let composition; let unobserver;
let telemetryProvider; let composition;
let loader; let telemetryProvider;
let loader;
beforeEach(function () {
telemObjectA = {
identifier: {
namespace: 'a',
key: 'telem'
}
};
telemObjectB = {
identifier: {
namespace: 'b',
key: 'telem'
}
};
summaryWidgetObject = {
name: 'Summary Widget',
type: 'summary-widget',
identifier: {
namespace: 'base',
key: 'widgetId'
},
composition: ['a:telem', 'b:telem'],
configuration: {
ruleOrder: ['default', 'rule0', 'rule1'],
ruleConfigById: {
default: {
name: 'safe',
label: "Don't Worry",
message: "It's Ok",
id: 'default',
icon: 'a-ok',
style: {
color: '#ffffff',
'background-color': '#38761d',
'border-color': 'rgba(0,0,0,0)'
},
conditions: [
{
object: '',
key: '',
operation: '',
values: []
}
],
trigger: 'any'
},
rule0: {
name: 'A High',
label: 'Start Worrying',
message: 'A is a little high...',
id: 'rule0',
icon: 'a-high',
style: {
color: '#000000',
'background-color': '#ffff00',
'border-color': 'rgba(1,1,0,0)'
},
conditions: [
{
object: 'a:telem',
key: 'measurement',
operation: 'greaterThan',
values: [50]
}
],
trigger: 'any'
},
rule1: {
name: 'B Low',
label: 'WORRY!',
message: 'B is Low',
id: 'rule1',
icon: 'b-low',
style: {
color: '#ff00ff',
'background-color': '#ff0000',
'border-color': 'rgba(1,0,0,0)'
},
conditions: [
{
object: 'b:telem',
key: 'measurement',
operation: 'lessThan',
values: [10]
}
],
trigger: 'any'
}
}
}
};
openmct = {
objects: jasmine.createSpyObj('objectAPI', ['get', 'observe']),
telemetry: jasmine.createSpyObj('telemetryAPI', [
'getMetadata',
'getFormatMap',
'request',
'subscribe',
'addProvider'
]),
composition: jasmine.createSpyObj('compositionAPI', ['get']),
time: jasmine.createSpyObj('timeAPI', ['getAllTimeSystems', 'timeSystem'])
};
openmct.time.getAllTimeSystems.and.returnValue([{ key: 'timestamp' }]);
openmct.time.timeSystem.and.returnValue({ key: 'timestamp' });
unobserver = jasmine.createSpy('unobserver');
openmct.objects.observe.and.returnValue(unobserver);
composition = jasmine.createSpyObj('compositionCollection', ['on', 'off', 'load']);
function notify(eventName, a, b) {
composition.on.calls
.all()
.filter(function (c) {
return c.args[0] === eventName;
})
.forEach(function (c) {
if (c.args[2]) {
// listener w/ context.
c.args[1].call(c.args[2], a, b);
} else {
// listener w/o context.
c.args[1](a, b);
}
});
}
loader = {};
loader.promise = new Promise(function (resolve, reject) {
loader.resolve = resolve;
loader.reject = reject;
});
composition.load.and.callFake(function () {
setTimeout(function () {
notify('add', telemObjectA);
setTimeout(function () {
notify('add', telemObjectB);
setTimeout(function () {
loader.resolve();
});
});
});
return loader.promise;
});
openmct.composition.get.and.returnValue(composition);
telemUnsubscribes = [];
openmct.telemetry.subscribe.and.callFake(function () {
const unsubscriber = jasmine.createSpy('unsubscriber' + telemUnsubscribes.length);
telemUnsubscribes.push(unsubscriber);
return unsubscriber;
});
openmct.telemetry.getMetadata.and.callFake(function (object) {
return {
name: 'fake metadata manager',
object: object,
keys: ['timestamp', 'measurement']
};
});
openmct.telemetry.getFormatMap.and.callFake(function (metadata) {
expect(metadata.name).toBe('fake metadata manager');
return {
metadata: metadata,
timestamp: {
parse: function (datum) {
return datum.t;
}
},
measurement: {
parse: function (datum) {
return datum.m;
}
}
};
});
telemetryProvider = new SummaryWidgetTelemetryProvider(openmct);
});
it('supports subscription for summary widgets', function () {
expect(telemetryProvider.supportsSubscribe(summaryWidgetObject)).toBe(true);
});
it('supports requests for summary widgets', function () {
expect(telemetryProvider.supportsRequest(summaryWidgetObject)).toBe(true);
});
it('does not support other requests or subscriptions', function () {
expect(telemetryProvider.supportsSubscribe(telemObjectA)).toBe(false);
expect(telemetryProvider.supportsRequest(telemObjectA)).toBe(false);
});
it('Returns no results for basic requests', function () {
return telemetryProvider.request(summaryWidgetObject, {}).then(function (result) {
expect(result).toEqual([]);
});
});
it('provides realtime telemetry', function () {
const callback = jasmine.createSpy('callback');
telemetryProvider.subscribe(summaryWidgetObject, callback);
return loader.promise
.then(function () {
return new Promise(function (resolve) {
setTimeout(resolve);
});
})
.then(function () {
expect(openmct.telemetry.subscribe.calls.count()).toBe(2);
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(
telemObjectA,
jasmine.any(Function)
);
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(
telemObjectB,
jasmine.any(Function)
);
const aCallback = openmct.telemetry.subscribe.calls.all()[0].args[1];
const bCallback = openmct.telemetry.subscribe.calls.all()[1].args[1];
aCallback({
t: 123,
m: 25
});
expect(callback).not.toHaveBeenCalled();
bCallback({
t: 123,
m: 25
});
expect(callback).toHaveBeenCalledWith({
timestamp: 123,
ruleLabel: "Don't Worry",
ruleName: 'safe',
message: "It's Ok",
ruleIndex: 0,
backgroundColor: '#38761d',
textColor: '#ffffff',
borderColor: 'rgba(0,0,0,0)',
icon: 'a-ok'
});
aCallback({
t: 140,
m: 55
});
expect(callback).toHaveBeenCalledWith({
timestamp: 140,
ruleLabel: 'Start Worrying',
ruleName: 'A High',
message: 'A is a little high...',
ruleIndex: 1,
backgroundColor: '#ffff00',
textColor: '#000000',
borderColor: 'rgba(1,1,0,0)',
icon: 'a-high'
});
bCallback({
t: 140,
m: -10
});
expect(callback).toHaveBeenCalledWith({
timestamp: 140,
ruleLabel: 'WORRY!',
ruleName: 'B Low',
message: 'B is Low',
ruleIndex: 2,
backgroundColor: '#ff0000',
textColor: '#ff00ff',
borderColor: 'rgba(1,0,0,0)',
icon: 'b-low'
});
aCallback({
t: 160,
m: 25
});
expect(callback).toHaveBeenCalledWith({
timestamp: 160,
ruleLabel: 'WORRY!',
ruleName: 'B Low',
message: 'B is Low',
ruleIndex: 2,
backgroundColor: '#ff0000',
textColor: '#ff00ff',
borderColor: 'rgba(1,0,0,0)',
icon: 'b-low'
});
bCallback({
t: 160,
m: 25
});
expect(callback).toHaveBeenCalledWith({
timestamp: 160,
ruleLabel: "Don't Worry",
ruleName: 'safe',
message: "It's Ok",
ruleIndex: 0,
backgroundColor: '#38761d',
textColor: '#ffffff',
borderColor: 'rgba(0,0,0,0)',
icon: 'a-ok'
});
});
});
describe('providing lad telemetry', function () {
let responseDatums;
let resultsShouldBe;
beforeEach(function () { beforeEach(function () {
telemObjectA = { openmct.telemetry.request.and.callFake(function (rObj, options) {
identifier: { expect(rObj).toEqual(jasmine.any(Object));
namespace: 'a', expect(options).toEqual({
key: 'telem' size: 1,
} strategy: 'latest',
}; domain: 'timestamp'
telemObjectB = {
identifier: {
namespace: 'b',
key: 'telem'
}
};
summaryWidgetObject = {
name: 'Summary Widget',
type: 'summary-widget',
identifier: {
namespace: 'base',
key: 'widgetId'
},
composition: ['a:telem', 'b:telem'],
configuration: {
ruleOrder: ['default', 'rule0', 'rule1'],
ruleConfigById: {
default: {
name: 'safe',
label: "Don't Worry",
message: "It's Ok",
id: 'default',
icon: 'a-ok',
style: {
color: '#ffffff',
'background-color': '#38761d',
'border-color': 'rgba(0,0,0,0)'
},
conditions: [
{
object: '',
key: '',
operation: '',
values: []
}
],
trigger: 'any'
},
rule0: {
name: 'A High',
label: 'Start Worrying',
message: 'A is a little high...',
id: 'rule0',
icon: 'a-high',
style: {
color: '#000000',
'background-color': '#ffff00',
'border-color': 'rgba(1,1,0,0)'
},
conditions: [
{
object: 'a:telem',
key: 'measurement',
operation: 'greaterThan',
values: [50]
}
],
trigger: 'any'
},
rule1: {
name: 'B Low',
label: 'WORRY!',
message: 'B is Low',
id: 'rule1',
icon: 'b-low',
style: {
color: '#ff00ff',
'background-color': '#ff0000',
'border-color': 'rgba(1,0,0,0)'
},
conditions: [
{
object: 'b:telem',
key: 'measurement',
operation: 'lessThan',
values: [10]
}
],
trigger: 'any'
}
}
}
};
openmct = {
objects: jasmine.createSpyObj('objectAPI', ['get', 'observe']),
telemetry: jasmine.createSpyObj('telemetryAPI', [
'getMetadata',
'getFormatMap',
'request',
'subscribe',
'addProvider'
]),
composition: jasmine.createSpyObj('compositionAPI', ['get']),
time: jasmine.createSpyObj('timeAPI', ['getAllTimeSystems', 'timeSystem'])
};
openmct.time.getAllTimeSystems.and.returnValue([{ key: 'timestamp' }]);
openmct.time.timeSystem.and.returnValue({ key: 'timestamp' });
unobserver = jasmine.createSpy('unobserver');
openmct.objects.observe.and.returnValue(unobserver);
composition = jasmine.createSpyObj('compositionCollection', ['on', 'off', 'load']);
function notify(eventName, a, b) {
composition.on.calls
.all()
.filter(function (c) {
return c.args[0] === eventName;
})
.forEach(function (c) {
if (c.args[2]) {
// listener w/ context.
c.args[1].call(c.args[2], a, b);
} else {
// listener w/o context.
c.args[1](a, b);
}
});
}
loader = {};
loader.promise = new Promise(function (resolve, reject) {
loader.resolve = resolve;
loader.reject = reject;
});
composition.load.and.callFake(function () {
setTimeout(function () {
notify('add', telemObjectA);
setTimeout(function () {
notify('add', telemObjectB);
setTimeout(function () {
loader.resolve();
});
});
}); });
expect(responseDatums[rObj.identifier.namespace]).toBeDefined();
return loader.promise; return Promise.resolve([responseDatums[rObj.identifier.namespace]]);
}); });
openmct.composition.get.and.returnValue(composition); responseDatums = {};
telemUnsubscribes = []; resultsShouldBe = function (results) {
openmct.telemetry.subscribe.and.callFake(function () { return telemetryProvider
const unsubscriber = jasmine.createSpy('unsubscriber' + telemUnsubscribes.length); .request(summaryWidgetObject, {
telemUnsubscribes.push(unsubscriber);
return unsubscriber;
});
openmct.telemetry.getMetadata.and.callFake(function (object) {
return {
name: 'fake metadata manager',
object: object,
keys: ['timestamp', 'measurement']
};
});
openmct.telemetry.getFormatMap.and.callFake(function (metadata) {
expect(metadata.name).toBe('fake metadata manager');
return {
metadata: metadata,
timestamp: {
parse: function (datum) {
return datum.t;
}
},
measurement: {
parse: function (datum) {
return datum.m;
}
}
};
});
telemetryProvider = new SummaryWidgetTelemetryProvider(openmct);
});
it('supports subscription for summary widgets', function () {
expect(telemetryProvider.supportsSubscribe(summaryWidgetObject)).toBe(true);
});
it('supports requests for summary widgets', function () {
expect(telemetryProvider.supportsRequest(summaryWidgetObject)).toBe(true);
});
it('does not support other requests or subscriptions', function () {
expect(telemetryProvider.supportsSubscribe(telemObjectA)).toBe(false);
expect(telemetryProvider.supportsRequest(telemObjectA)).toBe(false);
});
it('Returns no results for basic requests', function () {
return telemetryProvider.request(summaryWidgetObject, {}).then(function (result) {
expect(result).toEqual([]);
});
});
it('provides realtime telemetry', function () {
const callback = jasmine.createSpy('callback');
telemetryProvider.subscribe(summaryWidgetObject, callback);
return loader.promise
.then(function () {
return new Promise(function (resolve) {
setTimeout(resolve);
});
})
.then(function () {
expect(openmct.telemetry.subscribe.calls.count()).toBe(2);
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(
telemObjectA,
jasmine.any(Function)
);
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(
telemObjectB,
jasmine.any(Function)
);
const aCallback = openmct.telemetry.subscribe.calls.all()[0].args[1];
const bCallback = openmct.telemetry.subscribe.calls.all()[1].args[1];
aCallback({
t: 123,
m: 25
});
expect(callback).not.toHaveBeenCalled();
bCallback({
t: 123,
m: 25
});
expect(callback).toHaveBeenCalledWith({
timestamp: 123,
ruleLabel: "Don't Worry",
ruleName: 'safe',
message: "It's Ok",
ruleIndex: 0,
backgroundColor: '#38761d',
textColor: '#ffffff',
borderColor: 'rgba(0,0,0,0)',
icon: 'a-ok'
});
aCallback({
t: 140,
m: 55
});
expect(callback).toHaveBeenCalledWith({
timestamp: 140,
ruleLabel: 'Start Worrying',
ruleName: 'A High',
message: 'A is a little high...',
ruleIndex: 1,
backgroundColor: '#ffff00',
textColor: '#000000',
borderColor: 'rgba(1,1,0,0)',
icon: 'a-high'
});
bCallback({
t: 140,
m: -10
});
expect(callback).toHaveBeenCalledWith({
timestamp: 140,
ruleLabel: 'WORRY!',
ruleName: 'B Low',
message: 'B is Low',
ruleIndex: 2,
backgroundColor: '#ff0000',
textColor: '#ff00ff',
borderColor: 'rgba(1,0,0,0)',
icon: 'b-low'
});
aCallback({
t: 160,
m: 25
});
expect(callback).toHaveBeenCalledWith({
timestamp: 160,
ruleLabel: 'WORRY!',
ruleName: 'B Low',
message: 'B is Low',
ruleIndex: 2,
backgroundColor: '#ff0000',
textColor: '#ff00ff',
borderColor: 'rgba(1,0,0,0)',
icon: 'b-low'
});
bCallback({
t: 160,
m: 25
});
expect(callback).toHaveBeenCalledWith({
timestamp: 160,
ruleLabel: "Don't Worry",
ruleName: 'safe',
message: "It's Ok",
ruleIndex: 0,
backgroundColor: '#38761d',
textColor: '#ffffff',
borderColor: 'rgba(0,0,0,0)',
icon: 'a-ok'
});
});
});
describe('providing lad telemetry', function () {
let responseDatums;
let resultsShouldBe;
beforeEach(function () {
openmct.telemetry.request.and.callFake(function (rObj, options) {
expect(rObj).toEqual(jasmine.any(Object));
expect(options).toEqual({
size: 1, size: 1,
strategy: 'latest', strategy: 'latest',
domain: 'timestamp' domain: 'timestamp'
})
.then(function (r) {
expect(r).toEqual(results);
}); });
expect(responseDatums[rObj.identifier.namespace]).toBeDefined(); };
});
return Promise.resolve([responseDatums[rObj.identifier.namespace]]); it('returns default when no rule matches', function () {
}); responseDatums = {
responseDatums = {}; a: {
t: 122,
m: 25
},
b: {
t: 111,
m: 25
}
};
resultsShouldBe = function (results) { return resultsShouldBe([
return telemetryProvider {
.request(summaryWidgetObject, { timestamp: 122,
size: 1, ruleLabel: "Don't Worry",
strategy: 'latest', ruleName: 'safe',
domain: 'timestamp' message: "It's Ok",
}) ruleIndex: 0,
.then(function (r) { backgroundColor: '#38761d',
expect(r).toEqual(results); textColor: '#ffffff',
}); borderColor: 'rgba(0,0,0,0)',
}; icon: 'a-ok'
}); }
]);
});
it('returns default when no rule matches', function () { it('returns highest priority when multiple match', function () {
responseDatums = { responseDatums = {
a: { a: {
t: 122, t: 131,
m: 25 m: 55
}, },
b: { b: {
t: 111, t: 139,
m: 25 m: 5
} }
}; };
return resultsShouldBe([ return resultsShouldBe([
{ {
timestamp: 122, timestamp: 139,
ruleLabel: "Don't Worry", ruleLabel: 'WORRY!',
ruleName: 'safe', ruleName: 'B Low',
message: "It's Ok", message: 'B is Low',
ruleIndex: 0, ruleIndex: 2,
backgroundColor: '#38761d', backgroundColor: '#ff0000',
textColor: '#ffffff', textColor: '#ff00ff',
borderColor: 'rgba(0,0,0,0)', borderColor: 'rgba(1,0,0,0)',
icon: 'a-ok' icon: 'b-low'
} }
]); ]);
}); });
it('returns highest priority when multiple match', function () { it('returns matching rule', function () {
responseDatums = { responseDatums = {
a: { a: {
t: 131, t: 144,
m: 55 m: 55
}, },
b: { b: {
t: 139, t: 141,
m: 5 m: 15
} }
}; };
return resultsShouldBe([ return resultsShouldBe([
{ {
timestamp: 139, timestamp: 144,
ruleLabel: 'WORRY!', ruleLabel: 'Start Worrying',
ruleName: 'B Low', ruleName: 'A High',
message: 'B is Low', message: 'A is a little high...',
ruleIndex: 2, ruleIndex: 1,
backgroundColor: '#ff0000', backgroundColor: '#ffff00',
textColor: '#ff00ff', textColor: '#000000',
borderColor: 'rgba(1,0,0,0)', borderColor: 'rgba(1,1,0,0)',
icon: 'b-low' icon: 'a-high'
} }
]); ]);
});
it('returns matching rule', function () {
responseDatums = {
a: {
t: 144,
m: 55
},
b: {
t: 141,
m: 15
}
};
return resultsShouldBe([
{
timestamp: 144,
ruleLabel: 'Start Worrying',
ruleName: 'A High',
message: 'A is a little high...',
ruleIndex: 1,
backgroundColor: '#ffff00',
textColor: '#000000',
borderColor: 'rgba(1,1,0,0)',
icon: 'a-high'
}
]);
});
}); });
}); });
}); });

View File

@ -20,196 +20,194 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { const OPERATIONS = {
const OPERATIONS = { equalTo: {
equalTo: { operation: function (input) {
operation: function (input) { return input[0] === input[1];
return input[0] === input[1];
},
text: 'is equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' == ' + values[0];
}
}, },
notEqualTo: { text: 'is equal to',
operation: function (input) { appliesTo: ['number'],
return input[0] !== input[1]; inputCount: 1,
}, getDescription: function (values) {
text: 'is not equal to', return ' == ' + values[0];
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' != ' + values[0];
}
},
greaterThan: {
operation: function (input) {
return input[0] > input[1];
},
text: 'is greater than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' > ' + values[0];
}
},
lessThan: {
operation: function (input) {
return input[0] < input[1];
},
text: 'is less than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' < ' + values[0];
}
},
greaterThanOrEq: {
operation: function (input) {
return input[0] >= input[1];
},
text: 'is greater than or equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' >= ' + values[0];
}
},
lessThanOrEq: {
operation: function (input) {
return input[0] <= input[1];
},
text: 'is less than or equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' <= ' + values[0];
}
},
between: {
operation: function (input) {
return input[0] > input[1] && input[0] < input[2];
},
text: 'is between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' between ' + values[0] + ' and ' + values[1];
}
},
notBetween: {
operation: function (input) {
return input[0] < input[1] || input[0] > input[2];
},
text: 'is not between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' not between ' + values[0] + ' and ' + values[1];
}
},
textContains: {
operation: function (input) {
return input[0] && input[1] && input[0].includes(input[1]);
},
text: 'text contains',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' contains ' + values[0];
}
},
textDoesNotContain: {
operation: function (input) {
return input[0] && input[1] && !input[0].includes(input[1]);
},
text: 'text does not contain',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' does not contain ' + values[0];
}
},
textStartsWith: {
operation: function (input) {
return input[0].startsWith(input[1]);
},
text: 'text starts with',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' starts with ' + values[0];
}
},
textEndsWith: {
operation: function (input) {
return input[0].endsWith(input[1]);
},
text: 'text ends with',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' ends with ' + values[0];
}
},
textIsExactly: {
operation: function (input) {
return input[0] === input[1];
},
text: 'text is exactly',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' is exactly ' + values[0];
}
},
isUndefined: {
operation: function (input) {
return typeof input[0] === 'undefined';
},
text: 'is undefined',
appliesTo: ['string', 'number', 'enum'],
inputCount: 0,
getDescription: function () {
return ' is undefined';
}
},
isDefined: {
operation: function (input) {
return typeof input[0] !== 'undefined';
},
text: 'is defined',
appliesTo: ['string', 'number', 'enum'],
inputCount: 0,
getDescription: function () {
return ' is defined';
}
},
enumValueIs: {
operation: function (input) {
return input[0] === input[1];
},
text: 'is',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' == ' + values[0];
}
},
enumValueIsNot: {
operation: function (input) {
return input[0] !== input[1];
},
text: 'is not',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' != ' + values[0];
}
} }
}; },
notEqualTo: {
operation: function (input) {
return input[0] !== input[1];
},
text: 'is not equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' != ' + values[0];
}
},
greaterThan: {
operation: function (input) {
return input[0] > input[1];
},
text: 'is greater than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' > ' + values[0];
}
},
lessThan: {
operation: function (input) {
return input[0] < input[1];
},
text: 'is less than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' < ' + values[0];
}
},
greaterThanOrEq: {
operation: function (input) {
return input[0] >= input[1];
},
text: 'is greater than or equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' >= ' + values[0];
}
},
lessThanOrEq: {
operation: function (input) {
return input[0] <= input[1];
},
text: 'is less than or equal to',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' <= ' + values[0];
}
},
between: {
operation: function (input) {
return input[0] > input[1] && input[0] < input[2];
},
text: 'is between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' between ' + values[0] + ' and ' + values[1];
}
},
notBetween: {
operation: function (input) {
return input[0] < input[1] || input[0] > input[2];
},
text: 'is not between',
appliesTo: ['number'],
inputCount: 2,
getDescription: function (values) {
return ' not between ' + values[0] + ' and ' + values[1];
}
},
textContains: {
operation: function (input) {
return input[0] && input[1] && input[0].includes(input[1]);
},
text: 'text contains',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' contains ' + values[0];
}
},
textDoesNotContain: {
operation: function (input) {
return input[0] && input[1] && !input[0].includes(input[1]);
},
text: 'text does not contain',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' does not contain ' + values[0];
}
},
textStartsWith: {
operation: function (input) {
return input[0].startsWith(input[1]);
},
text: 'text starts with',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' starts with ' + values[0];
}
},
textEndsWith: {
operation: function (input) {
return input[0].endsWith(input[1]);
},
text: 'text ends with',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' ends with ' + values[0];
}
},
textIsExactly: {
operation: function (input) {
return input[0] === input[1];
},
text: 'text is exactly',
appliesTo: ['string'],
inputCount: 1,
getDescription: function (values) {
return ' is exactly ' + values[0];
}
},
isUndefined: {
operation: function (input) {
return typeof input[0] === 'undefined';
},
text: 'is undefined',
appliesTo: ['string', 'number', 'enum'],
inputCount: 0,
getDescription: function () {
return ' is undefined';
}
},
isDefined: {
operation: function (input) {
return typeof input[0] !== 'undefined';
},
text: 'is defined',
appliesTo: ['string', 'number', 'enum'],
inputCount: 0,
getDescription: function () {
return ' is defined';
}
},
enumValueIs: {
operation: function (input) {
return input[0] === input[1];
},
text: 'is',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' == ' + values[0];
}
},
enumValueIsNot: {
operation: function (input) {
return input[0] !== input[1];
},
text: 'is not',
appliesTo: ['enum'],
inputCount: 1,
getDescription: function (values) {
return ' != ' + values[0];
}
}
};
return OPERATIONS; export default OPERATIONS;
});

View File

@ -1,38 +1,52 @@
define(['../SummaryWidget', './SummaryWidgetView', 'objectUtils'], function ( /*****************************************************************************
SummaryWidgetEditView, * Open MCT, Copyright (c) 2014-2023, United States Government
SummaryWidgetView, * as represented by the Administrator of the National Aeronautics and Space
objectUtils * Administration. All rights reserved.
) { *
const DEFAULT_VIEW_PRIORITY = 100; * 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.
function SummaryWidgetViewProvider(openmct) { *
return { * Unless required by applicable law or agreed to in writing, software
key: 'summary-widget-viewer', * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
name: 'Summary View', * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
cssClass: 'icon-summary-widget', * License for the specific language governing permissions and limitations
canView: function (domainObject) { * under the License.
return domainObject.type === 'summary-widget'; *
}, * Open MCT includes source code licensed under additional open source
canEdit: function (domainObject) { * licenses. See the Open Source Licenses file (LICENSES.md) included with
return domainObject.type === 'summary-widget'; * this source code distribution or the Licensing information page available
}, * at runtime from the About dialog for additional information.
view: function (domainObject) { *****************************************************************************/
return new SummaryWidgetView(domainObject, openmct);
},
edit: function (domainObject) {
return new SummaryWidgetEditView(domainObject, openmct);
},
priority: function (domainObject) {
if (domainObject.type === 'summary-widget') {
return Number.MAX_VALUE;
} else {
return DEFAULT_VIEW_PRIORITY;
}
}
};
}
return SummaryWidgetViewProvider; import SummaryWidgetEditView from '../SummaryWidget';
}); import SummaryWidgetView from './SummaryWidgetView';
const DEFAULT_VIEW_PRIORITY = 100;
export default function SummaryWidgetViewProvider(openmct) {
return {
key: 'summary-widget-viewer',
name: 'Summary View',
cssClass: 'icon-summary-widget',
canView: function (domainObject) {
return domainObject.type === 'summary-widget';
},
canEdit: function (domainObject) {
return domainObject.type === 'summary-widget';
},
view: function (domainObject) {
return new SummaryWidgetView(domainObject, openmct);
},
edit: function (domainObject) {
return new SummaryWidgetEditView(domainObject, openmct);
},
priority: function (domainObject) {
if (domainObject.type === 'summary-widget') {
return Number.MAX_VALUE;
} else {
return DEFAULT_VIEW_PRIORITY;
}
}
};
}

View File

@ -1,363 +1,363 @@
define(['../src/ConditionEvaluator'], function (ConditionEvaluator) { import ConditionEvaluator from '../src/ConditionEvaluator';
describe('A Summary Widget Rule Evaluator', function () {
let evaluator;
let testEvaluator;
let testOperation;
let mockCache;
let mockTestCache;
let mockComposition;
let mockConditions;
let mockConditionsEmpty;
let mockConditionsUndefined;
let mockConditionsAnyTrue;
let mockConditionsAllTrue;
let mockConditionsAnyFalse;
let mockConditionsAllFalse;
let mockOperations;
beforeEach(function () { describe('A Summary Widget Rule Evaluator', function () {
mockCache = { let evaluator;
a: { let testEvaluator;
alpha: 3, let testOperation;
beta: 9, let mockCache;
gamma: 'Testing 1 2 3' let mockTestCache;
}, let mockComposition;
b: { let mockConditions;
alpha: 44, let mockConditionsEmpty;
beta: 23, let mockConditionsUndefined;
gamma: 'Hello World' let mockConditionsAnyTrue;
}, let mockConditionsAllTrue;
c: { let mockConditionsAnyFalse;
foo: 'bar', let mockConditionsAllFalse;
iAm: 'The Walrus', let mockOperations;
creature: {
type: 'Centaur'
}
}
};
mockTestCache = {
a: {
alpha: 1,
beta: 1,
gamma: 'Testing 4 5 6'
},
b: {
alpha: 2,
beta: 2,
gamma: 'Goodbye world'
}
};
mockComposition = {
a: {},
b: {},
c: {}
};
mockConditions = [
{
object: 'a',
key: 'alpha',
operation: 'greaterThan',
values: [2]
},
{
object: 'b',
key: 'gamma',
operation: 'lessThan',
values: [5]
}
];
mockConditionsEmpty = [
{
object: '',
key: '',
operation: '',
values: []
}
];
mockConditionsUndefined = [
{
object: 'No Such Object',
key: '',
operation: '',
values: []
},
{
object: 'a',
key: 'No Such Key',
operation: '',
values: []
},
{
object: 'a',
key: 'alpha',
operation: 'No Such Operation',
values: []
},
{
object: 'all',
key: 'Nonexistent Field',
operation: 'Random Operation',
values: []
},
{
object: 'any',
key: 'Nonexistent Field',
operation: 'Whatever Operation',
values: []
}
];
mockConditionsAnyTrue = [
{
object: 'any',
key: 'alpha',
operation: 'greaterThan',
values: [5]
}
];
mockConditionsAnyFalse = [
{
object: 'any',
key: 'alpha',
operation: 'greaterThan',
values: [1000]
}
];
mockConditionsAllFalse = [
{
object: 'all',
key: 'alpha',
operation: 'greaterThan',
values: [5]
}
];
mockConditionsAllTrue = [
{
object: 'all',
key: 'alpha',
operation: 'greaterThan',
values: [0]
}
];
mockOperations = {
greaterThan: {
operation: function (input) {
return input[0] > input[1];
},
text: 'is greater than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' > ' + values[0];
}
},
lessThan: {
operation: function (input) {
return input[0] < input[1];
},
text: 'is less than',
appliesTo: ['number'],
inputCount: 1
},
textContains: {
operation: function (input) {
return input[0] && input[1] && input[0].includes(input[1]);
},
text: 'text contains',
appliesTo: ['string'],
inputCount: 1
},
textIsExactly: {
operation: function (input) {
return input[0] === input[1];
},
text: 'text is exactly',
appliesTo: ['string'],
inputCount: 1
},
isHalfHorse: {
operation: function (input) {
return input[0].type === 'Centaur';
},
text: 'is Half Horse',
appliesTo: ['mythicalCreature'],
inputCount: 0,
getDescription: function () {
return 'is half horse';
}
}
};
evaluator = new ConditionEvaluator(mockCache, mockComposition);
testEvaluator = new ConditionEvaluator(mockCache, mockComposition);
evaluator.operations = mockOperations;
});
it('evaluates a condition when it has no configuration', function () { beforeEach(function () {
expect(evaluator.execute(mockConditionsEmpty, 'any')).toEqual(false); mockCache = {
expect(evaluator.execute(mockConditionsEmpty, 'all')).toEqual(false); a: {
}); alpha: 3,
beta: 9,
it('correctly evaluates a set of conditions', function () { gamma: 'Testing 1 2 3'
expect(evaluator.execute(mockConditions, 'any')).toEqual(true); },
expect(evaluator.execute(mockConditions, 'all')).toEqual(false); b: {
}); alpha: 44,
beta: 23,
it('correctly evaluates conditions involving "any telemetry"', function () { gamma: 'Hello World'
expect(evaluator.execute(mockConditionsAnyTrue, 'any')).toEqual(true); },
expect(evaluator.execute(mockConditionsAnyFalse, 'any')).toEqual(false); c: {
}); foo: 'bar',
iAm: 'The Walrus',
it('correctly evaluates conditions involving "all telemetry"', function () { creature: {
expect(evaluator.execute(mockConditionsAllTrue, 'any')).toEqual(true); type: 'Centaur'
expect(evaluator.execute(mockConditionsAllFalse, 'any')).toEqual(false); }
}); }
};
it('handles malformed conditions gracefully', function () { mockTestCache = {
//if no conditions are fully defined, should return false for any mode a: {
expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(false); alpha: 1,
expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(false); beta: 1,
//these conditions are true: evaluator should ignore undefined conditions, gamma: 'Testing 4 5 6'
//and evaluate the rule as true },
mockConditionsUndefined.push({ b: {
alpha: 2,
beta: 2,
gamma: 'Goodbye world'
}
};
mockComposition = {
a: {},
b: {},
c: {}
};
mockConditions = [
{
object: 'a', object: 'a',
key: 'alpha',
operation: 'greaterThan',
values: [2]
},
{
object: 'b',
key: 'gamma', key: 'gamma',
operation: 'textContains', operation: 'lessThan',
values: ['Testing'] values: [5]
}); }
expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(true); ];
mockConditionsUndefined.push({ mockConditionsEmpty = [
object: 'c', {
key: 'iAm', object: '',
operation: 'textContains', key: '',
values: ['Walrus'] operation: '',
}); values: []
expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(true); }
}); ];
mockConditionsUndefined = [
it('gets the keys for possible operations', function () { {
expect(evaluator.getOperationKeys()).toEqual([ object: 'No Such Object',
'greaterThan', key: '',
'lessThan', operation: '',
'textContains', values: []
'textIsExactly', },
'isHalfHorse' {
]);
});
it('gets output text for a given operation', function () {
expect(evaluator.getOperationText('isHalfHorse')).toEqual('is Half Horse');
});
it('correctly returns whether an operation applies to a given type', function () {
expect(evaluator.operationAppliesTo('isHalfHorse', 'mythicalCreature')).toEqual(true);
expect(evaluator.operationAppliesTo('isHalfHorse', 'spaceJunk')).toEqual(false);
});
it('returns the HTML input type associated with a given data type', function () {
expect(evaluator.getInputTypeById('string')).toEqual('text');
});
it('gets the number of inputs required for a given operation', function () {
expect(evaluator.getInputCount('isHalfHorse')).toEqual(0);
expect(evaluator.getInputCount('greaterThan')).toEqual(1);
});
it('gets a human-readable description of a condition', function () {
expect(evaluator.getOperationDescription('isHalfHorse')).toEqual('is half horse');
expect(evaluator.getOperationDescription('greaterThan', [1])).toEqual(' > 1');
});
it('allows setting a substitute cache for testing purposes, and toggling its use', function () {
evaluator.setTestDataCache(mockTestCache);
evaluator.useTestData(true);
expect(evaluator.execute(mockConditions, 'any')).toEqual(false);
expect(evaluator.execute(mockConditions, 'all')).toEqual(false);
mockConditions.push({
object: 'a', object: 'a',
key: 'gamma', key: 'No Such Key',
operation: 'textContains', operation: '',
values: ['4 5 6'] values: []
}); },
expect(evaluator.execute(mockConditions, 'any')).toEqual(true); {
expect(evaluator.execute(mockConditions, 'all')).toEqual(false); object: 'a',
mockConditions.pop(); key: 'alpha',
evaluator.useTestData(false); operation: 'No Such Operation',
expect(evaluator.execute(mockConditions, 'any')).toEqual(true); values: []
expect(evaluator.execute(mockConditions, 'all')).toEqual(false); },
}); {
object: 'all',
key: 'Nonexistent Field',
operation: 'Random Operation',
values: []
},
{
object: 'any',
key: 'Nonexistent Field',
operation: 'Whatever Operation',
values: []
}
];
mockConditionsAnyTrue = [
{
object: 'any',
key: 'alpha',
operation: 'greaterThan',
values: [5]
}
];
mockConditionsAnyFalse = [
{
object: 'any',
key: 'alpha',
operation: 'greaterThan',
values: [1000]
}
];
mockConditionsAllFalse = [
{
object: 'all',
key: 'alpha',
operation: 'greaterThan',
values: [5]
}
];
mockConditionsAllTrue = [
{
object: 'all',
key: 'alpha',
operation: 'greaterThan',
values: [0]
}
];
mockOperations = {
greaterThan: {
operation: function (input) {
return input[0] > input[1];
},
text: 'is greater than',
appliesTo: ['number'],
inputCount: 1,
getDescription: function (values) {
return ' > ' + values[0];
}
},
lessThan: {
operation: function (input) {
return input[0] < input[1];
},
text: 'is less than',
appliesTo: ['number'],
inputCount: 1
},
textContains: {
operation: function (input) {
return input[0] && input[1] && input[0].includes(input[1]);
},
text: 'text contains',
appliesTo: ['string'],
inputCount: 1
},
textIsExactly: {
operation: function (input) {
return input[0] === input[1];
},
text: 'text is exactly',
appliesTo: ['string'],
inputCount: 1
},
isHalfHorse: {
operation: function (input) {
return input[0].type === 'Centaur';
},
text: 'is Half Horse',
appliesTo: ['mythicalCreature'],
inputCount: 0,
getDescription: function () {
return 'is half horse';
}
}
};
evaluator = new ConditionEvaluator(mockCache, mockComposition);
testEvaluator = new ConditionEvaluator(mockCache, mockComposition);
evaluator.operations = mockOperations;
});
it('supports all required operations', function () { it('evaluates a condition when it has no configuration', function () {
//equal to expect(evaluator.execute(mockConditionsEmpty, 'any')).toEqual(false);
testOperation = testEvaluator.operations.equalTo.operation; expect(evaluator.execute(mockConditionsEmpty, 'all')).toEqual(false);
expect(testOperation([33, 33])).toEqual(true); });
expect(testOperation([55, 147])).toEqual(false);
//not equal to
testOperation = testEvaluator.operations.notEqualTo.operation;
expect(testOperation([33, 33])).toEqual(false);
expect(testOperation([55, 147])).toEqual(true);
//greater than
testOperation = testEvaluator.operations.greaterThan.operation;
expect(testOperation([100, 33])).toEqual(true);
expect(testOperation([33, 33])).toEqual(false);
expect(testOperation([55, 147])).toEqual(false);
//less than
testOperation = testEvaluator.operations.lessThan.operation;
expect(testOperation([100, 33])).toEqual(false);
expect(testOperation([33, 33])).toEqual(false);
expect(testOperation([55, 147])).toEqual(true);
//greater than or equal to
testOperation = testEvaluator.operations.greaterThanOrEq.operation;
expect(testOperation([100, 33])).toEqual(true);
expect(testOperation([33, 33])).toEqual(true);
expect(testOperation([55, 147])).toEqual(false);
//less than or equal to
testOperation = testEvaluator.operations.lessThanOrEq.operation;
expect(testOperation([100, 33])).toEqual(false);
expect(testOperation([33, 33])).toEqual(true);
expect(testOperation([55, 147])).toEqual(true);
//between
testOperation = testEvaluator.operations.between.operation;
expect(testOperation([100, 33, 66])).toEqual(false);
expect(testOperation([1, 33, 66])).toEqual(false);
expect(testOperation([45, 33, 66])).toEqual(true);
//not between
testOperation = testEvaluator.operations.notBetween.operation;
expect(testOperation([100, 33, 66])).toEqual(true);
expect(testOperation([1, 33, 66])).toEqual(true);
expect(testOperation([45, 33, 66])).toEqual(false);
//text contains
testOperation = testEvaluator.operations.textContains.operation;
expect(testOperation(['Testing', 'tin'])).toEqual(true);
expect(testOperation(['Testing', 'bind'])).toEqual(false);
//text does not contain
testOperation = testEvaluator.operations.textDoesNotContain.operation;
expect(testOperation(['Testing', 'tin'])).toEqual(false);
expect(testOperation(['Testing', 'bind'])).toEqual(true);
//text starts with
testOperation = testEvaluator.operations.textStartsWith.operation;
expect(testOperation(['Testing', 'Tes'])).toEqual(true);
expect(testOperation(['Testing', 'ting'])).toEqual(false);
//text ends with
testOperation = testEvaluator.operations.textEndsWith.operation;
expect(testOperation(['Testing', 'Tes'])).toEqual(false);
expect(testOperation(['Testing', 'ting'])).toEqual(true);
//text is exactly
testOperation = testEvaluator.operations.textIsExactly.operation;
expect(testOperation(['Testing', 'Testing'])).toEqual(true);
expect(testOperation(['Testing', 'Test'])).toEqual(false);
//undefined
testOperation = testEvaluator.operations.isUndefined.operation;
expect(testOperation([1])).toEqual(false);
expect(testOperation([])).toEqual(true);
//isDefined
testOperation = testEvaluator.operations.isDefined.operation;
expect(testOperation([1])).toEqual(true);
expect(testOperation([])).toEqual(false);
});
it('can produce a description for all supported operations', function () { it('correctly evaluates a set of conditions', function () {
testEvaluator.getOperationKeys().forEach(function (key) { expect(evaluator.execute(mockConditions, 'any')).toEqual(true);
expect(testEvaluator.getOperationDescription(key, [])).toBeDefined(); expect(evaluator.execute(mockConditions, 'all')).toEqual(false);
}); });
it('correctly evaluates conditions involving "any telemetry"', function () {
expect(evaluator.execute(mockConditionsAnyTrue, 'any')).toEqual(true);
expect(evaluator.execute(mockConditionsAnyFalse, 'any')).toEqual(false);
});
it('correctly evaluates conditions involving "all telemetry"', function () {
expect(evaluator.execute(mockConditionsAllTrue, 'any')).toEqual(true);
expect(evaluator.execute(mockConditionsAllFalse, 'any')).toEqual(false);
});
it('handles malformed conditions gracefully', function () {
//if no conditions are fully defined, should return false for any mode
expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(false);
expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(false);
//these conditions are true: evaluator should ignore undefined conditions,
//and evaluate the rule as true
mockConditionsUndefined.push({
object: 'a',
key: 'gamma',
operation: 'textContains',
values: ['Testing']
});
expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(true);
mockConditionsUndefined.push({
object: 'c',
key: 'iAm',
operation: 'textContains',
values: ['Walrus']
});
expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(true);
});
it('gets the keys for possible operations', function () {
expect(evaluator.getOperationKeys()).toEqual([
'greaterThan',
'lessThan',
'textContains',
'textIsExactly',
'isHalfHorse'
]);
});
it('gets output text for a given operation', function () {
expect(evaluator.getOperationText('isHalfHorse')).toEqual('is Half Horse');
});
it('correctly returns whether an operation applies to a given type', function () {
expect(evaluator.operationAppliesTo('isHalfHorse', 'mythicalCreature')).toEqual(true);
expect(evaluator.operationAppliesTo('isHalfHorse', 'spaceJunk')).toEqual(false);
});
it('returns the HTML input type associated with a given data type', function () {
expect(evaluator.getInputTypeById('string')).toEqual('text');
});
it('gets the number of inputs required for a given operation', function () {
expect(evaluator.getInputCount('isHalfHorse')).toEqual(0);
expect(evaluator.getInputCount('greaterThan')).toEqual(1);
});
it('gets a human-readable description of a condition', function () {
expect(evaluator.getOperationDescription('isHalfHorse')).toEqual('is half horse');
expect(evaluator.getOperationDescription('greaterThan', [1])).toEqual(' > 1');
});
it('allows setting a substitute cache for testing purposes, and toggling its use', function () {
evaluator.setTestDataCache(mockTestCache);
evaluator.useTestData(true);
expect(evaluator.execute(mockConditions, 'any')).toEqual(false);
expect(evaluator.execute(mockConditions, 'all')).toEqual(false);
mockConditions.push({
object: 'a',
key: 'gamma',
operation: 'textContains',
values: ['4 5 6']
});
expect(evaluator.execute(mockConditions, 'any')).toEqual(true);
expect(evaluator.execute(mockConditions, 'all')).toEqual(false);
mockConditions.pop();
evaluator.useTestData(false);
expect(evaluator.execute(mockConditions, 'any')).toEqual(true);
expect(evaluator.execute(mockConditions, 'all')).toEqual(false);
});
it('supports all required operations', function () {
//equal to
testOperation = testEvaluator.operations.equalTo.operation;
expect(testOperation([33, 33])).toEqual(true);
expect(testOperation([55, 147])).toEqual(false);
//not equal to
testOperation = testEvaluator.operations.notEqualTo.operation;
expect(testOperation([33, 33])).toEqual(false);
expect(testOperation([55, 147])).toEqual(true);
//greater than
testOperation = testEvaluator.operations.greaterThan.operation;
expect(testOperation([100, 33])).toEqual(true);
expect(testOperation([33, 33])).toEqual(false);
expect(testOperation([55, 147])).toEqual(false);
//less than
testOperation = testEvaluator.operations.lessThan.operation;
expect(testOperation([100, 33])).toEqual(false);
expect(testOperation([33, 33])).toEqual(false);
expect(testOperation([55, 147])).toEqual(true);
//greater than or equal to
testOperation = testEvaluator.operations.greaterThanOrEq.operation;
expect(testOperation([100, 33])).toEqual(true);
expect(testOperation([33, 33])).toEqual(true);
expect(testOperation([55, 147])).toEqual(false);
//less than or equal to
testOperation = testEvaluator.operations.lessThanOrEq.operation;
expect(testOperation([100, 33])).toEqual(false);
expect(testOperation([33, 33])).toEqual(true);
expect(testOperation([55, 147])).toEqual(true);
//between
testOperation = testEvaluator.operations.between.operation;
expect(testOperation([100, 33, 66])).toEqual(false);
expect(testOperation([1, 33, 66])).toEqual(false);
expect(testOperation([45, 33, 66])).toEqual(true);
//not between
testOperation = testEvaluator.operations.notBetween.operation;
expect(testOperation([100, 33, 66])).toEqual(true);
expect(testOperation([1, 33, 66])).toEqual(true);
expect(testOperation([45, 33, 66])).toEqual(false);
//text contains
testOperation = testEvaluator.operations.textContains.operation;
expect(testOperation(['Testing', 'tin'])).toEqual(true);
expect(testOperation(['Testing', 'bind'])).toEqual(false);
//text does not contain
testOperation = testEvaluator.operations.textDoesNotContain.operation;
expect(testOperation(['Testing', 'tin'])).toEqual(false);
expect(testOperation(['Testing', 'bind'])).toEqual(true);
//text starts with
testOperation = testEvaluator.operations.textStartsWith.operation;
expect(testOperation(['Testing', 'Tes'])).toEqual(true);
expect(testOperation(['Testing', 'ting'])).toEqual(false);
//text ends with
testOperation = testEvaluator.operations.textEndsWith.operation;
expect(testOperation(['Testing', 'Tes'])).toEqual(false);
expect(testOperation(['Testing', 'ting'])).toEqual(true);
//text is exactly
testOperation = testEvaluator.operations.textIsExactly.operation;
expect(testOperation(['Testing', 'Testing'])).toEqual(true);
expect(testOperation(['Testing', 'Test'])).toEqual(false);
//undefined
testOperation = testEvaluator.operations.isUndefined.operation;
expect(testOperation([1])).toEqual(false);
expect(testOperation([])).toEqual(true);
//isDefined
testOperation = testEvaluator.operations.isDefined.operation;
expect(testOperation([1])).toEqual(true);
expect(testOperation([])).toEqual(false);
});
it('can produce a description for all supported operations', function () {
testEvaluator.getOperationKeys().forEach(function (key) {
expect(testEvaluator.getOperationDescription(key, [])).toBeDefined();
}); });
}); });
}); });

View File

@ -20,270 +20,71 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['../src/ConditionManager'], function (ConditionManager) { import ConditionManager from '../src/ConditionManager';
describe('A Summary Widget Condition Manager', function () {
let conditionManager;
let mockDomainObject;
let mockCompObject1;
let mockCompObject2;
let mockCompObject3;
let mockMetadata;
let mockTelemetryCallbacks;
let mockEventCallbacks;
let unsubscribeSpies;
let unregisterSpies;
let mockMetadataManagers;
let mockComposition;
let mockOpenMCT;
let mockTelemetryAPI;
let addCallbackSpy;
let loadCallbackSpy;
let removeCallbackSpy;
let telemetryCallbackSpy;
let metadataCallbackSpy;
let telemetryRequests;
let mockTelemetryValues;
let mockTelemetryValues2;
let mockConditionEvaluator;
beforeEach(function () { describe('A Summary Widget Condition Manager', function () {
mockDomainObject = { let conditionManager;
identifier: { let mockDomainObject;
key: 'testKey' let mockCompObject1;
}, let mockCompObject2;
name: 'Test Object', let mockCompObject3;
composition: [ let mockMetadata;
{ let mockTelemetryCallbacks;
mockCompObject1: { let mockEventCallbacks;
key: 'mockCompObject1' let unsubscribeSpies;
}, let unregisterSpies;
mockCompObject2: { let mockMetadataManagers;
key: 'mockCompObject2' let mockComposition;
} let mockOpenMCT;
} let mockTelemetryAPI;
], let addCallbackSpy;
configuration: {} let loadCallbackSpy;
}; let removeCallbackSpy;
mockCompObject1 = { let telemetryCallbackSpy;
identifier: { let metadataCallbackSpy;
key: 'mockCompObject1' let telemetryRequests;
}, let mockTelemetryValues;
name: 'Object 1' let mockTelemetryValues2;
}; let mockConditionEvaluator;
mockCompObject2 = {
identifier: { beforeEach(function () {
key: 'mockCompObject2' mockDomainObject = {
}, identifier: {
name: 'Object 2' key: 'testKey'
}; },
mockCompObject3 = { name: 'Test Object',
identifier: { composition: [
key: 'mockCompObject3' {
}, mockCompObject1: {
name: 'Object 3' key: 'mockCompObject1'
};
mockMetadata = {
mockCompObject1: {
property1: {
key: 'property1',
name: 'Property 1',
format: 'string',
hints: {}
}, },
property2: { mockCompObject2: {
key: 'property2', key: 'mockCompObject2'
name: 'Property 2',
hints: {
domain: 1
}
}
},
mockCompObject2: {
property3: {
key: 'property3',
name: 'Property 3',
format: 'string',
hints: {}
},
property4: {
key: 'property4',
name: 'Property 4',
hints: {
range: 1
}
}
},
mockCompObject3: {
property1: {
key: 'property1',
name: 'Property 1',
hints: {}
},
property2: {
key: 'property2',
name: 'Property 2',
hints: {}
} }
} }
}; ],
mockTelemetryCallbacks = {}; configuration: {}
mockEventCallbacks = {}; };
unsubscribeSpies = jasmine.createSpyObj('mockUnsubscribeFunction', [ mockCompObject1 = {
'mockCompObject1', identifier: {
'mockCompObject2', key: 'mockCompObject1'
'mockCompObject3' },
]); name: 'Object 1'
unregisterSpies = jasmine.createSpyObj('mockUnregisterFunctions', ['load', 'remove', 'add']); };
mockTelemetryValues = { mockCompObject2 = {
mockCompObject1: { identifier: {
property1: 'Its a string', key: 'mockCompObject2'
property2: 42 },
}, name: 'Object 2'
mockCompObject2: { };
property3: 'Execute order:', mockCompObject3 = {
property4: 66 identifier: {
}, key: 'mockCompObject3'
mockCompObject3: { },
property1: 'Testing 1 2 3', name: 'Object 3'
property2: 9000 };
} mockMetadata = {
}; mockCompObject1: {
mockTelemetryValues2 = {
mockCompObject1: {
property1: 'Its a different string',
property2: 44
},
mockCompObject2: {
property3: 'Execute catch:',
property4: 22
},
mockCompObject3: {
property1: 'Walrus',
property2: 22
}
};
mockMetadataManagers = {
mockCompObject1: {
values: jasmine
.createSpy('metadataManager')
.and.returnValue(Object.values(mockMetadata.mockCompObject1))
},
mockCompObject2: {
values: jasmine
.createSpy('metadataManager')
.and.returnValue(Object.values(mockMetadata.mockCompObject2))
},
mockCompObject3: {
values: jasmine
.createSpy('metadataManager')
.and.returnValue(Object.values(mockMetadata.mockCompObject2))
}
};
mockComposition = jasmine.createSpyObj('composition', [
'on',
'off',
'load',
'triggerCallback'
]);
mockComposition.on.and.callFake(function (event, callback, context) {
mockEventCallbacks[event] = callback.bind(context);
});
mockComposition.off.and.callFake(function (event) {
unregisterSpies[event]();
});
mockComposition.load.and.callFake(function () {
mockComposition.triggerCallback('add', mockCompObject1);
mockComposition.triggerCallback('add', mockCompObject2);
mockComposition.triggerCallback('load');
});
mockComposition.triggerCallback.and.callFake(function (event, obj) {
if (event === 'add') {
mockEventCallbacks.add(obj);
} else if (event === 'remove') {
mockEventCallbacks.remove(obj.identifier);
} else {
mockEventCallbacks[event]();
}
});
telemetryRequests = [];
mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [
'request',
'isTelemetryObject',
'getMetadata',
'subscribe',
'triggerTelemetryCallback'
]);
mockTelemetryAPI.request.and.callFake(function (obj) {
const req = {
object: obj
};
req.promise = new Promise(function (resolve, reject) {
req.resolve = resolve;
req.reject = reject;
});
telemetryRequests.push(req);
return req.promise;
});
mockTelemetryAPI.isTelemetryObject.and.returnValue(true);
mockTelemetryAPI.getMetadata.and.callFake(function (obj) {
return mockMetadataManagers[obj.identifier.key];
});
mockTelemetryAPI.subscribe.and.callFake(function (obj, callback) {
mockTelemetryCallbacks[obj.identifier.key] = callback;
return unsubscribeSpies[obj.identifier.key];
});
mockTelemetryAPI.triggerTelemetryCallback.and.callFake(function (key) {
mockTelemetryCallbacks[key](mockTelemetryValues2[key]);
});
mockOpenMCT = {
telemetry: mockTelemetryAPI,
composition: {}
};
mockOpenMCT.composition.get = jasmine.createSpy('get').and.returnValue(mockComposition);
loadCallbackSpy = jasmine.createSpy('loadCallbackSpy');
addCallbackSpy = jasmine.createSpy('addCallbackSpy');
removeCallbackSpy = jasmine.createSpy('removeCallbackSpy');
metadataCallbackSpy = jasmine.createSpy('metadataCallbackSpy');
telemetryCallbackSpy = jasmine.createSpy('telemetryCallbackSpy');
conditionManager = new ConditionManager(mockDomainObject, mockOpenMCT);
conditionManager.on('load', loadCallbackSpy);
conditionManager.on('add', addCallbackSpy);
conditionManager.on('remove', removeCallbackSpy);
conditionManager.on('metadata', metadataCallbackSpy);
conditionManager.on('receiveTelemetry', telemetryCallbackSpy);
mockConditionEvaluator = jasmine.createSpy('mockConditionEvaluator');
mockConditionEvaluator.execute = jasmine.createSpy('execute');
conditionManager.evaluator = mockConditionEvaluator;
});
it('loads the initial composition and invokes the appropriate handlers', function () {
mockComposition.triggerCallback('load');
expect(conditionManager.getComposition()).toEqual({
mockCompObject1: mockCompObject1,
mockCompObject2: mockCompObject2
});
expect(loadCallbackSpy).toHaveBeenCalled();
expect(conditionManager.loadCompleted()).toEqual(true);
});
it('loads metadata from composition and gets it upon request', function () {
expect(conditionManager.getTelemetryMetadata('mockCompObject1')).toEqual(
mockMetadata.mockCompObject1
);
expect(conditionManager.getTelemetryMetadata('mockCompObject2')).toEqual(
mockMetadata.mockCompObject2
);
});
it('maintains lists of global metadata, and does not duplicate repeated fields', function () {
const allKeys = {
property1: { property1: {
key: 'property1', key: 'property1',
name: 'Property 1', name: 'Property 1',
@ -296,7 +97,9 @@ define(['../src/ConditionManager'], function (ConditionManager) {
hints: { hints: {
domain: 1 domain: 1
} }
}, }
},
mockCompObject2: {
property3: { property3: {
key: 'property3', key: 'property3',
name: 'Property 3', name: 'Property 3',
@ -310,134 +113,326 @@ define(['../src/ConditionManager'], function (ConditionManager) {
range: 1 range: 1
} }
} }
}; },
expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); mockCompObject3: {
expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); property1: {
mockComposition.triggerCallback('add', mockCompObject3); key: 'property1',
expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); name: 'Property 1',
expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); hints: {}
}); },
property2: {
it('loads and gets telemetry property types', function () { key: 'property2',
conditionManager.parseAllPropertyTypes(); name: 'Property 2',
expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1')).toEqual( hints: {}
'string'
);
expect(conditionManager.getTelemetryPropertyType('mockCompObject2', 'property4')).toEqual(
'number'
);
expect(conditionManager.metadataLoadCompleted()).toEqual(true);
expect(metadataCallbackSpy).toHaveBeenCalled();
});
it('responds to a composition add event and invokes the appropriate handlers', function () {
mockComposition.triggerCallback('add', mockCompObject3);
expect(addCallbackSpy).toHaveBeenCalledWith(mockCompObject3);
expect(conditionManager.getComposition()).toEqual({
mockCompObject1: mockCompObject1,
mockCompObject2: mockCompObject2,
mockCompObject3: mockCompObject3
});
});
it('responds to a composition remove event and invokes the appropriate handlers', function () {
mockComposition.triggerCallback('remove', mockCompObject2);
expect(removeCallbackSpy).toHaveBeenCalledWith({
key: 'mockCompObject2'
});
expect(unsubscribeSpies.mockCompObject2).toHaveBeenCalled();
expect(conditionManager.getComposition()).toEqual({
mockCompObject1: mockCompObject1
});
});
it('unregisters telemetry subscriptions and composition listeners on destroy', function () {
mockComposition.triggerCallback('add', mockCompObject3);
conditionManager.destroy();
Object.values(unsubscribeSpies).forEach(function (spy) {
expect(spy).toHaveBeenCalled();
});
Object.values(unregisterSpies).forEach(function (spy) {
expect(spy).toHaveBeenCalled();
});
});
xit('populates its LAD cache with historical data on load, if available', function (done) {
expect(telemetryRequests.length).toBe(2);
expect(telemetryRequests[0].object).toBe(mockCompObject1);
expect(telemetryRequests[1].object).toBe(mockCompObject2);
expect(telemetryCallbackSpy).not.toHaveBeenCalled();
telemetryCallbackSpy.and.callFake(function () {
if (telemetryCallbackSpy.calls.count() === 2) {
expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual(
'Its a string'
);
expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(66);
done();
} }
});
telemetryRequests[0].resolve([mockTelemetryValues.mockCompObject1]);
telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]);
});
xit('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () {
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1');
expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual(
'Its a different string'
);
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2');
expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(22);
expect(telemetryCallbackSpy).toHaveBeenCalled();
});
it(
'evaluates a set of rules and returns the id of the ' +
'last active rule, or the first if no rules are active',
function () {
const mockRuleOrder = ['default', 'rule0', 'rule1'];
const mockRules = {
default: {
getProperty: function () {}
},
rule0: {
getProperty: function () {}
},
rule1: {
getProperty: function () {}
}
};
mockConditionEvaluator.execute.and.returnValue(false);
expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('default');
mockConditionEvaluator.execute.and.returnValue(true);
expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('rule1');
} }
};
mockTelemetryCallbacks = {};
mockEventCallbacks = {};
unsubscribeSpies = jasmine.createSpyObj('mockUnsubscribeFunction', [
'mockCompObject1',
'mockCompObject2',
'mockCompObject3'
]);
unregisterSpies = jasmine.createSpyObj('mockUnregisterFunctions', ['load', 'remove', 'add']);
mockTelemetryValues = {
mockCompObject1: {
property1: 'Its a string',
property2: 42
},
mockCompObject2: {
property3: 'Execute order:',
property4: 66
},
mockCompObject3: {
property1: 'Testing 1 2 3',
property2: 9000
}
};
mockTelemetryValues2 = {
mockCompObject1: {
property1: 'Its a different string',
property2: 44
},
mockCompObject2: {
property3: 'Execute catch:',
property4: 22
},
mockCompObject3: {
property1: 'Walrus',
property2: 22
}
};
mockMetadataManagers = {
mockCompObject1: {
values: jasmine
.createSpy('metadataManager')
.and.returnValue(Object.values(mockMetadata.mockCompObject1))
},
mockCompObject2: {
values: jasmine
.createSpy('metadataManager')
.and.returnValue(Object.values(mockMetadata.mockCompObject2))
},
mockCompObject3: {
values: jasmine
.createSpy('metadataManager')
.and.returnValue(Object.values(mockMetadata.mockCompObject2))
}
};
mockComposition = jasmine.createSpyObj('composition', ['on', 'off', 'load', 'triggerCallback']);
mockComposition.on.and.callFake(function (event, callback, context) {
mockEventCallbacks[event] = callback.bind(context);
});
mockComposition.off.and.callFake(function (event) {
unregisterSpies[event]();
});
mockComposition.load.and.callFake(function () {
mockComposition.triggerCallback('add', mockCompObject1);
mockComposition.triggerCallback('add', mockCompObject2);
mockComposition.triggerCallback('load');
});
mockComposition.triggerCallback.and.callFake(function (event, obj) {
if (event === 'add') {
mockEventCallbacks.add(obj);
} else if (event === 'remove') {
mockEventCallbacks.remove(obj.identifier);
} else {
mockEventCallbacks[event]();
}
});
telemetryRequests = [];
mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [
'request',
'isTelemetryObject',
'getMetadata',
'subscribe',
'triggerTelemetryCallback'
]);
mockTelemetryAPI.request.and.callFake(function (obj) {
const req = {
object: obj
};
req.promise = new Promise(function (resolve, reject) {
req.resolve = resolve;
req.reject = reject;
});
telemetryRequests.push(req);
return req.promise;
});
mockTelemetryAPI.isTelemetryObject.and.returnValue(true);
mockTelemetryAPI.getMetadata.and.callFake(function (obj) {
return mockMetadataManagers[obj.identifier.key];
});
mockTelemetryAPI.subscribe.and.callFake(function (obj, callback) {
mockTelemetryCallbacks[obj.identifier.key] = callback;
return unsubscribeSpies[obj.identifier.key];
});
mockTelemetryAPI.triggerTelemetryCallback.and.callFake(function (key) {
mockTelemetryCallbacks[key](mockTelemetryValues2[key]);
});
mockOpenMCT = {
telemetry: mockTelemetryAPI,
composition: {}
};
mockOpenMCT.composition.get = jasmine.createSpy('get').and.returnValue(mockComposition);
loadCallbackSpy = jasmine.createSpy('loadCallbackSpy');
addCallbackSpy = jasmine.createSpy('addCallbackSpy');
removeCallbackSpy = jasmine.createSpy('removeCallbackSpy');
metadataCallbackSpy = jasmine.createSpy('metadataCallbackSpy');
telemetryCallbackSpy = jasmine.createSpy('telemetryCallbackSpy');
conditionManager = new ConditionManager(mockDomainObject, mockOpenMCT);
conditionManager.on('load', loadCallbackSpy);
conditionManager.on('add', addCallbackSpy);
conditionManager.on('remove', removeCallbackSpy);
conditionManager.on('metadata', metadataCallbackSpy);
conditionManager.on('receiveTelemetry', telemetryCallbackSpy);
mockConditionEvaluator = jasmine.createSpy('mockConditionEvaluator');
mockConditionEvaluator.execute = jasmine.createSpy('execute');
conditionManager.evaluator = mockConditionEvaluator;
});
it('loads the initial composition and invokes the appropriate handlers', function () {
mockComposition.triggerCallback('load');
expect(conditionManager.getComposition()).toEqual({
mockCompObject1: mockCompObject1,
mockCompObject2: mockCompObject2
});
expect(loadCallbackSpy).toHaveBeenCalled();
expect(conditionManager.loadCompleted()).toEqual(true);
});
it('loads metadata from composition and gets it upon request', function () {
expect(conditionManager.getTelemetryMetadata('mockCompObject1')).toEqual(
mockMetadata.mockCompObject1
); );
expect(conditionManager.getTelemetryMetadata('mockCompObject2')).toEqual(
mockMetadata.mockCompObject2
);
});
it('gets the human-readable name of a composition object', function () { it('maintains lists of global metadata, and does not duplicate repeated fields', function () {
expect(conditionManager.getObjectName('mockCompObject1')).toEqual('Object 1'); const allKeys = {
expect(conditionManager.getObjectName('all')).toEqual('all Telemetry'); property1: {
}); key: 'property1',
name: 'Property 1',
format: 'string',
hints: {}
},
property2: {
key: 'property2',
name: 'Property 2',
hints: {
domain: 1
}
},
property3: {
key: 'property3',
name: 'Property 3',
format: 'string',
hints: {}
},
property4: {
key: 'property4',
name: 'Property 4',
hints: {
range: 1
}
}
};
expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys);
expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys);
mockComposition.triggerCallback('add', mockCompObject3);
expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys);
expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys);
});
it('gets the human-readable name of a telemetry field', function () { it('loads and gets telemetry property types', function () {
expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1')).toEqual( conditionManager.parseAllPropertyTypes();
'Property 1' expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1')).toEqual(
); 'string'
expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4')).toEqual( );
'Property 4' expect(conditionManager.getTelemetryPropertyType('mockCompObject2', 'property4')).toEqual(
); 'number'
}); );
expect(conditionManager.metadataLoadCompleted()).toEqual(true);
expect(metadataCallbackSpy).toHaveBeenCalled();
});
it('gets its associated ConditionEvaluator', function () { it('responds to a composition add event and invokes the appropriate handlers', function () {
expect(conditionManager.getEvaluator()).toEqual(mockConditionEvaluator); mockComposition.triggerCallback('add', mockCompObject3);
}); expect(addCallbackSpy).toHaveBeenCalledWith(mockCompObject3);
expect(conditionManager.getComposition()).toEqual({
it('allows forcing a receive telemetry event', function () { mockCompObject1: mockCompObject1,
conditionManager.triggerTelemetryCallback(); mockCompObject2: mockCompObject2,
expect(telemetryCallbackSpy).toHaveBeenCalled(); mockCompObject3: mockCompObject3
}); });
}); });
it('responds to a composition remove event and invokes the appropriate handlers', function () {
mockComposition.triggerCallback('remove', mockCompObject2);
expect(removeCallbackSpy).toHaveBeenCalledWith({
key: 'mockCompObject2'
});
expect(unsubscribeSpies.mockCompObject2).toHaveBeenCalled();
expect(conditionManager.getComposition()).toEqual({
mockCompObject1: mockCompObject1
});
});
it('unregisters telemetry subscriptions and composition listeners on destroy', function () {
mockComposition.triggerCallback('add', mockCompObject3);
conditionManager.destroy();
Object.values(unsubscribeSpies).forEach(function (spy) {
expect(spy).toHaveBeenCalled();
});
Object.values(unregisterSpies).forEach(function (spy) {
expect(spy).toHaveBeenCalled();
});
});
xit('populates its LAD cache with historical data on load, if available', function (done) {
expect(telemetryRequests.length).toBe(2);
expect(telemetryRequests[0].object).toBe(mockCompObject1);
expect(telemetryRequests[1].object).toBe(mockCompObject2);
expect(telemetryCallbackSpy).not.toHaveBeenCalled();
telemetryCallbackSpy.and.callFake(function () {
if (telemetryCallbackSpy.calls.count() === 2) {
expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual(
'Its a string'
);
expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(66);
done();
}
});
telemetryRequests[0].resolve([mockTelemetryValues.mockCompObject1]);
telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]);
});
xit('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () {
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1');
expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual(
'Its a different string'
);
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2');
expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(22);
expect(telemetryCallbackSpy).toHaveBeenCalled();
});
it(
'evaluates a set of rules and returns the id of the ' +
'last active rule, or the first if no rules are active',
function () {
const mockRuleOrder = ['default', 'rule0', 'rule1'];
const mockRules = {
default: {
getProperty: function () {}
},
rule0: {
getProperty: function () {}
},
rule1: {
getProperty: function () {}
}
};
mockConditionEvaluator.execute.and.returnValue(false);
expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('default');
mockConditionEvaluator.execute.and.returnValue(true);
expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('rule1');
}
);
it('gets the human-readable name of a composition object', function () {
expect(conditionManager.getObjectName('mockCompObject1')).toEqual('Object 1');
expect(conditionManager.getObjectName('all')).toEqual('all Telemetry');
});
it('gets the human-readable name of a telemetry field', function () {
expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1')).toEqual(
'Property 1'
);
expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4')).toEqual(
'Property 4'
);
});
it('gets its associated ConditionEvaluator', function () {
expect(conditionManager.getEvaluator()).toEqual(mockConditionEvaluator);
});
it('allows forcing a receive telemetry event', function () {
conditionManager.triggerTelemetryCallback();
expect(telemetryCallbackSpy).toHaveBeenCalled();
});
}); });

View File

@ -20,186 +20,186 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['../src/Condition'], function (Condition) { import Condition from '../src/Condition';
describe('A summary widget condition', function () {
let testCondition;
let mockConfig;
let mockConditionManager;
let mockContainer;
let mockEvaluator;
let changeSpy;
let duplicateSpy;
let removeSpy;
let generateValuesSpy;
beforeEach(function () { describe('A summary widget condition', function () {
mockContainer = document.createElement('div'); let testCondition;
let mockConfig;
let mockConditionManager;
let mockContainer;
let mockEvaluator;
let changeSpy;
let duplicateSpy;
let removeSpy;
let generateValuesSpy;
mockConfig = { beforeEach(function () {
object: 'object1', mockContainer = document.createElement('div');
key: 'property1',
operation: 'operation1',
values: [1, 2, 3]
};
mockEvaluator = {}; mockConfig = {
mockEvaluator.getInputCount = jasmine.createSpy('inputCount'); object: 'object1',
mockEvaluator.getInputType = jasmine.createSpy('inputType'); key: 'property1',
operation: 'operation1',
values: [1, 2, 3]
};
mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ mockEvaluator = {};
'on', mockEvaluator.getInputCount = jasmine.createSpy('inputCount');
'getComposition', mockEvaluator.getInputType = jasmine.createSpy('inputType');
'loadCompleted',
'getEvaluator',
'getTelemetryMetadata',
'metadataLoadCompleted',
'getObjectName',
'getTelemetryPropertyName'
]);
mockConditionManager.loadCompleted.and.returnValue(false);
mockConditionManager.metadataLoadCompleted.and.returnValue(false);
mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);
mockConditionManager.getComposition.and.returnValue({});
mockConditionManager.getTelemetryMetadata.and.returnValue({});
mockConditionManager.getObjectName.and.returnValue('Object Name');
mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');
duplicateSpy = jasmine.createSpy('duplicate'); mockConditionManager = jasmine.createSpyObj('mockConditionManager', [
removeSpy = jasmine.createSpy('remove'); 'on',
changeSpy = jasmine.createSpy('change'); 'getComposition',
generateValuesSpy = jasmine.createSpy('generateValueInputs'); 'loadCompleted',
'getEvaluator',
'getTelemetryMetadata',
'metadataLoadCompleted',
'getObjectName',
'getTelemetryPropertyName'
]);
mockConditionManager.loadCompleted.and.returnValue(false);
mockConditionManager.metadataLoadCompleted.and.returnValue(false);
mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);
mockConditionManager.getComposition.and.returnValue({});
mockConditionManager.getTelemetryMetadata.and.returnValue({});
mockConditionManager.getObjectName.and.returnValue('Object Name');
mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');
testCondition = new Condition(mockConfig, 54, mockConditionManager); duplicateSpy = jasmine.createSpy('duplicate');
removeSpy = jasmine.createSpy('remove');
changeSpy = jasmine.createSpy('change');
generateValuesSpy = jasmine.createSpy('generateValueInputs');
testCondition.on('duplicate', duplicateSpy); testCondition = new Condition(mockConfig, 54, mockConditionManager);
testCondition.on('remove', removeSpy);
testCondition.on('change', changeSpy); testCondition.on('duplicate', duplicateSpy);
testCondition.on('remove', removeSpy);
testCondition.on('change', changeSpy);
});
it('exposes a DOM element to represent itself in the view', function () {
mockContainer.append(testCondition.getDOM());
expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(1);
});
it('responds to a change in its object select', function () {
testCondition.selects.object.setSelected('');
expect(changeSpy).toHaveBeenCalledWith({
value: '',
property: 'object',
index: 54
}); });
});
it('exposes a DOM element to represent itself in the view', function () { it('responds to a change in its key select', function () {
mockContainer.append(testCondition.getDOM()); testCondition.selects.key.setSelected('');
expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(1); expect(changeSpy).toHaveBeenCalledWith({
value: '',
property: 'key',
index: 54
}); });
});
it('responds to a change in its object select', function () { it('responds to a change in its operation select', function () {
testCondition.selects.object.setSelected(''); testCondition.generateValueInputs = generateValuesSpy;
expect(changeSpy).toHaveBeenCalledWith({ testCondition.selects.operation.setSelected('');
value: '', expect(changeSpy).toHaveBeenCalledWith({
property: 'object', value: '',
index: 54 property: 'operation',
}); index: 54
}); });
expect(generateValuesSpy).toHaveBeenCalledWith('');
});
it('responds to a change in its key select', function () { it('generates value inputs of the appropriate type and quantity', function () {
testCondition.selects.key.setSelected(''); let inputs;
expect(changeSpy).toHaveBeenCalledWith({
value: '', mockContainer.append(testCondition.getDOM());
property: 'key', mockEvaluator.getInputType.and.returnValue('number');
index: 54 mockEvaluator.getInputCount.and.returnValue(3);
}); testCondition.generateValueInputs('');
inputs = mockContainer.querySelectorAll('input');
const numberInputs = Array.from(inputs).filter((input) => input.type === 'number');
expect(numberInputs.length).toEqual(3);
expect(numberInputs[0].valueAsNumber).toEqual(1);
expect(numberInputs[1].valueAsNumber).toEqual(2);
expect(numberInputs[2].valueAsNumber).toEqual(3);
mockEvaluator.getInputType.and.returnValue('text');
mockEvaluator.getInputCount.and.returnValue(2);
testCondition.config.values = ['Text I Am', 'Text It Is'];
testCondition.generateValueInputs('');
inputs = mockContainer.querySelectorAll('input');
const textInputs = Array.from(inputs).filter((input) => input.type === 'text');
expect(textInputs.length).toEqual(2);
expect(textInputs[0].value).toEqual('Text I Am');
expect(textInputs[1].value).toEqual('Text It Is');
});
it('ensures reasonable defaults on values if none are provided', function () {
let inputs;
mockContainer.append(testCondition.getDOM());
mockEvaluator.getInputType.and.returnValue('number');
mockEvaluator.getInputCount.and.returnValue(3);
testCondition.config.values = [];
testCondition.generateValueInputs('');
inputs = Array.from(mockContainer.querySelectorAll('input'));
expect(inputs[0].valueAsNumber).toEqual(0);
expect(inputs[1].valueAsNumber).toEqual(0);
expect(inputs[2].valueAsNumber).toEqual(0);
expect(testCondition.config.values).toEqual([0, 0, 0]);
mockEvaluator.getInputType.and.returnValue('text');
mockEvaluator.getInputCount.and.returnValue(2);
testCondition.config.values = [];
testCondition.generateValueInputs('');
inputs = Array.from(mockContainer.querySelectorAll('input'));
expect(inputs[0].value).toEqual('');
expect(inputs[1].value).toEqual('');
expect(testCondition.config.values).toEqual(['', '']);
});
it('responds to a change in its value inputs', function () {
mockContainer.append(testCondition.getDOM());
mockEvaluator.getInputType.and.returnValue('number');
mockEvaluator.getInputCount.and.returnValue(3);
testCondition.generateValueInputs('');
const event = new Event('input', {
bubbles: true,
cancelable: true
}); });
const inputs = mockContainer.querySelectorAll('input');
it('responds to a change in its operation select', function () { inputs[1].value = 9001;
testCondition.generateValueInputs = generateValuesSpy; inputs[1].dispatchEvent(event);
testCondition.selects.operation.setSelected('');
expect(changeSpy).toHaveBeenCalledWith({ expect(changeSpy).toHaveBeenCalledWith({
value: '', value: 9001,
property: 'operation', property: 'values[1]',
index: 54 index: 54
});
expect(generateValuesSpy).toHaveBeenCalledWith('');
}); });
});
it('generates value inputs of the appropriate type and quantity', function () { it('can remove itself from the configuration', function () {
let inputs; testCondition.remove();
expect(removeSpy).toHaveBeenCalledWith(54);
});
mockContainer.append(testCondition.getDOM()); it('can duplicate itself', function () {
mockEvaluator.getInputType.and.returnValue('number'); testCondition.duplicate();
mockEvaluator.getInputCount.and.returnValue(3); expect(duplicateSpy).toHaveBeenCalledWith({
testCondition.generateValueInputs(''); sourceCondition: mockConfig,
index: 54
inputs = mockContainer.querySelectorAll('input');
const numberInputs = Array.from(inputs).filter((input) => input.type === 'number');
expect(numberInputs.length).toEqual(3);
expect(numberInputs[0].valueAsNumber).toEqual(1);
expect(numberInputs[1].valueAsNumber).toEqual(2);
expect(numberInputs[2].valueAsNumber).toEqual(3);
mockEvaluator.getInputType.and.returnValue('text');
mockEvaluator.getInputCount.and.returnValue(2);
testCondition.config.values = ['Text I Am', 'Text It Is'];
testCondition.generateValueInputs('');
inputs = mockContainer.querySelectorAll('input');
const textInputs = Array.from(inputs).filter((input) => input.type === 'text');
expect(textInputs.length).toEqual(2);
expect(textInputs[0].value).toEqual('Text I Am');
expect(textInputs[1].value).toEqual('Text It Is');
});
it('ensures reasonable defaults on values if none are provided', function () {
let inputs;
mockContainer.append(testCondition.getDOM());
mockEvaluator.getInputType.and.returnValue('number');
mockEvaluator.getInputCount.and.returnValue(3);
testCondition.config.values = [];
testCondition.generateValueInputs('');
inputs = Array.from(mockContainer.querySelectorAll('input'));
expect(inputs[0].valueAsNumber).toEqual(0);
expect(inputs[1].valueAsNumber).toEqual(0);
expect(inputs[2].valueAsNumber).toEqual(0);
expect(testCondition.config.values).toEqual([0, 0, 0]);
mockEvaluator.getInputType.and.returnValue('text');
mockEvaluator.getInputCount.and.returnValue(2);
testCondition.config.values = [];
testCondition.generateValueInputs('');
inputs = Array.from(mockContainer.querySelectorAll('input'));
expect(inputs[0].value).toEqual('');
expect(inputs[1].value).toEqual('');
expect(testCondition.config.values).toEqual(['', '']);
});
it('responds to a change in its value inputs', function () {
mockContainer.append(testCondition.getDOM());
mockEvaluator.getInputType.and.returnValue('number');
mockEvaluator.getInputCount.and.returnValue(3);
testCondition.generateValueInputs('');
const event = new Event('input', {
bubbles: true,
cancelable: true
});
const inputs = mockContainer.querySelectorAll('input');
inputs[1].value = 9001;
inputs[1].dispatchEvent(event);
expect(changeSpy).toHaveBeenCalledWith({
value: 9001,
property: 'values[1]',
index: 54
});
});
it('can remove itself from the configuration', function () {
testCondition.remove();
expect(removeSpy).toHaveBeenCalledWith(54);
});
it('can duplicate itself', function () {
testCondition.duplicate();
expect(duplicateSpy).toHaveBeenCalledWith({
sourceCondition: mockConfig,
index: 54
});
}); });
}); });
}); });

View File

@ -1,143 +1,53 @@
define(['../src/Rule'], function (Rule) { /*****************************************************************************
describe('A Summary Widget Rule', function () { * Open MCT, Copyright (c) 2014-2023, United States Government
let mockRuleConfig; * as represented by the Administrator of the National Aeronautics and Space
let mockDomainObject; * Administration. All rights reserved.
let mockOpenMCT; *
let mockConditionManager; * Open MCT is licensed under the Apache License, Version 2.0 (the
let mockWidgetDnD; * "License"); you may not use this file except in compliance with the License.
let mockEvaluator; * You may obtain a copy of the License at
let mockContainer; * http://www.apache.org/licenses/LICENSE-2.0.
let testRule; *
let removeSpy; * Unless required by applicable law or agreed to in writing, software
let duplicateSpy; * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
let changeSpy; * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
let conditionChangeSpy; * 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.
*****************************************************************************/
beforeEach(function () { import Rule from '../src/Rule';
mockRuleConfig = {
name: 'Name',
id: 'mockRule',
icon: 'test-icon-name',
style: {
'background-color': '',
'border-color': '',
color: ''
},
expanded: true,
conditions: [
{
object: '',
key: '',
operation: '',
values: []
},
{
object: 'blah',
key: 'blah',
operation: 'blah',
values: ['blah.', 'blah!', 'blah?']
}
]
};
mockDomainObject = {
configuration: {
ruleConfigById: {
mockRule: mockRuleConfig,
otherRule: {}
},
ruleOrder: ['default', 'mockRule', 'otherRule']
}
};
mockOpenMCT = {}; describe('A Summary Widget Rule', function () {
mockOpenMCT.objects = {}; let mockRuleConfig;
mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); let mockDomainObject;
let mockOpenMCT;
let mockConditionManager;
let mockWidgetDnD;
let mockEvaluator;
let mockContainer;
let testRule;
let removeSpy;
let duplicateSpy;
let changeSpy;
let conditionChangeSpy;
mockEvaluator = {}; beforeEach(function () {
mockEvaluator.getOperationDescription = jasmine mockRuleConfig = {
.createSpy('evaluator') name: 'Name',
.and.returnValue('Operation Description'); id: 'mockRule',
icon: 'test-icon-name',
mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ style: {
'on', 'background-color': '',
'getComposition', 'border-color': '',
'loadCompleted', color: ''
'getEvaluator', },
'getTelemetryMetadata', expanded: true,
'metadataLoadCompleted', conditions: [
'getObjectName',
'getTelemetryPropertyName'
]);
mockConditionManager.loadCompleted.and.returnValue(false);
mockConditionManager.metadataLoadCompleted.and.returnValue(false);
mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);
mockConditionManager.getComposition.and.returnValue({});
mockConditionManager.getTelemetryMetadata.and.returnValue({});
mockConditionManager.getObjectName.and.returnValue('Object Name');
mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');
mockWidgetDnD = jasmine.createSpyObj('dnd', ['on', 'setDragImage', 'dragStart']);
mockContainer = document.createElement('div');
removeSpy = jasmine.createSpy('removeCallback');
duplicateSpy = jasmine.createSpy('duplicateCallback');
changeSpy = jasmine.createSpy('changeCallback');
conditionChangeSpy = jasmine.createSpy('conditionChangeCallback');
testRule = new Rule(
mockRuleConfig,
mockDomainObject,
mockOpenMCT,
mockConditionManager,
mockWidgetDnD
);
testRule.on('remove', removeSpy);
testRule.on('duplicate', duplicateSpy);
testRule.on('change', changeSpy);
testRule.on('conditionChange', conditionChangeSpy);
});
it('closes its configuration panel on initial load', function () {
expect(testRule.getProperty('expanded')).toEqual(false);
});
it('gets its DOM element', function () {
mockContainer.append(testRule.getDOM());
expect(mockContainer.querySelectorAll('.l-widget-rule').length).toBeGreaterThan(0);
});
it('gets its configuration properties', function () {
expect(testRule.getProperty('name')).toEqual('Name');
expect(testRule.getProperty('icon')).toEqual('test-icon-name');
});
it('can duplicate itself', function () {
testRule.duplicate();
mockRuleConfig.expanded = true;
expect(duplicateSpy).toHaveBeenCalledWith(mockRuleConfig);
});
it('can remove itself from the configuration', function () {
testRule.remove();
expect(removeSpy).toHaveBeenCalled();
expect(mockDomainObject.configuration.ruleConfigById.mockRule).not.toBeDefined();
expect(mockDomainObject.configuration.ruleOrder).toEqual(['default', 'otherRule']);
});
it('updates its configuration on a condition change and invokes callbacks', function () {
testRule.onConditionChange({
value: 'newValue',
property: 'object',
index: 0
});
expect(testRule.getProperty('conditions')[0].object).toEqual('newValue');
expect(conditionChangeSpy).toHaveBeenCalled();
});
it('allows initializing a new condition with a default configuration', function () {
testRule.initCondition();
expect(mockRuleConfig.conditions).toEqual([
{ {
object: '', object: '',
key: '', key: '',
@ -149,145 +59,257 @@ define(['../src/Rule'], function (Rule) {
key: 'blah', key: 'blah',
operation: 'blah', operation: 'blah',
values: ['blah.', 'blah!', 'blah?'] values: ['blah.', 'blah!', 'blah?']
},
{
object: '',
key: '',
operation: '',
values: []
} }
]); ]
}); };
mockDomainObject = {
it('allows initializing a new condition from a given configuration', function () { configuration: {
testRule.initCondition({ ruleConfigById: {
sourceCondition: { mockRule: mockRuleConfig,
object: 'object1', otherRule: {}
key: 'key1',
operation: 'operation1',
values: [1, 2, 3]
}, },
index: 0 ruleOrder: ['default', 'mockRule', 'otherRule']
}); }
expect(mockRuleConfig.conditions).toEqual([ };
{
object: '', mockOpenMCT = {};
key: '', mockOpenMCT.objects = {};
operation: '', mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');
values: []
}, mockEvaluator = {};
{ mockEvaluator.getOperationDescription = jasmine
object: 'object1', .createSpy('evaluator')
key: 'key1', .and.returnValue('Operation Description');
operation: 'operation1',
values: [1, 2, 3] mockConditionManager = jasmine.createSpyObj('mockConditionManager', [
}, 'on',
{ 'getComposition',
object: 'blah', 'loadCompleted',
key: 'blah', 'getEvaluator',
operation: 'blah', 'getTelemetryMetadata',
values: ['blah.', 'blah!', 'blah?'] 'metadataLoadCompleted',
} 'getObjectName',
]); 'getTelemetryPropertyName'
]);
mockConditionManager.loadCompleted.and.returnValue(false);
mockConditionManager.metadataLoadCompleted.and.returnValue(false);
mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);
mockConditionManager.getComposition.and.returnValue({});
mockConditionManager.getTelemetryMetadata.and.returnValue({});
mockConditionManager.getObjectName.and.returnValue('Object Name');
mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');
mockWidgetDnD = jasmine.createSpyObj('dnd', ['on', 'setDragImage', 'dragStart']);
mockContainer = document.createElement('div');
removeSpy = jasmine.createSpy('removeCallback');
duplicateSpy = jasmine.createSpy('duplicateCallback');
changeSpy = jasmine.createSpy('changeCallback');
conditionChangeSpy = jasmine.createSpy('conditionChangeCallback');
testRule = new Rule(
mockRuleConfig,
mockDomainObject,
mockOpenMCT,
mockConditionManager,
mockWidgetDnD
);
testRule.on('remove', removeSpy);
testRule.on('duplicate', duplicateSpy);
testRule.on('change', changeSpy);
testRule.on('conditionChange', conditionChangeSpy);
});
it('closes its configuration panel on initial load', function () {
expect(testRule.getProperty('expanded')).toEqual(false);
});
it('gets its DOM element', function () {
mockContainer.append(testRule.getDOM());
expect(mockContainer.querySelectorAll('.l-widget-rule').length).toBeGreaterThan(0);
});
it('gets its configuration properties', function () {
expect(testRule.getProperty('name')).toEqual('Name');
expect(testRule.getProperty('icon')).toEqual('test-icon-name');
});
it('can duplicate itself', function () {
testRule.duplicate();
mockRuleConfig.expanded = true;
expect(duplicateSpy).toHaveBeenCalledWith(mockRuleConfig);
});
it('can remove itself from the configuration', function () {
testRule.remove();
expect(removeSpy).toHaveBeenCalled();
expect(mockDomainObject.configuration.ruleConfigById.mockRule).not.toBeDefined();
expect(mockDomainObject.configuration.ruleOrder).toEqual(['default', 'otherRule']);
});
it('updates its configuration on a condition change and invokes callbacks', function () {
testRule.onConditionChange({
value: 'newValue',
property: 'object',
index: 0
}); });
expect(testRule.getProperty('conditions')[0].object).toEqual('newValue');
expect(conditionChangeSpy).toHaveBeenCalled();
});
it('invokes mutate when updating the domain object', function () { it('allows initializing a new condition with a default configuration', function () {
testRule.updateDomainObject(); testRule.initCondition();
expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); expect(mockRuleConfig.conditions).toEqual([
{
object: '',
key: '',
operation: '',
values: []
},
{
object: 'blah',
key: 'blah',
operation: 'blah',
values: ['blah.', 'blah!', 'blah?']
},
{
object: '',
key: '',
operation: '',
values: []
}
]);
});
it('allows initializing a new condition from a given configuration', function () {
testRule.initCondition({
sourceCondition: {
object: 'object1',
key: 'key1',
operation: 'operation1',
values: [1, 2, 3]
},
index: 0
}); });
expect(mockRuleConfig.conditions).toEqual([
{
object: '',
key: '',
operation: '',
values: []
},
{
object: 'object1',
key: 'key1',
operation: 'operation1',
values: [1, 2, 3]
},
{
object: 'blah',
key: 'blah',
operation: 'blah',
values: ['blah.', 'blah!', 'blah?']
}
]);
});
it('builds condition view from condition configuration', function () { it('invokes mutate when updating the domain object', function () {
mockContainer.append(testRule.getDOM()); testRule.updateDomainObject();
expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(2); expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();
});
it('builds condition view from condition configuration', function () {
mockContainer.append(testRule.getDOM());
expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(2);
});
it('responds to input of style properties, and updates the preview', function () {
testRule.colorInputs['background-color'].set('#434343');
expect(mockRuleConfig.style['background-color']).toEqual('#434343');
testRule.colorInputs['border-color'].set('#666666');
expect(mockRuleConfig.style['border-color']).toEqual('#666666');
testRule.colorInputs.color.set('#999999');
expect(mockRuleConfig.style.color).toEqual('#999999');
expect(testRule.thumbnail.style['background-color']).toEqual('rgb(67, 67, 67)');
expect(testRule.thumbnail.style['border-color']).toEqual('rgb(102, 102, 102)');
expect(testRule.thumbnail.style.color).toEqual('rgb(153, 153, 153)');
expect(changeSpy).toHaveBeenCalled();
});
it('responds to input for the icon property', function () {
testRule.iconInput.set('icon-alert-rect');
expect(mockRuleConfig.icon).toEqual('icon-alert-rect');
expect(changeSpy).toHaveBeenCalled();
});
/*
test for js condition commented out for v1
*/
// it('responds to input of text properties', function () {
// var testInputs = ['name', 'label', 'message', 'jsCondition'],
// input;
// testInputs.forEach(function (key) {
// input = testRule.textInputs[key];
// input.prop('value', 'A new ' + key);
// input.trigger('input');
// expect(mockRuleConfig[key]).toEqual('A new ' + key);
// });
// expect(changeSpy).toHaveBeenCalled();
// });
it('allows input for when the rule triggers', function () {
testRule.trigger.value = 'all';
const event = new Event('change', {
bubbles: true,
cancelable: true
}); });
testRule.trigger.dispatchEvent(event);
expect(testRule.config.trigger).toEqual('all');
expect(conditionChangeSpy).toHaveBeenCalled();
});
it('responds to input of style properties, and updates the preview', function () { it('generates a human-readable description from its conditions', function () {
testRule.colorInputs['background-color'].set('#434343'); testRule.generateDescription();
expect(mockRuleConfig.style['background-color']).toEqual('#434343'); expect(testRule.config.description).toContain(
testRule.colorInputs['border-color'].set('#666666'); "Object Name's Property Name Operation Description"
expect(mockRuleConfig.style['border-color']).toEqual('#666666'); );
testRule.colorInputs.color.set('#999999'); testRule.config.trigger = 'js';
expect(mockRuleConfig.style.color).toEqual('#999999'); testRule.generateDescription();
expect(testRule.config.description).toContain(
'when a custom JavaScript condition evaluates to true'
);
});
expect(testRule.thumbnail.style['background-color']).toEqual('rgb(67, 67, 67)'); it('initiates a drag event when its grippy is clicked', function () {
expect(testRule.thumbnail.style['border-color']).toEqual('rgb(102, 102, 102)'); const event = new Event('mousedown', {
expect(testRule.thumbnail.style.color).toEqual('rgb(153, 153, 153)'); bubbles: true,
cancelable: true
expect(changeSpy).toHaveBeenCalled();
}); });
testRule.grippy.dispatchEvent(event);
it('responds to input for the icon property', function () { expect(mockWidgetDnD.setDragImage).toHaveBeenCalled();
testRule.iconInput.set('icon-alert-rect'); expect(mockWidgetDnD.dragStart).toHaveBeenCalledWith('mockRule');
expect(mockRuleConfig.icon).toEqual('icon-alert-rect'); });
expect(changeSpy).toHaveBeenCalled();
});
/* /*
test for js condition commented out for v1 test for js condition commented out for v1
*/ */
// it('responds to input of text properties', function () { it('can remove a condition from its configuration', function () {
// var testInputs = ['name', 'label', 'message', 'jsCondition'], testRule.removeCondition(0);
// input; expect(testRule.config.conditions).toEqual([
{
// testInputs.forEach(function (key) { object: 'blah',
// input = testRule.textInputs[key]; key: 'blah',
// input.prop('value', 'A new ' + key); operation: 'blah',
// input.trigger('input'); values: ['blah.', 'blah!', 'blah?']
// expect(mockRuleConfig[key]).toEqual('A new ' + key); }
// }); ]);
// expect(changeSpy).toHaveBeenCalled();
// });
it('allows input for when the rule triggers', function () {
testRule.trigger.value = 'all';
const event = new Event('change', {
bubbles: true,
cancelable: true
});
testRule.trigger.dispatchEvent(event);
expect(testRule.config.trigger).toEqual('all');
expect(conditionChangeSpy).toHaveBeenCalled();
});
it('generates a human-readable description from its conditions', function () {
testRule.generateDescription();
expect(testRule.config.description).toContain(
"Object Name's Property Name Operation Description"
);
testRule.config.trigger = 'js';
testRule.generateDescription();
expect(testRule.config.description).toContain(
'when a custom JavaScript condition evaluates to true'
);
});
it('initiates a drag event when its grippy is clicked', function () {
const event = new Event('mousedown', {
bubbles: true,
cancelable: true
});
testRule.grippy.dispatchEvent(event);
expect(mockWidgetDnD.setDragImage).toHaveBeenCalled();
expect(mockWidgetDnD.dragStart).toHaveBeenCalledWith('mockRule');
});
/*
test for js condition commented out for v1
*/
it('can remove a condition from its configuration', function () {
testRule.removeCondition(0);
expect(testRule.config.conditions).toEqual([
{
object: 'blah',
key: 'blah',
operation: 'blah',
values: ['blah.', 'blah!', 'blah?']
}
]);
});
}); });
}); });

View File

@ -20,173 +20,173 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['../src/SummaryWidget'], function (SummaryWidget) { import SummaryWidget from '../src/SummaryWidget';
describe('The Summary Widget', function () {
let summaryWidget;
let mockDomainObject;
let mockOldDomainObject;
let mockOpenMCT;
let mockObjectService;
let mockStatusCapability;
let mockComposition;
let mockContainer;
let listenCallback;
let listenCallbackSpy;
beforeEach(function () { describe('The Summary Widget', function () {
mockDomainObject = { let summaryWidget;
identifier: { let mockDomainObject;
key: 'testKey', let mockOldDomainObject;
namespace: 'testNamespace' let mockOpenMCT;
}, let mockObjectService;
name: 'testName', let mockStatusCapability;
composition: [], let mockComposition;
configuration: {} let mockContainer;
}; let listenCallback;
mockComposition = jasmine.createSpyObj('composition', ['on', 'off', 'load']); let listenCallbackSpy;
mockStatusCapability = jasmine.createSpyObj('statusCapability', [
'get',
'listen',
'triggerCallback'
]);
listenCallbackSpy = jasmine.createSpy('listenCallbackSpy', function () {}); beforeEach(function () {
mockStatusCapability.get.and.returnValue([]); mockDomainObject = {
mockStatusCapability.listen.and.callFake(function (callback) { identifier: {
listenCallback = callback; key: 'testKey',
namespace: 'testNamespace'
},
name: 'testName',
composition: [],
configuration: {}
};
mockComposition = jasmine.createSpyObj('composition', ['on', 'off', 'load']);
mockStatusCapability = jasmine.createSpyObj('statusCapability', [
'get',
'listen',
'triggerCallback'
]);
return listenCallbackSpy; listenCallbackSpy = jasmine.createSpy('listenCallbackSpy', function () {});
}); mockStatusCapability.get.and.returnValue([]);
mockStatusCapability.triggerCallback.and.callFake(function () { mockStatusCapability.listen.and.callFake(function (callback) {
listenCallback(['editing']); listenCallback = callback;
});
mockOldDomainObject = {}; return listenCallbackSpy;
mockOldDomainObject.getCapability = jasmine.createSpy('capability'); });
mockOldDomainObject.getCapability.and.returnValue(mockStatusCapability); mockStatusCapability.triggerCallback.and.callFake(function () {
listenCallback(['editing']);
mockObjectService = {};
mockObjectService.getObjects = jasmine.createSpy('objectService');
mockObjectService.getObjects.and.returnValue(
new Promise(function (resolve, reject) {
resolve({
'testNamespace:testKey': mockOldDomainObject
});
})
);
mockOpenMCT = jasmine.createSpyObj('openmct', ['$injector', 'composition', 'objects']);
mockOpenMCT.$injector.get = jasmine.createSpy('get');
mockOpenMCT.$injector.get.and.returnValue(mockObjectService);
mockOpenMCT.composition = jasmine.createSpyObj('composition', ['get', 'on']);
mockOpenMCT.composition.get.and.returnValue(mockComposition);
mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');
mockOpenMCT.objects.observe = jasmine.createSpy('observe');
mockOpenMCT.objects.observe.and.returnValue(function () {});
summaryWidget = new SummaryWidget(mockDomainObject, mockOpenMCT);
mockContainer = document.createElement('div');
summaryWidget.show(mockContainer);
}); });
xit('queries with legacyId', function () { mockOldDomainObject = {};
expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']); mockOldDomainObject.getCapability = jasmine.createSpy('capability');
mockOldDomainObject.getCapability.and.returnValue(mockStatusCapability);
mockObjectService = {};
mockObjectService.getObjects = jasmine.createSpy('objectService');
mockObjectService.getObjects.and.returnValue(
new Promise(function (resolve, reject) {
resolve({
'testNamespace:testKey': mockOldDomainObject
});
})
);
mockOpenMCT = jasmine.createSpyObj('openmct', ['$injector', 'composition', 'objects']);
mockOpenMCT.$injector.get = jasmine.createSpy('get');
mockOpenMCT.$injector.get.and.returnValue(mockObjectService);
mockOpenMCT.composition = jasmine.createSpyObj('composition', ['get', 'on']);
mockOpenMCT.composition.get.and.returnValue(mockComposition);
mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');
mockOpenMCT.objects.observe = jasmine.createSpy('observe');
mockOpenMCT.objects.observe.and.returnValue(function () {});
summaryWidget = new SummaryWidget(mockDomainObject, mockOpenMCT);
mockContainer = document.createElement('div');
summaryWidget.show(mockContainer);
});
xit('queries with legacyId', function () {
expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']);
});
it('adds its DOM element to the view', function () {
expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0);
});
it('initializes a default rule', function () {
expect(mockDomainObject.configuration.ruleConfigById.default).toBeDefined();
expect(mockDomainObject.configuration.ruleOrder).toEqual(['default']);
});
it('builds rules and rule placeholders in view from configuration', function () {
expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(2);
});
it('allows initializing a new rule with a particular identifier', function () {
summaryWidget.initRule('rule0', 'Rule');
expect(mockDomainObject.configuration.ruleConfigById.rule0).toBeDefined();
});
it('allows adding a new rule with a unique identifier to the configuration and view', function () {
summaryWidget.addRule();
expect(mockDomainObject.configuration.ruleOrder.length).toEqual(2);
mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) {
expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined();
}); });
summaryWidget.addRule();
it('adds its DOM element to the view', function () { expect(mockDomainObject.configuration.ruleOrder.length).toEqual(3);
expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0); mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) {
expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined();
}); });
expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(6);
});
it('initializes a default rule', function () { it('allows duplicating a rule from source configuration', function () {
expect(mockDomainObject.configuration.ruleConfigById.default).toBeDefined(); const sourceConfig = JSON.parse(
expect(mockDomainObject.configuration.ruleOrder).toEqual(['default']); JSON.stringify(mockDomainObject.configuration.ruleConfigById.default)
);
summaryWidget.duplicateRule(sourceConfig);
expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(2);
});
it('does not duplicate an existing rule in the configuration', function () {
summaryWidget.initRule('default', 'Default');
expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(1);
});
it('uses mutate when updating the domain object only when in edit mode', function () {
summaryWidget.editing = true;
summaryWidget.updateDomainObject();
expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();
});
xit('shows configuration interfaces when in edit mode, and hides them otherwise', function () {
setTimeout(function () {
summaryWidget.onEdit([]);
expect(summaryWidget.editing).toEqual(false);
expect(summaryWidget.ruleArea.css('display')).toEqual('none');
expect(summaryWidget.testDataArea.css('display')).toEqual('none');
expect(summaryWidget.addRuleButton.css('display')).toEqual('none');
summaryWidget.onEdit(['editing']);
expect(summaryWidget.editing).toEqual(true);
expect(summaryWidget.ruleArea.css('display')).not.toEqual('none');
expect(summaryWidget.testDataArea.css('display')).not.toEqual('none');
expect(summaryWidget.addRuleButton.css('display')).not.toEqual('none');
}, 100);
});
xit('unregisters any registered listeners on a destroy', function () {
setTimeout(function () {
summaryWidget.destroy();
expect(listenCallbackSpy).toHaveBeenCalled();
}, 100);
});
it('allows reorders of rules', function () {
summaryWidget.initRule('rule0');
summaryWidget.initRule('rule1');
summaryWidget.domainObject.configuration.ruleOrder = ['default', 'rule0', 'rule1'];
summaryWidget.reorder({
draggingId: 'rule1',
dropTarget: 'default'
}); });
expect(summaryWidget.domainObject.configuration.ruleOrder).toEqual([
'default',
'rule1',
'rule0'
]);
});
it('builds rules and rule placeholders in view from configuration', function () { it('adds hyperlink to the widget button and sets newTab preference', function () {
expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(2); summaryWidget.addHyperlink('https://www.nasa.gov', 'newTab');
});
it('allows initializing a new rule with a particular identifier', function () { const widgetButton = mockContainer.querySelector('#widget');
summaryWidget.initRule('rule0', 'Rule');
expect(mockDomainObject.configuration.ruleConfigById.rule0).toBeDefined();
});
it('allows adding a new rule with a unique identifier to the configuration and view', function () { expect(widgetButton.href).toEqual('https://www.nasa.gov/');
summaryWidget.addRule(); expect(widgetButton.target).toEqual('_blank');
expect(mockDomainObject.configuration.ruleOrder.length).toEqual(2);
mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) {
expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined();
});
summaryWidget.addRule();
expect(mockDomainObject.configuration.ruleOrder.length).toEqual(3);
mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) {
expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined();
});
expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(6);
});
it('allows duplicating a rule from source configuration', function () {
const sourceConfig = JSON.parse(
JSON.stringify(mockDomainObject.configuration.ruleConfigById.default)
);
summaryWidget.duplicateRule(sourceConfig);
expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(2);
});
it('does not duplicate an existing rule in the configuration', function () {
summaryWidget.initRule('default', 'Default');
expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(1);
});
it('uses mutate when updating the domain object only when in edit mode', function () {
summaryWidget.editing = true;
summaryWidget.updateDomainObject();
expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();
});
xit('shows configuration interfaces when in edit mode, and hides them otherwise', function () {
setTimeout(function () {
summaryWidget.onEdit([]);
expect(summaryWidget.editing).toEqual(false);
expect(summaryWidget.ruleArea.css('display')).toEqual('none');
expect(summaryWidget.testDataArea.css('display')).toEqual('none');
expect(summaryWidget.addRuleButton.css('display')).toEqual('none');
summaryWidget.onEdit(['editing']);
expect(summaryWidget.editing).toEqual(true);
expect(summaryWidget.ruleArea.css('display')).not.toEqual('none');
expect(summaryWidget.testDataArea.css('display')).not.toEqual('none');
expect(summaryWidget.addRuleButton.css('display')).not.toEqual('none');
}, 100);
});
xit('unregisters any registered listeners on a destroy', function () {
setTimeout(function () {
summaryWidget.destroy();
expect(listenCallbackSpy).toHaveBeenCalled();
}, 100);
});
it('allows reorders of rules', function () {
summaryWidget.initRule('rule0');
summaryWidget.initRule('rule1');
summaryWidget.domainObject.configuration.ruleOrder = ['default', 'rule0', 'rule1'];
summaryWidget.reorder({
draggingId: 'rule1',
dropTarget: 'default'
});
expect(summaryWidget.domainObject.configuration.ruleOrder).toEqual([
'default',
'rule1',
'rule0'
]);
});
it('adds hyperlink to the widget button and sets newTab preference', function () {
summaryWidget.addHyperlink('https://www.nasa.gov', 'newTab');
const widgetButton = mockContainer.querySelector('#widget');
expect(widgetButton.href).toEqual('https://www.nasa.gov/');
expect(widgetButton.target).toEqual('_blank');
});
}); });
}); });

View File

@ -20,39 +20,39 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['../SummaryWidgetViewPolicy'], function (SummaryWidgetViewPolicy) { import SummaryWidgetViewPolicy from '../SummaryWidgetViewPolicy';
describe('SummaryWidgetViewPolicy', function () {
let policy;
let domainObject;
let view;
beforeEach(function () {
policy = new SummaryWidgetViewPolicy();
domainObject = jasmine.createSpyObj('domainObject', ['getModel']);
domainObject.getModel.and.returnValue({});
view = {};
});
it('returns true for other object types', function () { describe('SummaryWidgetViewPolicy', function () {
domainObject.getModel.and.returnValue({ let policy;
type: 'random' let domainObject;
}); let view;
expect(policy.allow(view, domainObject)).toBe(true); beforeEach(function () {
}); policy = new SummaryWidgetViewPolicy();
domainObject = jasmine.createSpyObj('domainObject', ['getModel']);
domainObject.getModel.and.returnValue({});
view = {};
});
it('allows summary widget view for summary widgets', function () { it('returns true for other object types', function () {
domainObject.getModel.and.returnValue({ domainObject.getModel.and.returnValue({
type: 'summary-widget' type: 'random'
});
view.key = 'summary-widget-viewer';
expect(policy.allow(view, domainObject)).toBe(true);
}); });
expect(policy.allow(view, domainObject)).toBe(true);
});
it('disallows other views for summary widgets', function () { it('allows summary widget view for summary widgets', function () {
domainObject.getModel.and.returnValue({ domainObject.getModel.and.returnValue({
type: 'summary-widget' type: 'summary-widget'
});
view.key = 'other view';
expect(policy.allow(view, domainObject)).toBe(false);
}); });
view.key = 'summary-widget-viewer';
expect(policy.allow(view, domainObject)).toBe(true);
});
it('disallows other views for summary widgets', function () {
domainObject.getModel.and.returnValue({
type: 'summary-widget'
});
view.key = 'other view';
expect(policy.allow(view, domainObject)).toBe(false);
}); });
}); });

View File

@ -1,167 +1,167 @@
define(['../src/TestDataItem'], function (TestDataItem) { import TestDataItem from '../src/TestDataItem';
describe('A summary widget test data item', function () {
let testDataItem;
let mockConfig;
let mockConditionManager;
let mockContainer;
let mockEvaluator;
let changeSpy;
let duplicateSpy;
let removeSpy;
let generateValueSpy;
beforeEach(function () { describe('A summary widget test data item', function () {
mockContainer = document.createElement('div'); let testDataItem;
let mockConfig;
let mockConditionManager;
let mockContainer;
let mockEvaluator;
let changeSpy;
let duplicateSpy;
let removeSpy;
let generateValueSpy;
mockConfig = { beforeEach(function () {
object: 'object1', mockContainer = document.createElement('div');
key: 'property1',
value: 1
};
mockEvaluator = {}; mockConfig = {
mockEvaluator.getInputTypeById = jasmine.createSpy('inputType'); object: 'object1',
key: 'property1',
value: 1
};
mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ mockEvaluator = {};
'on', mockEvaluator.getInputTypeById = jasmine.createSpy('inputType');
'getComposition',
'loadCompleted',
'getEvaluator',
'getTelemetryMetadata',
'metadataLoadCompleted',
'getObjectName',
'getTelemetryPropertyName',
'getTelemetryPropertyType'
]);
mockConditionManager.loadCompleted.and.returnValue(false);
mockConditionManager.metadataLoadCompleted.and.returnValue(false);
mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);
mockConditionManager.getComposition.and.returnValue({});
mockConditionManager.getTelemetryMetadata.and.returnValue({});
mockConditionManager.getObjectName.and.returnValue('Object Name');
mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');
mockConditionManager.getTelemetryPropertyType.and.returnValue('');
duplicateSpy = jasmine.createSpy('duplicate'); mockConditionManager = jasmine.createSpyObj('mockConditionManager', [
removeSpy = jasmine.createSpy('remove'); 'on',
changeSpy = jasmine.createSpy('change'); 'getComposition',
generateValueSpy = jasmine.createSpy('generateValueInput'); 'loadCompleted',
'getEvaluator',
'getTelemetryMetadata',
'metadataLoadCompleted',
'getObjectName',
'getTelemetryPropertyName',
'getTelemetryPropertyType'
]);
mockConditionManager.loadCompleted.and.returnValue(false);
mockConditionManager.metadataLoadCompleted.and.returnValue(false);
mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);
mockConditionManager.getComposition.and.returnValue({});
mockConditionManager.getTelemetryMetadata.and.returnValue({});
mockConditionManager.getObjectName.and.returnValue('Object Name');
mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');
mockConditionManager.getTelemetryPropertyType.and.returnValue('');
testDataItem = new TestDataItem(mockConfig, 54, mockConditionManager); duplicateSpy = jasmine.createSpy('duplicate');
removeSpy = jasmine.createSpy('remove');
changeSpy = jasmine.createSpy('change');
generateValueSpy = jasmine.createSpy('generateValueInput');
testDataItem.on('duplicate', duplicateSpy); testDataItem = new TestDataItem(mockConfig, 54, mockConditionManager);
testDataItem.on('remove', removeSpy);
testDataItem.on('change', changeSpy); testDataItem.on('duplicate', duplicateSpy);
testDataItem.on('remove', removeSpy);
testDataItem.on('change', changeSpy);
});
it('exposes a DOM element to represent itself in the view', function () {
mockContainer.append(testDataItem.getDOM());
expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(1);
});
it('responds to a change in its object select', function () {
testDataItem.selects.object.setSelected('');
expect(changeSpy).toHaveBeenCalledWith({
value: '',
property: 'object',
index: 54
});
});
it('responds to a change in its key select', function () {
testDataItem.generateValueInput = generateValueSpy;
testDataItem.selects.key.setSelected('');
expect(changeSpy).toHaveBeenCalledWith({
value: '',
property: 'key',
index: 54
});
expect(generateValueSpy).toHaveBeenCalledWith('');
});
it('generates a value input of the appropriate type', function () {
let inputs;
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.and.returnValue('number');
testDataItem.generateValueInput('');
inputs = mockContainer.querySelectorAll('input');
const numberInputs = Array.from(inputs).filter((input) => input.type === 'number');
expect(numberInputs.length).toEqual(1);
expect(inputs[0].valueAsNumber).toEqual(1);
mockEvaluator.getInputTypeById.and.returnValue('text');
testDataItem.config.value = 'Text I Am';
testDataItem.generateValueInput('');
inputs = mockContainer.querySelectorAll('input');
const textInputs = Array.from(inputs).filter((input) => input.type === 'text');
expect(textInputs.length).toEqual(1);
expect(inputs[0].value).toEqual('Text I Am');
});
it('ensures reasonable defaults on values if none are provided', function () {
let inputs;
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.and.returnValue('number');
testDataItem.config.value = undefined;
testDataItem.generateValueInput('');
inputs = mockContainer.querySelectorAll('input');
const numberInputs = Array.from(inputs).filter((input) => input.type === 'number');
expect(numberInputs.length).toEqual(1);
expect(inputs[0].valueAsNumber).toEqual(0);
expect(testDataItem.config.value).toEqual(0);
mockEvaluator.getInputTypeById.and.returnValue('text');
testDataItem.config.value = undefined;
testDataItem.generateValueInput('');
inputs = mockContainer.querySelectorAll('input');
const textInputs = Array.from(inputs).filter((input) => input.type === 'text');
expect(textInputs.length).toEqual(1);
expect(inputs[0].value).toEqual('');
expect(testDataItem.config.value).toEqual('');
});
it('responds to a change in its value inputs', function () {
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.and.returnValue('number');
testDataItem.generateValueInput('');
const event = new Event('input', {
bubbles: true,
cancelable: true
}); });
it('exposes a DOM element to represent itself in the view', function () { mockContainer.querySelector('input').value = 9001;
mockContainer.append(testDataItem.getDOM()); mockContainer.querySelector('input').dispatchEvent(event);
expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(1);
expect(changeSpy).toHaveBeenCalledWith({
value: 9001,
property: 'value',
index: 54
}); });
});
it('responds to a change in its object select', function () { it('can remove itself from the configuration', function () {
testDataItem.selects.object.setSelected(''); testDataItem.remove();
expect(changeSpy).toHaveBeenCalledWith({ expect(removeSpy).toHaveBeenCalledWith(54);
value: '', });
property: 'object',
index: 54
});
});
it('responds to a change in its key select', function () { it('can duplicate itself', function () {
testDataItem.generateValueInput = generateValueSpy; testDataItem.duplicate();
testDataItem.selects.key.setSelected(''); expect(duplicateSpy).toHaveBeenCalledWith({
expect(changeSpy).toHaveBeenCalledWith({ sourceItem: mockConfig,
value: '', index: 54
property: 'key',
index: 54
});
expect(generateValueSpy).toHaveBeenCalledWith('');
});
it('generates a value input of the appropriate type', function () {
let inputs;
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.and.returnValue('number');
testDataItem.generateValueInput('');
inputs = mockContainer.querySelectorAll('input');
const numberInputs = Array.from(inputs).filter((input) => input.type === 'number');
expect(numberInputs.length).toEqual(1);
expect(inputs[0].valueAsNumber).toEqual(1);
mockEvaluator.getInputTypeById.and.returnValue('text');
testDataItem.config.value = 'Text I Am';
testDataItem.generateValueInput('');
inputs = mockContainer.querySelectorAll('input');
const textInputs = Array.from(inputs).filter((input) => input.type === 'text');
expect(textInputs.length).toEqual(1);
expect(inputs[0].value).toEqual('Text I Am');
});
it('ensures reasonable defaults on values if none are provided', function () {
let inputs;
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.and.returnValue('number');
testDataItem.config.value = undefined;
testDataItem.generateValueInput('');
inputs = mockContainer.querySelectorAll('input');
const numberInputs = Array.from(inputs).filter((input) => input.type === 'number');
expect(numberInputs.length).toEqual(1);
expect(inputs[0].valueAsNumber).toEqual(0);
expect(testDataItem.config.value).toEqual(0);
mockEvaluator.getInputTypeById.and.returnValue('text');
testDataItem.config.value = undefined;
testDataItem.generateValueInput('');
inputs = mockContainer.querySelectorAll('input');
const textInputs = Array.from(inputs).filter((input) => input.type === 'text');
expect(textInputs.length).toEqual(1);
expect(inputs[0].value).toEqual('');
expect(testDataItem.config.value).toEqual('');
});
it('responds to a change in its value inputs', function () {
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.and.returnValue('number');
testDataItem.generateValueInput('');
const event = new Event('input', {
bubbles: true,
cancelable: true
});
mockContainer.querySelector('input').value = 9001;
mockContainer.querySelector('input').dispatchEvent(event);
expect(changeSpy).toHaveBeenCalledWith({
value: 9001,
property: 'value',
index: 54
});
});
it('can remove itself from the configuration', function () {
testDataItem.remove();
expect(removeSpy).toHaveBeenCalledWith(54);
});
it('can duplicate itself', function () {
testDataItem.duplicate();
expect(duplicateSpy).toHaveBeenCalledWith({
sourceItem: mockConfig,
index: 54
});
}); });
}); });
}); });

View File

@ -1,250 +1,244 @@
define(['../src/TestDataManager'], function (TestDataManager) { import TestDataManager from '../src/TestDataManager';
describe('A Summary Widget Rule', function () {
let mockDomainObject;
let mockOpenMCT;
let mockConditionManager;
let mockEvaluator;
let mockContainer;
let mockTelemetryMetadata;
let testDataManager;
let mockCompObject1;
let mockCompObject2;
beforeEach(function () { describe('A Summary Widget Rule', function () {
mockDomainObject = { let mockDomainObject;
configuration: { let mockOpenMCT;
testDataConfig: [ let mockConditionManager;
{ let mockEvaluator;
object: '', let mockContainer;
key: '', let mockTelemetryMetadata;
value: '' let testDataManager;
}, let mockCompObject1;
{ let mockCompObject2;
object: 'object1',
key: 'property1', beforeEach(function () {
value: 66 mockDomainObject = {
}, configuration: {
{ testDataConfig: [
object: 'object2',
key: 'property4',
value: 'Text It Is'
}
]
},
composition: [
{ {
object1: { object: '',
key: 'object1', key: '',
name: 'Object 1' value: ''
}, },
object2: { {
key: 'object2', object: 'object1',
name: 'Object 2' key: 'property1',
} value: 66
},
{
object: 'object2',
key: 'property4',
value: 'Text It Is'
} }
] ]
}; },
composition: [
mockTelemetryMetadata = { {
object1: {
property1: {
key: 'property1'
},
property2: {
key: 'property2'
}
},
object2: {
property3: {
key: 'property3'
},
property4: {
key: 'property4'
}
}
};
mockCompObject1 = {
identifier: {
key: 'object1'
},
name: 'Object 1'
};
mockCompObject2 = {
identifier: {
key: 'object2'
},
name: 'Object 2'
};
mockOpenMCT = {};
mockOpenMCT.objects = {};
mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');
mockEvaluator = {};
mockEvaluator.setTestDataCache = jasmine.createSpy('testDataCache');
mockEvaluator.useTestData = jasmine.createSpy('useTestData');
mockConditionManager = jasmine.createSpyObj('mockConditionManager', [
'on',
'getComposition',
'loadCompleted',
'getEvaluator',
'getTelemetryMetadata',
'metadataLoadCompleted',
'getObjectName',
'getTelemetryPropertyName',
'triggerTelemetryCallback'
]);
mockConditionManager.loadCompleted.and.returnValue(false);
mockConditionManager.metadataLoadCompleted.and.returnValue(false);
mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);
mockConditionManager.getComposition.and.returnValue({
object1: mockCompObject1,
object2: mockCompObject2
});
mockConditionManager.getTelemetryMetadata.and.callFake(function (id) {
return mockTelemetryMetadata[id];
});
mockConditionManager.getObjectName.and.returnValue('Object Name');
mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');
mockContainer = document.createElement('div');
testDataManager = new TestDataManager(mockDomainObject, mockConditionManager, mockOpenMCT);
});
it('closes its configuration panel on initial load', function () {});
it('exposes a DOM element to represent itself in the view', function () {
mockContainer.append(testDataManager.getDOM());
expect(mockContainer.querySelectorAll('.t-widget-test-data-content').length).toBeGreaterThan(
0
);
});
it('generates a test cache in the format expected by a condition evaluator', function () {
testDataManager.updateTestCache();
expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({
object1: {
property1: 66,
property2: ''
},
object2: {
property3: '',
property4: 'Text It Is'
}
});
});
it(
'updates its configuration on a item change and provides an updated' +
'cache to the evaluator',
function () {
testDataManager.onItemChange({
value: 26,
property: 'value',
index: 1
});
expect(testDataManager.config[1].value).toEqual(26);
expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({
object1: { object1: {
property1: 26, key: 'object1',
property2: '' name: 'Object 1'
}, },
object2: { object2: {
property3: '', key: 'object2',
property4: 'Text It Is' name: 'Object 2'
} }
}); }
]
};
mockTelemetryMetadata = {
object1: {
property1: {
key: 'property1'
},
property2: {
key: 'property2'
}
},
object2: {
property3: {
key: 'property3'
},
property4: {
key: 'property4'
}
} }
); };
it('allows initializing a new item with a default configuration', function () { mockCompObject1 = {
testDataManager.initItem(); identifier: {
expect(mockDomainObject.configuration.testDataConfig).toEqual([ key: 'object1'
{ },
object: '', name: 'Object 1'
key: '', };
value: '' mockCompObject2 = {
}, identifier: {
{ key: 'object2'
object: 'object1', },
key: 'property1', name: 'Object 2'
value: 66 };
},
{ mockOpenMCT = {};
object: 'object2', mockOpenMCT.objects = {};
key: 'property4', mockOpenMCT.objects.mutate = jasmine.createSpy('mutate');
value: 'Text It Is'
}, mockEvaluator = {};
{ mockEvaluator.setTestDataCache = jasmine.createSpy('testDataCache');
object: '', mockEvaluator.useTestData = jasmine.createSpy('useTestData');
key: '',
value: '' mockConditionManager = jasmine.createSpyObj('mockConditionManager', [
} 'on',
]); 'getComposition',
'loadCompleted',
'getEvaluator',
'getTelemetryMetadata',
'metadataLoadCompleted',
'getObjectName',
'getTelemetryPropertyName',
'triggerTelemetryCallback'
]);
mockConditionManager.loadCompleted.and.returnValue(false);
mockConditionManager.metadataLoadCompleted.and.returnValue(false);
mockConditionManager.getEvaluator.and.returnValue(mockEvaluator);
mockConditionManager.getComposition.and.returnValue({
object1: mockCompObject1,
object2: mockCompObject2
}); });
mockConditionManager.getTelemetryMetadata.and.callFake(function (id) {
it('allows initializing a new item from a given configuration', function () { return mockTelemetryMetadata[id];
testDataManager.initItem({
sourceItem: {
object: 'object2',
key: 'property3',
value: 1
},
index: 0
});
expect(mockDomainObject.configuration.testDataConfig).toEqual([
{
object: '',
key: '',
value: ''
},
{
object: 'object2',
key: 'property3',
value: 1
},
{
object: 'object1',
key: 'property1',
value: 66
},
{
object: 'object2',
key: 'property4',
value: 'Text It Is'
}
]);
}); });
mockConditionManager.getObjectName.and.returnValue('Object Name');
mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');
it('invokes mutate when updating the domain object', function () { mockContainer = document.createElement('div');
testDataManager.updateDomainObject();
expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();
});
it('builds item view from item configuration', function () { testDataManager = new TestDataManager(mockDomainObject, mockConditionManager, mockOpenMCT);
mockContainer.append(testDataManager.getDOM());
expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(3);
});
it('can remove a item from its configuration', function () {
testDataManager.removeItem(0);
expect(mockDomainObject.configuration.testDataConfig).toEqual([
{
object: 'object1',
key: 'property1',
value: 66
},
{
object: 'object2',
key: 'property4',
value: 'Text It Is'
}
]);
});
it('exposes a UI element to toggle test data on and off', function () {});
}); });
it('closes its configuration panel on initial load', function () {});
it('exposes a DOM element to represent itself in the view', function () {
mockContainer.append(testDataManager.getDOM());
expect(mockContainer.querySelectorAll('.t-widget-test-data-content').length).toBeGreaterThan(0);
});
it('generates a test cache in the format expected by a condition evaluator', function () {
testDataManager.updateTestCache();
expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({
object1: {
property1: 66,
property2: ''
},
object2: {
property3: '',
property4: 'Text It Is'
}
});
});
it('updates its configuration on a item change and provides an updated cache to the evaluator', function () {
testDataManager.onItemChange({
value: 26,
property: 'value',
index: 1
});
expect(testDataManager.config[1].value).toEqual(26);
expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({
object1: {
property1: 26,
property2: ''
},
object2: {
property3: '',
property4: 'Text It Is'
}
});
});
it('allows initializing a new item with a default configuration', function () {
testDataManager.initItem();
expect(mockDomainObject.configuration.testDataConfig).toEqual([
{
object: '',
key: '',
value: ''
},
{
object: 'object1',
key: 'property1',
value: 66
},
{
object: 'object2',
key: 'property4',
value: 'Text It Is'
},
{
object: '',
key: '',
value: ''
}
]);
});
it('allows initializing a new item from a given configuration', function () {
testDataManager.initItem({
sourceItem: {
object: 'object2',
key: 'property3',
value: 1
},
index: 0
});
expect(mockDomainObject.configuration.testDataConfig).toEqual([
{
object: '',
key: '',
value: ''
},
{
object: 'object2',
key: 'property3',
value: 1
},
{
object: 'object1',
key: 'property1',
value: 66
},
{
object: 'object2',
key: 'property4',
value: 'Text It Is'
}
]);
});
it('invokes mutate when updating the domain object', function () {
testDataManager.updateDomainObject();
expect(mockOpenMCT.objects.mutate).toHaveBeenCalled();
});
it('builds item view from item configuration', function () {
mockContainer.append(testDataManager.getDOM());
expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(3);
});
it('can remove a item from its configuration', function () {
testDataManager.removeItem(0);
expect(mockDomainObject.configuration.testDataConfig).toEqual([
{
object: 'object1',
key: 'property1',
value: 66
},
{
object: 'object2',
key: 'property4',
value: 'Text It Is'
}
]);
});
it('exposes a UI element to toggle test data on and off', function () {});
}); });

View File

@ -1,24 +1,24 @@
define(['../../src/input/ColorPalette'], function (ColorPalette) { import ColorPalette from '../../src/input/ColorPalette';
describe('An Open MCT color palette', function () {
let colorPalette;
let changeCallback;
beforeEach(function () { describe('An Open MCT color palette', function () {
changeCallback = jasmine.createSpy('changeCallback'); let colorPalette;
}); let changeCallback;
it('allows defining a custom color set', function () { beforeEach(function () {
colorPalette = new ColorPalette('someClass', 'someContainer', ['color1', 'color2', 'color3']); changeCallback = jasmine.createSpy('changeCallback');
expect(colorPalette.getCurrent()).toEqual('color1'); });
colorPalette.on('change', changeCallback);
colorPalette.set('color2');
expect(colorPalette.getCurrent()).toEqual('color2');
expect(changeCallback).toHaveBeenCalledWith('color2');
});
it('loads with a default color set if one is not provided', function () { it('allows defining a custom color set', function () {
colorPalette = new ColorPalette('someClass', 'someContainer'); colorPalette = new ColorPalette('someClass', 'someContainer', ['color1', 'color2', 'color3']);
expect(colorPalette.getCurrent()).toBeDefined(); expect(colorPalette.getCurrent()).toEqual('color1');
}); colorPalette.on('change', changeCallback);
colorPalette.set('color2');
expect(colorPalette.getCurrent()).toEqual('color2');
expect(changeCallback).toHaveBeenCalledWith('color2');
});
it('loads with a default color set if one is not provided', function () {
colorPalette = new ColorPalette('someClass', 'someContainer');
expect(colorPalette.getCurrent()).toBeDefined();
}); });
}); });

View File

@ -1,24 +1,24 @@
define(['../../src/input/IconPalette'], function (IconPalette) { import IconPalette from '../../src/input/IconPalette';
describe('An Open MCT icon palette', function () {
let iconPalette;
let changeCallback;
beforeEach(function () { describe('An Open MCT icon palette', function () {
changeCallback = jasmine.createSpy('changeCallback'); let iconPalette;
}); let changeCallback;
it('allows defining a custom icon set', function () { beforeEach(function () {
iconPalette = new IconPalette('', 'someContainer', ['icon1', 'icon2', 'icon3']); changeCallback = jasmine.createSpy('changeCallback');
expect(iconPalette.getCurrent()).toEqual('icon1'); });
iconPalette.on('change', changeCallback);
iconPalette.set('icon2');
expect(iconPalette.getCurrent()).toEqual('icon2');
expect(changeCallback).toHaveBeenCalledWith('icon2');
});
it('loads with a default icon set if one is not provided', function () { it('allows defining a custom icon set', function () {
iconPalette = new IconPalette('someClass', 'someContainer'); iconPalette = new IconPalette('', 'someContainer', ['icon1', 'icon2', 'icon3']);
expect(iconPalette.getCurrent()).toBeDefined(); expect(iconPalette.getCurrent()).toEqual('icon1');
}); iconPalette.on('change', changeCallback);
iconPalette.set('icon2');
expect(iconPalette.getCurrent()).toEqual('icon2');
expect(changeCallback).toHaveBeenCalledWith('icon2');
});
it('loads with a default icon set if one is not provided', function () {
iconPalette = new IconPalette('someClass', 'someContainer');
expect(iconPalette.getCurrent()).toBeDefined();
}); });
}); });

View File

@ -1,123 +1,123 @@
define(['../../src/input/KeySelect'], function (KeySelect) { import KeySelect from '../../src/input/KeySelect';
describe('A select for choosing composition object properties', function () {
let mockConfig;
let mockBadConfig;
let mockManager;
let keySelect;
let mockMetadata;
let mockObjectSelect;
beforeEach(function () {
mockConfig = {
object: 'object1',
key: 'a'
};
mockBadConfig = { describe('A select for choosing composition object properties', function () {
object: 'object1', let mockConfig;
key: 'someNonexistentKey' let mockBadConfig;
}; let mockManager;
let keySelect;
let mockMetadata;
let mockObjectSelect;
beforeEach(function () {
mockConfig = {
object: 'object1',
key: 'a'
};
mockMetadata = { mockBadConfig = {
object1: { object: 'object1',
a: { key: 'someNonexistentKey'
name: 'A' };
},
b: { mockMetadata = {
name: 'B' object1: {
} a: {
name: 'A'
}, },
object2: { b: {
alpha: { name: 'B'
name: 'Alpha'
},
beta: {
name: 'Beta'
}
},
object3: {
a: {
name: 'A'
}
} }
}; },
object2: {
alpha: {
name: 'Alpha'
},
beta: {
name: 'Beta'
}
},
object3: {
a: {
name: 'A'
}
}
};
mockManager = jasmine.createSpyObj('mockManager', [ mockManager = jasmine.createSpyObj('mockManager', [
'on', 'on',
'metadataLoadCompleted', 'metadataLoadCompleted',
'triggerCallback', 'triggerCallback',
'getTelemetryMetadata' 'getTelemetryMetadata'
]); ]);
mockObjectSelect = jasmine.createSpyObj('mockObjectSelect', ['on', 'triggerCallback']); mockObjectSelect = jasmine.createSpyObj('mockObjectSelect', ['on', 'triggerCallback']);
mockObjectSelect.on.and.callFake((event, callback) => { mockObjectSelect.on.and.callFake((event, callback) => {
mockObjectSelect.callbacks = mockObjectSelect.callbacks || {}; mockObjectSelect.callbacks = mockObjectSelect.callbacks || {};
mockObjectSelect.callbacks[event] = callback; mockObjectSelect.callbacks[event] = callback;
});
mockObjectSelect.triggerCallback.and.callFake((event, key) => {
mockObjectSelect.callbacks[event](key);
});
mockManager.on.and.callFake((event, callback) => {
mockManager.callbacks = mockManager.callbacks || {};
mockManager.callbacks[event] = callback;
});
mockManager.triggerCallback.and.callFake((event) => {
mockManager.callbacks[event]();
});
mockManager.getTelemetryMetadata.and.callFake(function (key) {
return mockMetadata[key];
});
}); });
it('waits until the metadata fully loads to populate itself', function () { mockObjectSelect.triggerCallback.and.callFake((event, key) => {
mockManager.metadataLoadCompleted.and.returnValue(false); mockObjectSelect.callbacks[event](key);
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
expect(keySelect.getSelected()).toEqual('');
}); });
it('populates itself with metadata on a metadata load', function () { mockManager.on.and.callFake((event, callback) => {
mockManager.metadataLoadCompleted.and.returnValue(false); mockManager.callbacks = mockManager.callbacks || {};
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); mockManager.callbacks[event] = callback;
mockManager.triggerCallback('metadata');
expect(keySelect.getSelected()).toEqual('a');
}); });
it('populates itself with metadata if metadata load is already complete', function () { mockManager.triggerCallback.and.callFake((event) => {
mockManager.metadataLoadCompleted.and.returnValue(true); mockManager.callbacks[event]();
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
expect(keySelect.getSelected()).toEqual('a');
}); });
it('clears its selection state if the property in its config is not in its object', function () { mockManager.getTelemetryMetadata.and.callFake(function (key) {
mockManager.metadataLoadCompleted.and.returnValue(true); return mockMetadata[key];
keySelect = new KeySelect(mockBadConfig, mockObjectSelect, mockManager);
expect(keySelect.getSelected()).toEqual('');
});
it('populates with the appropriate options when its linked object changes', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
mockObjectSelect.triggerCallback('change', 'object2');
keySelect.setSelected('alpha');
expect(keySelect.getSelected()).toEqual('alpha');
});
it('clears its selected state on change if the field is not present in the new object', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
mockObjectSelect.triggerCallback('change', 'object2');
expect(keySelect.getSelected()).toEqual('');
});
it('maintains its selected state on change if field is present in new object', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
mockObjectSelect.triggerCallback('change', 'object3');
expect(keySelect.getSelected()).toEqual('a');
}); });
}); });
it('waits until the metadata fully loads to populate itself', function () {
mockManager.metadataLoadCompleted.and.returnValue(false);
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
expect(keySelect.getSelected()).toEqual('');
});
it('populates itself with metadata on a metadata load', function () {
mockManager.metadataLoadCompleted.and.returnValue(false);
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
mockManager.triggerCallback('metadata');
expect(keySelect.getSelected()).toEqual('a');
});
it('populates itself with metadata if metadata load is already complete', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
expect(keySelect.getSelected()).toEqual('a');
});
it('clears its selection state if the property in its config is not in its object', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
keySelect = new KeySelect(mockBadConfig, mockObjectSelect, mockManager);
expect(keySelect.getSelected()).toEqual('');
});
it('populates with the appropriate options when its linked object changes', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
mockObjectSelect.triggerCallback('change', 'object2');
keySelect.setSelected('alpha');
expect(keySelect.getSelected()).toEqual('alpha');
});
it('clears its selected state on change if the field is not present in the new object', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
mockObjectSelect.triggerCallback('change', 'object2');
expect(keySelect.getSelected()).toEqual('');
});
it('maintains its selected state on change if field is present in new object', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager);
mockObjectSelect.triggerCallback('change', 'object3');
expect(keySelect.getSelected()).toEqual('a');
});
}); });

View File

@ -1,112 +1,134 @@
define(['../../src/input/ObjectSelect'], function (ObjectSelect) { /*****************************************************************************
describe('A select for choosing composition objects', function () { * Open MCT, Copyright (c) 2014-2023, United States Government
let mockConfig; * as represented by the Administrator of the National Aeronautics and Space
let mockBadConfig; * Administration. All rights reserved.
let mockManager; *
let objectSelect; * Open MCT is licensed under the Apache License, Version 2.0 (the
let mockComposition; * "License"); you may not use this file except in compliance with the License.
beforeEach(function () { * You may obtain a copy of the License at
mockConfig = { * http://www.apache.org/licenses/LICENSE-2.0.
object: 'key1' *
}; * 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.
*****************************************************************************/
mockBadConfig = { import ObjectSelect from '../../src/input/ObjectSelect';
object: 'someNonexistentObject'
};
mockComposition = { describe('A select for choosing composition objects', function () {
key1: { let mockConfig;
identifier: { let mockBadConfig;
key: 'key1' let mockManager;
}, let objectSelect;
name: 'Object 1' let mockComposition;
}, beforeEach(function () {
key2: { mockConfig = {
identifier: { object: 'key1'
key: 'key2' };
},
name: 'Object 2'
}
};
mockManager = jasmine.createSpyObj('mockManager', [
'on',
'loadCompleted',
'triggerCallback',
'getComposition'
]);
mockManager.on.and.callFake((event, callback) => { mockBadConfig = {
mockManager.callbacks = mockManager.callbacks || {}; object: 'someNonexistentObject'
mockManager.callbacks[event] = callback; };
});
mockManager.triggerCallback.and.callFake((event, newObj) => { mockComposition = {
if (event === 'add') { key1: {
mockManager.callbacks.add(newObj);
} else {
mockManager.callbacks[event]();
}
});
mockManager.getComposition.and.callFake(function () {
return mockComposition;
});
});
it('allows setting special keyword options', function () {
mockManager.loadCompleted.and.returnValue(true);
objectSelect = new ObjectSelect(mockConfig, mockManager, [
['keyword1', 'A special option'],
['keyword2', 'A special option']
]);
objectSelect.setSelected('keyword1');
expect(objectSelect.getSelected()).toEqual('keyword1');
});
it('waits until the composition fully loads to populate itself', function () {
mockManager.loadCompleted.and.returnValue(false);
objectSelect = new ObjectSelect(mockConfig, mockManager);
expect(objectSelect.getSelected()).toEqual('');
});
it('populates itself with composition objects on a composition load', function () {
mockManager.loadCompleted.and.returnValue(false);
objectSelect = new ObjectSelect(mockConfig, mockManager);
mockManager.triggerCallback('load');
expect(objectSelect.getSelected()).toEqual('key1');
});
it('populates itself with composition objects if load is already complete', function () {
mockManager.loadCompleted.and.returnValue(true);
objectSelect = new ObjectSelect(mockConfig, mockManager);
expect(objectSelect.getSelected()).toEqual('key1');
});
it('clears its selection state if the object in its config is not in the composition', function () {
mockManager.loadCompleted.and.returnValue(true);
objectSelect = new ObjectSelect(mockBadConfig, mockManager);
expect(objectSelect.getSelected()).toEqual('');
});
it('adds a new option on a composition add', function () {
mockManager.loadCompleted.and.returnValue(true);
objectSelect = new ObjectSelect(mockConfig, mockManager);
mockManager.triggerCallback('add', {
identifier: { identifier: {
key: 'key3' key: 'key1'
}, },
name: 'Object 3' name: 'Object 1'
}); },
objectSelect.setSelected('key3'); key2: {
expect(objectSelect.getSelected()).toEqual('key3'); identifier: {
key: 'key2'
},
name: 'Object 2'
}
};
mockManager = jasmine.createSpyObj('mockManager', [
'on',
'loadCompleted',
'triggerCallback',
'getComposition'
]);
mockManager.on.and.callFake((event, callback) => {
mockManager.callbacks = mockManager.callbacks || {};
mockManager.callbacks[event] = callback;
}); });
it('removes an option on a composition remove', function () { mockManager.triggerCallback.and.callFake((event, newObj) => {
mockManager.loadCompleted.and.returnValue(true); if (event === 'add') {
objectSelect = new ObjectSelect(mockConfig, mockManager); mockManager.callbacks.add(newObj);
delete mockComposition.key1; } else {
mockManager.triggerCallback('remove'); mockManager.callbacks[event]();
expect(objectSelect.getSelected()).not.toEqual('key1'); }
});
mockManager.getComposition.and.callFake(function () {
return mockComposition;
}); });
}); });
it('allows setting special keyword options', function () {
mockManager.loadCompleted.and.returnValue(true);
objectSelect = new ObjectSelect(mockConfig, mockManager, [
['keyword1', 'A special option'],
['keyword2', 'A special option']
]);
objectSelect.setSelected('keyword1');
expect(objectSelect.getSelected()).toEqual('keyword1');
});
it('waits until the composition fully loads to populate itself', function () {
mockManager.loadCompleted.and.returnValue(false);
objectSelect = new ObjectSelect(mockConfig, mockManager);
expect(objectSelect.getSelected()).toEqual('');
});
it('populates itself with composition objects on a composition load', function () {
mockManager.loadCompleted.and.returnValue(false);
objectSelect = new ObjectSelect(mockConfig, mockManager);
mockManager.triggerCallback('load');
expect(objectSelect.getSelected()).toEqual('key1');
});
it('populates itself with composition objects if load is already complete', function () {
mockManager.loadCompleted.and.returnValue(true);
objectSelect = new ObjectSelect(mockConfig, mockManager);
expect(objectSelect.getSelected()).toEqual('key1');
});
it('clears its selection state if the object in its config is not in the composition', function () {
mockManager.loadCompleted.and.returnValue(true);
objectSelect = new ObjectSelect(mockBadConfig, mockManager);
expect(objectSelect.getSelected()).toEqual('');
});
it('adds a new option on a composition add', function () {
mockManager.loadCompleted.and.returnValue(true);
objectSelect = new ObjectSelect(mockConfig, mockManager);
mockManager.triggerCallback('add', {
identifier: {
key: 'key3'
},
name: 'Object 3'
});
objectSelect.setSelected('key3');
expect(objectSelect.getSelected()).toEqual('key3');
});
it('removes an option on a composition remove', function () {
mockManager.loadCompleted.and.returnValue(true);
objectSelect = new ObjectSelect(mockConfig, mockManager);
delete mockComposition.key1;
mockManager.triggerCallback('remove');
expect(objectSelect.getSelected()).not.toEqual('key1');
});
}); });

View File

@ -1,143 +1,165 @@
define(['../../src/input/OperationSelect'], function (OperationSelect) { /*****************************************************************************
describe('A select for choosing composition object properties', function () { * Open MCT, Copyright (c) 2014-2023, United States Government
let mockConfig; * as represented by the Administrator of the National Aeronautics and Space
let mockBadConfig; * Administration. All rights reserved.
let mockManager; *
let operationSelect; * Open MCT is licensed under the Apache License, Version 2.0 (the
let mockOperations; * "License"); you may not use this file except in compliance with the License.
let mockPropertyTypes; * You may obtain a copy of the License at
let mockKeySelect; * http://www.apache.org/licenses/LICENSE-2.0.
let mockEvaluator; *
beforeEach(function () { * Unless required by applicable law or agreed to in writing, software
mockConfig = { * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
object: 'object1', * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
key: 'a', * License for the specific language governing permissions and limitations
operation: 'operation1' * 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.
*****************************************************************************/
mockBadConfig = { import OperationSelect from '../../src/input/OperationSelect';
object: 'object1',
key: 'a',
operation: 'someNonexistentOperation'
};
mockOperations = { describe('A select for choosing composition object properties', function () {
operation1: { let mockConfig;
text: 'An operation', let mockBadConfig;
appliesTo: ['number'] let mockManager;
}, let operationSelect;
operation2: { let mockOperations;
text: 'Another operation', let mockPropertyTypes;
appliesTo: ['string'] let mockKeySelect;
} let mockEvaluator;
}; beforeEach(function () {
mockConfig = {
object: 'object1',
key: 'a',
operation: 'operation1'
};
mockPropertyTypes = { mockBadConfig = {
object1: { object: 'object1',
a: 'number', key: 'a',
b: 'string', operation: 'someNonexistentOperation'
c: 'number' };
}
};
mockManager = jasmine.createSpyObj('mockManager', [ mockOperations = {
'on', operation1: {
'metadataLoadCompleted', text: 'An operation',
'triggerCallback', appliesTo: ['number']
'getTelemetryPropertyType', },
'getEvaluator' operation2: {
]); text: 'Another operation',
appliesTo: ['string']
}
};
mockKeySelect = jasmine.createSpyObj('mockKeySelect', ['on', 'triggerCallback']); mockPropertyTypes = {
object1: {
a: 'number',
b: 'string',
c: 'number'
}
};
mockEvaluator = jasmine.createSpyObj('mockEvaluator', [ mockManager = jasmine.createSpyObj('mockManager', [
'getOperationKeys', 'on',
'operationAppliesTo', 'metadataLoadCompleted',
'getOperationText' 'triggerCallback',
]); 'getTelemetryPropertyType',
'getEvaluator'
]);
mockEvaluator.getOperationKeys.and.returnValue(Object.keys(mockOperations)); mockKeySelect = jasmine.createSpyObj('mockKeySelect', ['on', 'triggerCallback']);
mockEvaluator.getOperationText.and.callFake(function (key) { mockEvaluator = jasmine.createSpyObj('mockEvaluator', [
return mockOperations[key].text; 'getOperationKeys',
}); 'operationAppliesTo',
'getOperationText'
]);
mockEvaluator.operationAppliesTo.and.callFake(function (operation, type) { mockEvaluator.getOperationKeys.and.returnValue(Object.keys(mockOperations));
return mockOperations[operation].appliesTo.includes(type);
});
mockKeySelect.on.and.callFake((event, callback) => { mockEvaluator.getOperationText.and.callFake(function (key) {
mockKeySelect.callbacks = mockKeySelect.callbacks || {}; return mockOperations[key].text;
mockKeySelect.callbacks[event] = callback;
});
mockKeySelect.triggerCallback.and.callFake((event, key) => {
mockKeySelect.callbacks[event](key);
});
mockManager.on.and.callFake((event, callback) => {
mockManager.callbacks = mockManager.callbacks || {};
mockManager.callbacks[event] = callback;
});
mockManager.triggerCallback.and.callFake((event) => {
mockManager.callbacks[event]();
});
mockManager.getTelemetryPropertyType.and.callFake(function (object, key) {
return mockPropertyTypes[object][key];
});
mockManager.getEvaluator.and.returnValue(mockEvaluator);
}); });
it('waits until the metadata fully loads to populate itself', function () { mockEvaluator.operationAppliesTo.and.callFake(function (operation, type) {
mockManager.metadataLoadCompleted.and.returnValue(false); return mockOperations[operation].appliesTo.includes(type);
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
expect(operationSelect.getSelected()).toEqual('');
}); });
it('populates itself with operations on a metadata load', function () { mockKeySelect.on.and.callFake((event, callback) => {
mockManager.metadataLoadCompleted.and.returnValue(false); mockKeySelect.callbacks = mockKeySelect.callbacks || {};
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); mockKeySelect.callbacks[event] = callback;
mockManager.triggerCallback('metadata');
expect(operationSelect.getSelected()).toEqual('operation1');
}); });
it('populates itself with operations if metadata load is already complete', function () { mockKeySelect.triggerCallback.and.callFake((event, key) => {
mockManager.metadataLoadCompleted.and.returnValue(true); mockKeySelect.callbacks[event](key);
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
expect(operationSelect.getSelected()).toEqual('operation1');
}); });
it('clears its selection state if the operation in its config does not apply', function () { mockManager.on.and.callFake((event, callback) => {
mockManager.metadataLoadCompleted.and.returnValue(true); mockManager.callbacks = mockManager.callbacks || {};
operationSelect = new OperationSelect(mockBadConfig, mockKeySelect, mockManager); mockManager.callbacks[event] = callback;
expect(operationSelect.getSelected()).toEqual('');
}); });
it('populates with the appropriate options when its linked key changes', function () { mockManager.triggerCallback.and.callFake((event) => {
mockManager.metadataLoadCompleted.and.returnValue(true); mockManager.callbacks[event]();
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
mockKeySelect.triggerCallback('change', 'b');
operationSelect.setSelected('operation2');
expect(operationSelect.getSelected()).toEqual('operation2');
operationSelect.setSelected('operation1');
expect(operationSelect.getSelected()).not.toEqual('operation1');
}); });
it('clears its selection on a change if the operation does not apply', function () { mockManager.getTelemetryPropertyType.and.callFake(function (object, key) {
mockManager.metadataLoadCompleted.and.returnValue(true); return mockPropertyTypes[object][key];
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
mockKeySelect.triggerCallback('change', 'b');
expect(operationSelect.getSelected()).toEqual('');
}); });
it('maintains its selected state on change if the operation does apply', function () { mockManager.getEvaluator.and.returnValue(mockEvaluator);
mockManager.metadataLoadCompleted.and.returnValue(true); });
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
mockKeySelect.triggerCallback('change', 'c'); it('waits until the metadata fully loads to populate itself', function () {
expect(operationSelect.getSelected()).toEqual('operation1'); mockManager.metadataLoadCompleted.and.returnValue(false);
}); operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
expect(operationSelect.getSelected()).toEqual('');
});
it('populates itself with operations on a metadata load', function () {
mockManager.metadataLoadCompleted.and.returnValue(false);
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
mockManager.triggerCallback('metadata');
expect(operationSelect.getSelected()).toEqual('operation1');
});
it('populates itself with operations if metadata load is already complete', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
expect(operationSelect.getSelected()).toEqual('operation1');
});
it('clears its selection state if the operation in its config does not apply', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
operationSelect = new OperationSelect(mockBadConfig, mockKeySelect, mockManager);
expect(operationSelect.getSelected()).toEqual('');
});
it('populates with the appropriate options when its linked key changes', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
mockKeySelect.triggerCallback('change', 'b');
operationSelect.setSelected('operation2');
expect(operationSelect.getSelected()).toEqual('operation2');
operationSelect.setSelected('operation1');
expect(operationSelect.getSelected()).not.toEqual('operation1');
});
it('clears its selection on a change if the operation does not apply', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
mockKeySelect.triggerCallback('change', 'b');
expect(operationSelect.getSelected()).toEqual('');
});
it('maintains its selected state on change if the operation does apply', function () {
mockManager.metadataLoadCompleted.and.returnValue(true);
operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager);
mockKeySelect.triggerCallback('change', 'c');
expect(operationSelect.getSelected()).toEqual('operation1');
}); });
}); });

View File

@ -1,44 +1,44 @@
define(['../../src/input/Palette'], function (Palette) { import Palette from '../../src/input/Palette';
describe('A generic Open MCT palette input', function () {
let palette;
let callbackSpy1;
let callbackSpy2;
beforeEach(function () { describe('A generic Open MCT palette input', function () {
palette = new Palette('someClass', 'someContainer', ['item1', 'item2', 'item3']); let palette;
callbackSpy1 = jasmine.createSpy('changeCallback1'); let callbackSpy1;
callbackSpy2 = jasmine.createSpy('changeCallback2'); let callbackSpy2;
});
it('gets the current item', function () { beforeEach(function () {
expect(palette.getCurrent()).toEqual('item1'); palette = new Palette('someClass', 'someContainer', ['item1', 'item2', 'item3']);
}); callbackSpy1 = jasmine.createSpy('changeCallback1');
callbackSpy2 = jasmine.createSpy('changeCallback2');
});
it('allows setting the current item', function () { it('gets the current item', function () {
palette.set('item2'); expect(palette.getCurrent()).toEqual('item1');
expect(palette.getCurrent()).toEqual('item2'); });
});
it('allows registering change callbacks, and errors when an unsupported event is registered', function () { it('allows setting the current item', function () {
expect(function () { palette.set('item2');
palette.on('change', callbackSpy1); expect(palette.getCurrent()).toEqual('item2');
}).not.toThrow(); });
expect(function () {
palette.on('someUnsupportedEvent', callbackSpy1);
}).toThrow();
});
it('injects its callbacks with the new selected item on change', function () { it('allows registering change callbacks, and errors when an unsupported event is registered', function () {
expect(function () {
palette.on('change', callbackSpy1); palette.on('change', callbackSpy1);
palette.on('change', callbackSpy2); }).not.toThrow();
palette.set('item2'); expect(function () {
expect(callbackSpy1).toHaveBeenCalledWith('item2'); palette.on('someUnsupportedEvent', callbackSpy1);
expect(callbackSpy2).toHaveBeenCalledWith('item2'); }).toThrow();
}); });
it('gracefully handles being set to an item not included in its set', function () { it('injects its callbacks with the new selected item on change', function () {
palette.set('foobar'); palette.on('change', callbackSpy1);
expect(palette.getCurrent()).not.toEqual('foobar'); palette.on('change', callbackSpy2);
}); palette.set('item2');
expect(callbackSpy1).toHaveBeenCalledWith('item2');
expect(callbackSpy2).toHaveBeenCalledWith('item2');
});
it('gracefully handles being set to an item not included in its set', function () {
palette.set('foobar');
expect(palette.getCurrent()).not.toEqual('foobar');
}); });
}); });

View File

@ -1,61 +1,61 @@
define(['../../src/input/Select'], function (Select) { import Select from '../../src/input/Select';
describe('A select wrapper', function () {
let select;
let testOptions;
let callbackSpy1;
let callbackSpy2;
beforeEach(function () {
select = new Select();
testOptions = [
['item1', 'Item 1'],
['item2', 'Item 2'],
['item3', 'Item 3']
];
select.setOptions(testOptions);
callbackSpy1 = jasmine.createSpy('callbackSpy1');
callbackSpy2 = jasmine.createSpy('callbackSpy2');
});
it('gets and sets the current item', function () { describe('A select wrapper', function () {
select.setSelected('item1'); let select;
expect(select.getSelected()).toEqual('item1'); let testOptions;
}); let callbackSpy1;
let callbackSpy2;
beforeEach(function () {
select = new Select();
testOptions = [
['item1', 'Item 1'],
['item2', 'Item 2'],
['item3', 'Item 3']
];
select.setOptions(testOptions);
callbackSpy1 = jasmine.createSpy('callbackSpy1');
callbackSpy2 = jasmine.createSpy('callbackSpy2');
});
it('allows adding a single new option', function () { it('gets and sets the current item', function () {
select.addOption('newOption', 'A New Option'); select.setSelected('item1');
select.setSelected('newOption'); expect(select.getSelected()).toEqual('item1');
expect(select.getSelected()).toEqual('newOption'); });
});
it('allows populating with a new set of options', function () { it('allows adding a single new option', function () {
select.setOptions([ select.addOption('newOption', 'A New Option');
['newItem1', 'Item 1'], select.setSelected('newOption');
['newItem2', 'Item 2'] expect(select.getSelected()).toEqual('newOption');
]); });
select.setSelected('newItem1');
expect(select.getSelected()).toEqual('newItem1');
});
it('allows registering change callbacks, and errors when an unsupported event is registered', function () { it('allows populating with a new set of options', function () {
expect(function () { select.setOptions([
select.on('change', callbackSpy1); ['newItem1', 'Item 1'],
}).not.toThrow(); ['newItem2', 'Item 2']
expect(function () { ]);
select.on('someUnsupportedEvent', callbackSpy1); select.setSelected('newItem1');
}).toThrow(); expect(select.getSelected()).toEqual('newItem1');
}); });
it('injects its callbacks with its property and value on a change', function () { it('allows registering change callbacks, and errors when an unsupported event is registered', function () {
expect(function () {
select.on('change', callbackSpy1); select.on('change', callbackSpy1);
select.on('change', callbackSpy2); }).not.toThrow();
select.setSelected('item2'); expect(function () {
expect(callbackSpy1).toHaveBeenCalledWith('item2'); select.on('someUnsupportedEvent', callbackSpy1);
expect(callbackSpy2).toHaveBeenCalledWith('item2'); }).toThrow();
}); });
it('gracefully handles being set to an item not included in its set', function () { it('injects its callbacks with its property and value on a change', function () {
select.setSelected('foobar'); select.on('change', callbackSpy1);
expect(select.getSelected()).not.toEqual('foobar'); select.on('change', callbackSpy2);
}); select.setSelected('item2');
expect(callbackSpy1).toHaveBeenCalledWith('item2');
expect(callbackSpy2).toHaveBeenCalledWith('item2');
});
it('gracefully handles being set to an item not included in its set', function () {
select.setSelected('foobar');
expect(select.getSelected()).not.toEqual('foobar');
}); });
}); });

View File

@ -19,41 +19,40 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import Tabs from './tabs';
define(['./tabs'], function (Tabs) { export default function plugin() {
return function plugin() { return function install(openmct) {
return function install(openmct) { openmct.objectViews.addProvider(new Tabs(openmct));
openmct.objectViews.addProvider(new Tabs.default(openmct));
openmct.types.addType('tabs', { openmct.types.addType('tabs', {
name: 'Tabs View', name: 'Tabs View',
description: 'Quickly navigate between multiple objects of any type using tabs.', description: 'Quickly navigate between multiple objects of any type using tabs.',
creatable: true, creatable: true,
cssClass: 'icon-tabs-view', cssClass: 'icon-tabs-view',
initialize(domainObject) { initialize(domainObject) {
domainObject.composition = []; domainObject.composition = [];
domainObject.keep_alive = true; domainObject.keep_alive = true;
}, },
form: [ form: [
{ {
key: 'keep_alive', key: 'keep_alive',
name: 'Eager Load Tabs', name: 'Eager Load Tabs',
control: 'select', control: 'select',
options: [ options: [
{ {
name: 'True', name: 'True',
value: true value: true
}, },
{ {
name: 'False', name: 'False',
value: false value: false
} }
], ],
required: true, required: true,
cssClass: 'l-input' cssClass: 'l-input'
} }
] ]
}); });
};
}; };
}); }

View File

@ -47,7 +47,7 @@ export default class Tabs {
let component = null; let component = null;
return { return {
show: function (element, editMode) { show(element, editMode) {
const { vNode, destroy } = mount( const { vNode, destroy } = mount(
{ {
el: element, el: element,

View File

@ -20,59 +20,57 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./src/MeanTelemetryProvider'], function (MeanTelemetryProvider) { import MeanTelemetryProvider from './src/MeanTelemetryProvider';
const DEFAULT_SAMPLES = 10;
function plugin() { const DEFAULT_SAMPLES = 10;
return function install(openmct) {
openmct.types.addType('telemetry-mean', { export default function plugin() {
name: 'Telemetry Filter', return function install(openmct) {
description: openmct.types.addType('telemetry-mean', {
'Provides telemetry values that represent the mean of the last N values of a telemetry stream', name: 'Telemetry Filter',
creatable: true, description:
cssClass: 'icon-telemetry', 'Provides telemetry values that represent the mean of the last N values of a telemetry stream',
initialize: function (domainObject) { creatable: true,
domainObject.samples = DEFAULT_SAMPLES; cssClass: 'icon-telemetry',
domainObject.telemetry = {}; initialize: function (domainObject) {
domainObject.telemetry.values = openmct.time domainObject.samples = DEFAULT_SAMPLES;
.getAllTimeSystems() domainObject.telemetry = {};
.map(function (timeSystem, index) { domainObject.telemetry.values = openmct.time
return { .getAllTimeSystems()
key: timeSystem.key, .map(function (timeSystem, index) {
name: timeSystem.name, return {
hints: { key: timeSystem.key,
domain: index + 1 name: timeSystem.name,
} hints: {
}; domain: index + 1
}); }
domainObject.telemetry.values.push({ };
key: 'value',
name: 'Value',
hints: {
range: 1
}
}); });
}, domainObject.telemetry.values.push({
form: [ key: 'value',
{ name: 'Value',
key: 'telemetryPoint', hints: {
name: 'Telemetry Point', range: 1
control: 'textfield',
required: true,
cssClass: 'l-input-lg'
},
{
key: 'samples',
name: 'Samples to Average',
control: 'textfield',
required: true,
cssClass: 'l-input-sm'
} }
] });
}); },
openmct.telemetry.addProvider(new MeanTelemetryProvider(openmct)); form: [
}; {
} key: 'telemetryPoint',
name: 'Telemetry Point',
return plugin; control: 'textfield',
}); required: true,
cssClass: 'l-input-lg'
},
{
key: 'samples',
name: 'Samples to Average',
control: 'textfield',
required: true,
cssClass: 'l-input-sm'
}
]
});
openmct.telemetry.addProvider(new MeanTelemetryProvider(openmct));
};
}

View File

@ -20,114 +20,114 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['objectUtils', './TelemetryAverager'], function (objectUtils, TelemetryAverager) { import objectUtils from 'objectUtils';
function MeanTelemetryProvider(openmct) {
this.openmct = openmct;
this.telemetryAPI = openmct.telemetry;
this.timeAPI = openmct.time;
this.objectAPI = openmct.objects;
this.perObjectProviders = {};
}
MeanTelemetryProvider.prototype.canProvideTelemetry = function (domainObject) { import TelemetryAverager from './TelemetryAverager';
return domainObject.type === 'telemetry-mean';
};
MeanTelemetryProvider.prototype.supportsRequest = export default function MeanTelemetryProvider(openmct) {
MeanTelemetryProvider.prototype.supportsSubscribe = this.openmct = openmct;
MeanTelemetryProvider.prototype.canProvideTelemetry; this.telemetryAPI = openmct.telemetry;
this.timeAPI = openmct.time;
this.objectAPI = openmct.objects;
this.perObjectProviders = {};
}
MeanTelemetryProvider.prototype.subscribe = function (domainObject, callback) { MeanTelemetryProvider.prototype.canProvideTelemetry = function (domainObject) {
let wrappedUnsubscribe; return domainObject.type === 'telemetry-mean';
let unsubscribeCalled = false; };
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
const samples = domainObject.samples;
this.objectAPI MeanTelemetryProvider.prototype.supportsRequest =
.get(objectId) MeanTelemetryProvider.prototype.supportsSubscribe =
.then( MeanTelemetryProvider.prototype.canProvideTelemetry;
function (linkedDomainObject) {
if (!unsubscribeCalled) {
wrappedUnsubscribe = this.subscribeToAverage(linkedDomainObject, samples, callback);
}
}.bind(this)
)
.catch(logError);
return function unsubscribe() { MeanTelemetryProvider.prototype.subscribe = function (domainObject, callback) {
unsubscribeCalled = true; let wrappedUnsubscribe;
if (wrappedUnsubscribe !== undefined) { let unsubscribeCalled = false;
wrappedUnsubscribe(); const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
} const samples = domainObject.samples;
};
};
MeanTelemetryProvider.prototype.subscribeToAverage = function (domainObject, samples, callback) { this.objectAPI
const telemetryAverager = new TelemetryAverager( .get(objectId)
this.telemetryAPI, .then(
this.timeAPI,
domainObject,
samples,
callback
);
const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager);
return this.telemetryAPI.subscribe(domainObject, createAverageDatum);
};
MeanTelemetryProvider.prototype.request = function (domainObject, request) {
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
const samples = domainObject.samples;
return this.objectAPI.get(objectId).then(
function (linkedDomainObject) { function (linkedDomainObject) {
return this.requestAverageTelemetry(linkedDomainObject, request, samples); if (!unsubscribeCalled) {
wrappedUnsubscribe = this.subscribeToAverage(linkedDomainObject, samples, callback);
}
}.bind(this) }.bind(this)
); )
}; .catch(logError);
/** return function unsubscribe() {
* @private unsubscribeCalled = true;
*/ if (wrappedUnsubscribe !== undefined) {
MeanTelemetryProvider.prototype.requestAverageTelemetry = function ( wrappedUnsubscribe();
domainObject,
request,
samples
) {
const averageData = [];
const addToAverageData = averageData.push.bind(averageData);
const telemetryAverager = new TelemetryAverager(
this.telemetryAPI,
this.timeAPI,
domainObject,
samples,
addToAverageData
);
const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager);
return this.telemetryAPI.request(domainObject, request).then(function (telemetryData) {
telemetryData.forEach(createAverageDatum);
return averageData;
});
};
/**
* @private
*/
MeanTelemetryProvider.prototype.getLinkedObject = function (domainObject) {
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
return this.objectAPI.get(objectId);
};
function logError(error) {
if (error.stack) {
console.error(error.stack);
} else {
console.error(error);
} }
} };
};
return MeanTelemetryProvider; MeanTelemetryProvider.prototype.subscribeToAverage = function (domainObject, samples, callback) {
}); const telemetryAverager = new TelemetryAverager(
this.telemetryAPI,
this.timeAPI,
domainObject,
samples,
callback
);
const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager);
return this.telemetryAPI.subscribe(domainObject, createAverageDatum);
};
MeanTelemetryProvider.prototype.request = function (domainObject, request) {
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
const samples = domainObject.samples;
return this.objectAPI.get(objectId).then(
function (linkedDomainObject) {
return this.requestAverageTelemetry(linkedDomainObject, request, samples);
}.bind(this)
);
};
/**
* @private
*/
MeanTelemetryProvider.prototype.requestAverageTelemetry = function (
domainObject,
request,
samples
) {
const averageData = [];
const addToAverageData = averageData.push.bind(averageData);
const telemetryAverager = new TelemetryAverager(
this.telemetryAPI,
this.timeAPI,
domainObject,
samples,
addToAverageData
);
const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager);
return this.telemetryAPI.request(domainObject, request).then(function (telemetryData) {
telemetryData.forEach(createAverageDatum);
return averageData;
});
};
/**
* @private
*/
MeanTelemetryProvider.prototype.getLinkedObject = function (domainObject) {
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
return this.objectAPI.get(objectId);
};
function logError(error) {
if (error.stack) {
console.error(error.stack);
} else {
console.error(error);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -20,82 +20,78 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { export default function MockTelemetryApi() {
function MockTelemetryApi() { this.createSpy('subscribe');
this.createSpy('subscribe'); this.createSpy('getMetadata');
this.createSpy('getMetadata');
this.metadata = this.createMockMetadata(); this.metadata = this.createMockMetadata();
this.setDefaultRangeTo('defaultRange'); this.setDefaultRangeTo('defaultRange');
this.unsubscribe = jasmine.createSpy('unsubscribe'); this.unsubscribe = jasmine.createSpy('unsubscribe');
this.mockReceiveTelemetry = this.mockReceiveTelemetry.bind(this); this.mockReceiveTelemetry = this.mockReceiveTelemetry.bind(this);
} }
MockTelemetryApi.prototype.subscribe = function () { MockTelemetryApi.prototype.subscribe = function () {
return this.unsubscribe; return this.unsubscribe;
};
MockTelemetryApi.prototype.getMetadata = function (object) {
return this.metadata;
};
MockTelemetryApi.prototype.request = jasmine.createSpy('request');
MockTelemetryApi.prototype.getValueFormatter = function (valueMetadata) {
const mockValueFormatter = jasmine.createSpyObj('valueFormatter', ['parse']);
mockValueFormatter.parse.and.callFake(function (value) {
return value[valueMetadata.key];
});
return mockValueFormatter;
};
MockTelemetryApi.prototype.mockReceiveTelemetry = function (newTelemetryDatum) {
const subscriptionCallback = this.subscribe.calls.mostRecent().args[1];
subscriptionCallback(newTelemetryDatum);
};
/**
* @private
*/
MockTelemetryApi.prototype.onRequestReturn = function (telemetryData) {
this.requestTelemetry = telemetryData;
};
/**
* @private
*/
MockTelemetryApi.prototype.setDefaultRangeTo = function (rangeKey) {
const mockMetadataValue = {
key: rangeKey
}; };
this.metadata.valuesForHints.and.returnValue([mockMetadataValue]);
};
MockTelemetryApi.prototype.getMetadata = function (object) { /**
return this.metadata; * @private
}; */
MockTelemetryApi.prototype.createMockMetadata = function () {
const mockMetadata = jasmine.createSpyObj('metadata', ['value', 'valuesForHints']);
MockTelemetryApi.prototype.request = jasmine.createSpy('request'); mockMetadata.value.and.callFake(function (key) {
return {
MockTelemetryApi.prototype.getValueFormatter = function (valueMetadata) { key: key
const mockValueFormatter = jasmine.createSpyObj('valueFormatter', ['parse']);
mockValueFormatter.parse.and.callFake(function (value) {
return value[valueMetadata.key];
});
return mockValueFormatter;
};
MockTelemetryApi.prototype.mockReceiveTelemetry = function (newTelemetryDatum) {
const subscriptionCallback = this.subscribe.calls.mostRecent().args[1];
subscriptionCallback(newTelemetryDatum);
};
/**
* @private
*/
MockTelemetryApi.prototype.onRequestReturn = function (telemetryData) {
this.requestTelemetry = telemetryData;
};
/**
* @private
*/
MockTelemetryApi.prototype.setDefaultRangeTo = function (rangeKey) {
const mockMetadataValue = {
key: rangeKey
}; };
this.metadata.valuesForHints.and.returnValue([mockMetadataValue]); });
};
/** return mockMetadata;
* @private };
*/
MockTelemetryApi.prototype.createMockMetadata = function () {
const mockMetadata = jasmine.createSpyObj('metadata', ['value', 'valuesForHints']);
mockMetadata.value.and.callFake(function (key) { /**
return { * @private
key: key */
}; MockTelemetryApi.prototype.createSpy = function (functionName) {
}); this[functionName] = this[functionName].bind(this);
spyOn(this, functionName);
return mockMetadata; this[functionName].and.callThrough();
}; };
/**
* @private
*/
MockTelemetryApi.prototype.createSpy = function (functionName) {
this[functionName] = this[functionName].bind(this);
spyOn(this, functionName);
this[functionName].and.callThrough();
};
return MockTelemetryApi;
});

View File

@ -20,100 +20,102 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { export default function TelemetryAverager(
function TelemetryAverager(telemetryAPI, timeAPI, domainObject, samples, averageDatumCallback) { telemetryAPI,
this.telemetryAPI = telemetryAPI; timeAPI,
this.timeAPI = timeAPI; domainObject,
samples,
averageDatumCallback
) {
this.telemetryAPI = telemetryAPI;
this.timeAPI = timeAPI;
this.domainObject = domainObject; this.domainObject = domainObject;
this.samples = samples; this.samples = samples;
this.averagingWindow = []; this.averagingWindow = [];
this.rangeKey = undefined; this.rangeKey = undefined;
this.rangeFormatter = undefined; this.rangeFormatter = undefined;
this.setRangeKeyAndFormatter(); this.setRangeKeyAndFormatter();
// Defined dynamically based on current time system // Defined dynamically based on current time system
this.domainKey = undefined; this.domainKey = undefined;
this.domainFormatter = undefined; this.domainFormatter = undefined;
this.averageDatumCallback = averageDatumCallback; this.averageDatumCallback = averageDatumCallback;
}
TelemetryAverager.prototype.createAverageDatum = function (telemetryDatum) {
this.setDomainKeyAndFormatter();
const timeValue = this.domainFormatter.parse(telemetryDatum);
const rangeValue = this.rangeFormatter.parse(telemetryDatum);
this.averagingWindow.push(rangeValue);
if (this.averagingWindow.length < this.samples) {
// We do not have enough data to produce an average
return;
} else if (this.averagingWindow.length > this.samples) {
//Do not let averaging window grow beyond defined sample size
this.averagingWindow.shift();
} }
TelemetryAverager.prototype.createAverageDatum = function (telemetryDatum) { const averageValue = this.calculateMean();
this.setDomainKeyAndFormatter();
const timeValue = this.domainFormatter.parse(telemetryDatum); const meanDatum = {};
const rangeValue = this.rangeFormatter.parse(telemetryDatum); meanDatum[this.domainKey] = timeValue;
meanDatum.value = averageValue;
this.averagingWindow.push(rangeValue); this.averageDatumCallback(meanDatum);
};
if (this.averagingWindow.length < this.samples) { /**
// We do not have enough data to produce an average * @private
return; */
} else if (this.averagingWindow.length > this.samples) { TelemetryAverager.prototype.calculateMean = function () {
//Do not let averaging window grow beyond defined sample size let sum = 0;
this.averagingWindow.shift(); let i = 0;
}
const averageValue = this.calculateMean(); for (; i < this.averagingWindow.length; i++) {
sum += this.averagingWindow[i];
}
const meanDatum = {}; return sum / this.averagingWindow.length;
meanDatum[this.domainKey] = timeValue; };
meanDatum.value = averageValue;
this.averageDatumCallback(meanDatum); /**
}; * The mean telemetry filter produces domain values in whatever time
* system is currently selected from the conductor. Because this can
* change dynamically, the averager needs to be updated regularly with
* the current domain.
* @private
*/
TelemetryAverager.prototype.setDomainKeyAndFormatter = function () {
const domainKey = this.timeAPI.timeSystem().key;
if (domainKey !== this.domainKey) {
this.domainKey = domainKey;
this.domainFormatter = this.getFormatter(domainKey);
}
};
/** /**
* @private * @private
*/ */
TelemetryAverager.prototype.calculateMean = function () { TelemetryAverager.prototype.setRangeKeyAndFormatter = function () {
let sum = 0; const metadatas = this.telemetryAPI.getMetadata(this.domainObject);
let i = 0; const rangeValues = metadatas.valuesForHints(['range']);
for (; i < this.averagingWindow.length; i++) { this.rangeKey = rangeValues[0].key;
sum += this.averagingWindow[i]; this.rangeFormatter = this.getFormatter(this.rangeKey);
} };
return sum / this.averagingWindow.length; /**
}; * @private
*/
TelemetryAverager.prototype.getFormatter = function (key) {
const objectMetadata = this.telemetryAPI.getMetadata(this.domainObject);
const valueMetadata = objectMetadata.value(key);
/** return this.telemetryAPI.getValueFormatter(valueMetadata);
* The mean telemetry filter produces domain values in whatever time };
* system is currently selected from the conductor. Because this can
* change dynamically, the averager needs to be updated regularly with
* the current domain.
* @private
*/
TelemetryAverager.prototype.setDomainKeyAndFormatter = function () {
const domainKey = this.timeAPI.timeSystem().key;
if (domainKey !== this.domainKey) {
this.domainKey = domainKey;
this.domainFormatter = this.getFormatter(domainKey);
}
};
/**
* @private
*/
TelemetryAverager.prototype.setRangeKeyAndFormatter = function () {
const metadatas = this.telemetryAPI.getMetadata(this.domainObject);
const rangeValues = metadatas.valuesForHints(['range']);
this.rangeKey = rangeValues[0].key;
this.rangeFormatter = this.getFormatter(this.rangeKey);
};
/**
* @private
*/
TelemetryAverager.prototype.getFormatter = function (key) {
const objectMetadata = this.telemetryAPI.getMetadata(this.domainObject);
const valueMetadata = objectMetadata.value(key);
return this.telemetryAPI.getValueFormatter(valueMetadata);
};
return TelemetryAverager;
});

View File

@ -20,457 +20,444 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([ import EventEmitter from 'EventEmitter';
'EventEmitter', import _ from 'lodash';
'lodash',
'./collections/TableRowCollection',
'./TelemetryTableRow',
'./TelemetryTableNameColumn',
'./TelemetryTableColumn',
'./TelemetryTableUnitColumn',
'./TelemetryTableConfiguration',
'../../utils/staleness'
], function (
EventEmitter,
_,
TableRowCollection,
TelemetryTableRow,
TelemetryTableNameColumn,
TelemetryTableColumn,
TelemetryTableUnitColumn,
TelemetryTableConfiguration,
StalenessUtils
) {
class TelemetryTable extends EventEmitter {
constructor(domainObject, openmct) {
super();
this.domainObject = domainObject; import StalenessUtils from '../../utils/staleness';
this.openmct = openmct; import TableRowCollection from './collections/TableRowCollection';
this.rowCount = 100; import TelemetryTableColumn from './TelemetryTableColumn';
this.tableComposition = undefined; import TelemetryTableConfiguration from './TelemetryTableConfiguration';
this.datumCache = []; import TelemetryTableNameColumn from './TelemetryTableNameColumn';
this.configuration = new TelemetryTableConfiguration(domainObject, openmct); import TelemetryTableRow from './TelemetryTableRow';
this.paused = false; import TelemetryTableUnitColumn from './TelemetryTableUnitColumn';
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.telemetryObjects = {}; export default class TelemetryTable extends EventEmitter {
this.subscribedStaleObjects = new Map(); constructor(domainObject, openmct) {
this.telemetryCollections = {}; super();
this.delayedActions = [];
this.outstandingRequests = 0;
this.stalenessSubscription = {};
this.addTelemetryObject = this.addTelemetryObject.bind(this); this.domainObject = domainObject;
this.removeTelemetryObject = this.removeTelemetryObject.bind(this); this.openmct = openmct;
this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this); this.rowCount = 100;
this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this); this.tableComposition = undefined;
this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this); this.datumCache = [];
this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this); this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
this.isTelemetryObject = this.isTelemetryObject.bind(this); this.paused = false;
this.updateFilters = this.updateFilters.bind(this); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.clearData = this.clearData.bind(this);
this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this);
this.filterObserver = undefined; this.telemetryObjects = {};
this.subscribedStaleObjects = new Map();
this.telemetryCollections = {};
this.delayedActions = [];
this.outstandingRequests = 0;
this.stalenessSubscription = {};
this.createTableRowCollections(); this.addTelemetryObject = this.addTelemetryObject.bind(this);
this.resubscribeToStaleness = this.resubscribeAllObjectsToStaleness.bind(this); this.removeTelemetryObject = this.removeTelemetryObject.bind(this);
this.openmct.time.on('clockChanged', this.resubscribeToStaleness); this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this);
} this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this);
this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this);
this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this);
this.isTelemetryObject = this.isTelemetryObject.bind(this);
this.updateFilters = this.updateFilters.bind(this);
this.clearData = this.clearData.bind(this);
this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this);
/** this.filterObserver = undefined;
* @private
*/
addNameColumn(telemetryObject, metadataValues) {
let metadatum = metadataValues.find((m) => m.key === 'name');
if (!metadatum) {
metadatum = {
format: 'string',
key: 'name',
name: 'Name'
};
}
const column = new TelemetryTableNameColumn(this.openmct, telemetryObject, metadatum); this.createTableRowCollections();
this.resubscribeToStaleness = this.resubscribeAllObjectsToStaleness.bind(this);
this.openmct.time.on('clockChanged', this.resubscribeToStaleness);
}
this.configuration.addSingleColumnForObject(telemetryObject, column); /**
} * @private
*/
initialize() { addNameColumn(telemetryObject, metadataValues) {
if (this.domainObject.type === 'table') { let metadatum = metadataValues.find((m) => m.key === 'name');
this.filterObserver = this.openmct.objects.observe( if (!metadatum) {
this.domainObject, metadatum = {
'configuration.filters', format: 'string',
this.updateFilters key: 'name',
); name: 'Name'
this.filters = this.domainObject.configuration.filters;
this.loadComposition();
} else {
this.addTelemetryObject(this.domainObject);
}
}
createTableRowCollections() {
this.tableRows = new TableRowCollection();
//Fetch any persisted default sort
let sortOptions = this.configuration.getConfiguration().sortOptions;
//If no persisted sort order, default to sorting by time system, ascending.
sortOptions = sortOptions || {
key: this.openmct.time.timeSystem().key,
direction: 'asc'
}; };
this.tableRows.sortBy(sortOptions);
this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);
} }
loadComposition() { const column = new TelemetryTableNameColumn(this.openmct, telemetryObject, metadatum);
this.tableComposition = this.openmct.composition.get(this.domainObject);
if (this.tableComposition !== undefined) { this.configuration.addSingleColumnForObject(telemetryObject, column);
this.tableComposition.load().then((composition) => { }
composition = composition.filter(this.isTelemetryObject);
composition.forEach(this.addTelemetryObject);
this.tableComposition.on('add', this.addTelemetryObject); initialize() {
this.tableComposition.on('remove', this.removeTelemetryObject); if (this.domainObject.type === 'table') {
}); this.filterObserver = this.openmct.objects.observe(
} this.domainObject,
} 'configuration.filters',
this.updateFilters
addTelemetryObject(telemetryObject) {
this.addColumnsForObject(telemetryObject, true);
const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let requestOptions = this.buildOptionsFromConfiguration(telemetryObject);
let columnMap = this.getColumnMapForObject(keyString);
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator);
const telemetryRemover = this.getTelemetryRemover();
this.removeTelemetryCollection(keyString);
this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection(
telemetryObject,
requestOptions
); );
this.filters = this.domainObject.configuration.filters;
this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests); this.loadComposition();
this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests); } else {
this.telemetryCollections[keyString].on('remove', telemetryRemover); this.addTelemetryObject(this.domainObject);
this.telemetryCollections[keyString].on('add', telemetryProcessor);
this.telemetryCollections[keyString].on('clear', this.clearData);
this.telemetryCollections[keyString].load();
this.subscribeToStaleness(telemetryObject);
this.telemetryObjects[keyString] = {
telemetryObject,
keyString,
requestOptions,
columnMap,
limitEvaluator
};
this.emit('object-added', telemetryObject);
}
resubscribeAllObjectsToStaleness() {
if (!this.subscribedStaleObjects || this.subscribedStaleObjects.size < 1) {
return;
}
for (const [, telemetryObject] of this.subscribedStaleObjects) {
this.subscribeToStaleness(telemetryObject);
}
}
subscribeToStaleness(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (this.stalenessSubscription?.[keyString]) {
this.unsubscribeFromStaleness(domainObject.identifier);
}
this.stalenessSubscription[keyString] = {};
this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils.default(
this.openmct,
domainObject
);
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(keyString, stalenessResponse);
}
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(
domainObject,
(stalenessResponse) => {
this.handleStaleness(keyString, stalenessResponse);
}
);
this.subscribedStaleObjects.set(keyString, domainObject);
this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription;
}
handleStaleness(keyString, stalenessResponse, skipCheck = false) {
if (
skipCheck ||
this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(
stalenessResponse,
keyString
)
) {
this.emit('telemetry-staleness', {
keyString,
stalenessResponse: stalenessResponse
});
}
}
getTelemetryProcessor(keyString, columnMap, limitEvaluator) {
return (telemetry) => {
//Check that telemetry object has not been removed since telemetry was requested.
if (!this.telemetryObjects[keyString]) {
return;
}
const metadataValue = this.openmct.telemetry
.getMetadata(this.telemetryObjects[keyString].telemetryObject)
.getUseToUpdateInPlaceValue();
let telemetryRows = telemetry.map(
(datum) =>
new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator, metadataValue?.key)
);
if (this.paused) {
this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));
} else {
this.tableRows.addRows(telemetryRows);
}
};
}
getTelemetryRemover() {
return (telemetry) => {
if (this.paused) {
this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry));
} else {
this.tableRows.removeRowsByData(telemetry);
}
};
}
/**
* @private
*/
incrementOutstandingRequests() {
if (this.outstandingRequests === 0) {
this.emit('outstanding-requests', true);
}
this.outstandingRequests++;
}
/**
* @private
*/
decrementOutstandingRequests() {
this.outstandingRequests--;
if (this.outstandingRequests === 0) {
this.emit('outstanding-requests', false);
}
}
// will pull all necessary information for all existing bounded telemetry
// and pass to table row collection to reset without making any new requests
// triggered by filtering
resetRowsFromAllData() {
let allRows = [];
Object.keys(this.telemetryCollections).forEach((keyString) => {
let { columnMap, limitEvaluator } = this.telemetryObjects[keyString];
const metadataValue = this.openmct.telemetry
.getMetadata(this.telemetryObjects[keyString].telemetryObject)
.getUseToUpdateInPlaceValue();
this.telemetryCollections[keyString].getAll().forEach((datum) => {
allRows.push(
new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator, metadataValue?.key)
);
});
});
this.tableRows.clearRowsFromTableAndFilter(allRows);
}
updateFilters(updatedFilters) {
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
this.filters = deepCopiedFilters;
this.tableRows.clear();
this.clearAndResubscribe();
} else {
this.filters = deepCopiedFilters;
}
}
clearAndResubscribe() {
let objectKeys = Object.keys(this.telemetryObjects);
this.tableRows.clear();
objectKeys.forEach((keyString) => {
this.addTelemetryObject(this.telemetryObjects[keyString].telemetryObject);
});
}
removeTelemetryObject(objectIdentifier) {
const keyString = this.openmct.objects.makeKeyString(objectIdentifier);
this.configuration.removeColumnsForObject(objectIdentifier, true);
this.tableRows.removeRowsByObject(keyString);
this.removeTelemetryCollection(keyString);
delete this.telemetryObjects[keyString];
this.emit('object-removed', objectIdentifier);
this.unsubscribeFromStaleness(objectIdentifier);
}
unsubscribeFromStaleness(objectIdentifier) {
const keyString = this.openmct.objects.makeKeyString(objectIdentifier);
const SKIP_CHECK = true;
this.stalenessSubscription[keyString].unsubscribe();
this.stalenessSubscription[keyString].stalenessUtils.destroy();
this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK);
delete this.stalenessSubscription[keyString];
}
clearData() {
this.tableRows.clear();
this.emit('refresh');
}
addColumnsForObject(telemetryObject) {
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValues = metadata.values();
this.addNameColumn(telemetryObject, metadataValues);
metadataValues.forEach((metadatum) => {
if (metadatum.key === 'name' || metadata.isInPlaceUpdateValue(metadatum)) {
return;
}
let column = this.createColumn(metadatum);
this.configuration.addSingleColumnForObject(telemetryObject, column);
// add units column if available
if (metadatum.unit !== undefined) {
let unitColumn = this.createUnitColumn(metadatum);
this.configuration.addSingleColumnForObject(telemetryObject, unitColumn);
}
});
}
getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns();
if (columns[objectKeyString]) {
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
return map;
}, {});
}
return {};
}
buildOptionsFromConfiguration(telemetryObject) {
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let filters =
this.domainObject.configuration &&
this.domainObject.configuration.filters &&
this.domainObject.configuration.filters[keyString];
return { filters } || {};
}
createColumn(metadatum) {
return new TelemetryTableColumn(this.openmct, metadatum);
}
createUnitColumn(metadatum) {
return new TelemetryTableUnitColumn(this.openmct, metadatum);
}
isTelemetryObject(domainObject) {
return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry');
}
sortBy(sortOptions) {
this.tableRows.sortBy(sortOptions);
if (this.openmct.editor.isEditing()) {
let configuration = this.configuration.getConfiguration();
configuration.sortOptions = sortOptions;
this.configuration.updateConfiguration(configuration);
}
}
runDelayedActions() {
this.delayedActions.forEach((action) => action());
this.delayedActions = [];
}
removeTelemetryCollection(keyString) {
if (this.telemetryCollections[keyString]) {
this.telemetryCollections[keyString].destroy();
this.telemetryCollections[keyString] = undefined;
delete this.telemetryCollections[keyString];
}
}
pause() {
this.paused = true;
}
unpause() {
this.paused = false;
this.runDelayedActions();
}
destroy() {
this.tableRows.destroy();
this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData);
this.openmct.time.off('clockChanged', this.resubscribeToStaleness);
let keystrings = Object.keys(this.telemetryCollections);
keystrings.forEach(this.removeTelemetryCollection);
if (this.filterObserver) {
this.filterObserver();
}
Object.values(this.stalenessSubscription).forEach((stalenessSubscription) => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
if (this.tableComposition !== undefined) {
this.tableComposition.off('add', this.addTelemetryObject);
this.tableComposition.off('remove', this.removeTelemetryObject);
}
} }
} }
return TelemetryTable; createTableRowCollections() {
}); this.tableRows = new TableRowCollection();
//Fetch any persisted default sort
let sortOptions = this.configuration.getConfiguration().sortOptions;
//If no persisted sort order, default to sorting by time system, ascending.
sortOptions = sortOptions || {
key: this.openmct.time.timeSystem().key,
direction: 'asc'
};
this.tableRows.sortBy(sortOptions);
this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);
}
loadComposition() {
this.tableComposition = this.openmct.composition.get(this.domainObject);
if (this.tableComposition !== undefined) {
this.tableComposition.load().then((composition) => {
composition = composition.filter(this.isTelemetryObject);
composition.forEach(this.addTelemetryObject);
this.tableComposition.on('add', this.addTelemetryObject);
this.tableComposition.on('remove', this.removeTelemetryObject);
});
}
}
addTelemetryObject(telemetryObject) {
this.addColumnsForObject(telemetryObject, true);
const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let requestOptions = this.buildOptionsFromConfiguration(telemetryObject);
let columnMap = this.getColumnMapForObject(keyString);
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator);
const telemetryRemover = this.getTelemetryRemover();
this.removeTelemetryCollection(keyString);
this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection(
telemetryObject,
requestOptions
);
this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests);
this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests);
this.telemetryCollections[keyString].on('remove', telemetryRemover);
this.telemetryCollections[keyString].on('add', telemetryProcessor);
this.telemetryCollections[keyString].on('clear', this.clearData);
this.telemetryCollections[keyString].load();
this.subscribeToStaleness(telemetryObject);
this.telemetryObjects[keyString] = {
telemetryObject,
keyString,
requestOptions,
columnMap,
limitEvaluator
};
this.emit('object-added', telemetryObject);
}
resubscribeAllObjectsToStaleness() {
if (!this.subscribedStaleObjects || this.subscribedStaleObjects.size < 1) {
return;
}
for (const [, telemetryObject] of this.subscribedStaleObjects) {
this.subscribeToStaleness(telemetryObject);
}
}
subscribeToStaleness(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (this.stalenessSubscription?.[keyString]) {
this.unsubscribeFromStaleness(domainObject.identifier);
}
this.stalenessSubscription[keyString] = {};
this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils(
this.openmct,
domainObject
);
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(keyString, stalenessResponse);
}
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(
domainObject,
(stalenessResponse) => {
this.handleStaleness(keyString, stalenessResponse);
}
);
this.subscribedStaleObjects.set(keyString, domainObject);
this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription;
}
handleStaleness(keyString, stalenessResponse, skipCheck = false) {
if (
skipCheck ||
this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(
stalenessResponse,
keyString
)
) {
this.emit('telemetry-staleness', {
keyString,
stalenessResponse: stalenessResponse
});
}
}
getTelemetryProcessor(keyString, columnMap, limitEvaluator) {
return (telemetry) => {
//Check that telemetry object has not been removed since telemetry was requested.
if (!this.telemetryObjects[keyString]) {
return;
}
const metadataValue = this.openmct.telemetry
.getMetadata(this.telemetryObjects[keyString].telemetryObject)
.getUseToUpdateInPlaceValue();
let telemetryRows = telemetry.map(
(datum) =>
new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator, metadataValue?.key)
);
if (this.paused) {
this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));
} else {
this.tableRows.addRows(telemetryRows);
}
};
}
getTelemetryRemover() {
return (telemetry) => {
if (this.paused) {
this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry));
} else {
this.tableRows.removeRowsByData(telemetry);
}
};
}
/**
* @private
*/
incrementOutstandingRequests() {
if (this.outstandingRequests === 0) {
this.emit('outstanding-requests', true);
}
this.outstandingRequests++;
}
/**
* @private
*/
decrementOutstandingRequests() {
this.outstandingRequests--;
if (this.outstandingRequests === 0) {
this.emit('outstanding-requests', false);
}
}
// will pull all necessary information for all existing bounded telemetry
// and pass to table row collection to reset without making any new requests
// triggered by filtering
resetRowsFromAllData() {
let allRows = [];
Object.keys(this.telemetryCollections).forEach((keyString) => {
let { columnMap, limitEvaluator } = this.telemetryObjects[keyString];
const metadataValue = this.openmct.telemetry
.getMetadata(this.telemetryObjects[keyString].telemetryObject)
.getUseToUpdateInPlaceValue();
this.telemetryCollections[keyString].getAll().forEach((datum) => {
allRows.push(
new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator, metadataValue?.key)
);
});
});
this.tableRows.clearRowsFromTableAndFilter(allRows);
}
updateFilters(updatedFilters) {
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
this.filters = deepCopiedFilters;
this.tableRows.clear();
this.clearAndResubscribe();
} else {
this.filters = deepCopiedFilters;
}
}
clearAndResubscribe() {
let objectKeys = Object.keys(this.telemetryObjects);
this.tableRows.clear();
objectKeys.forEach((keyString) => {
this.addTelemetryObject(this.telemetryObjects[keyString].telemetryObject);
});
}
removeTelemetryObject(objectIdentifier) {
const keyString = this.openmct.objects.makeKeyString(objectIdentifier);
this.configuration.removeColumnsForObject(objectIdentifier, true);
this.tableRows.removeRowsByObject(keyString);
this.removeTelemetryCollection(keyString);
delete this.telemetryObjects[keyString];
this.emit('object-removed', objectIdentifier);
this.unsubscribeFromStaleness(objectIdentifier);
}
unsubscribeFromStaleness(objectIdentifier) {
const keyString = this.openmct.objects.makeKeyString(objectIdentifier);
const SKIP_CHECK = true;
this.stalenessSubscription[keyString].unsubscribe();
this.stalenessSubscription[keyString].stalenessUtils.destroy();
this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK);
delete this.stalenessSubscription[keyString];
}
clearData() {
this.tableRows.clear();
this.emit('refresh');
}
addColumnsForObject(telemetryObject) {
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValues = metadata.values();
this.addNameColumn(telemetryObject, metadataValues);
metadataValues.forEach((metadatum) => {
if (metadatum.key === 'name' || metadata.isInPlaceUpdateValue(metadatum)) {
return;
}
let column = this.createColumn(metadatum);
this.configuration.addSingleColumnForObject(telemetryObject, column);
// add units column if available
if (metadatum.unit !== undefined) {
let unitColumn = this.createUnitColumn(metadatum);
this.configuration.addSingleColumnForObject(telemetryObject, unitColumn);
}
});
}
getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns();
if (columns[objectKeyString]) {
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
return map;
}, {});
}
return {};
}
buildOptionsFromConfiguration(telemetryObject) {
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let filters =
this.domainObject.configuration &&
this.domainObject.configuration.filters &&
this.domainObject.configuration.filters[keyString];
return { filters } || {};
}
createColumn(metadatum) {
return new TelemetryTableColumn(this.openmct, metadatum);
}
createUnitColumn(metadatum) {
return new TelemetryTableUnitColumn(this.openmct, metadatum);
}
isTelemetryObject(domainObject) {
return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry');
}
sortBy(sortOptions) {
this.tableRows.sortBy(sortOptions);
if (this.openmct.editor.isEditing()) {
let configuration = this.configuration.getConfiguration();
configuration.sortOptions = sortOptions;
this.configuration.updateConfiguration(configuration);
}
}
runDelayedActions() {
this.delayedActions.forEach((action) => action());
this.delayedActions = [];
}
removeTelemetryCollection(keyString) {
if (this.telemetryCollections[keyString]) {
this.telemetryCollections[keyString].destroy();
this.telemetryCollections[keyString] = undefined;
delete this.telemetryCollections[keyString];
}
}
pause() {
this.paused = true;
}
unpause() {
this.paused = false;
this.runDelayedActions();
}
destroy() {
this.tableRows.destroy();
this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData);
this.openmct.time.off('clockChanged', this.resubscribeToStaleness);
let keystrings = Object.keys(this.telemetryCollections);
keystrings.forEach(this.removeTelemetryCollection);
if (this.filterObserver) {
this.filterObserver();
}
Object.values(this.stalenessSubscription).forEach((stalenessSubscription) => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
if (this.tableComposition !== undefined) {
this.tableComposition.off('add', this.addTelemetryObject);
this.tableComposition.off('remove', this.removeTelemetryObject);
}
}
}

View File

@ -19,48 +19,44 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(function () { export default class TelemetryTableColumn {
class TelemetryTableColumn { constructor(openmct, metadatum, options = { selectable: false }) {
constructor(openmct, metadatum, options = { selectable: false }) { this.metadatum = metadatum;
this.metadatum = metadatum; this.formatter = openmct.telemetry.getValueFormatter(metadatum);
this.formatter = openmct.telemetry.getValueFormatter(metadatum); this.titleValue = this.metadatum.name;
this.titleValue = this.metadatum.name; this.selectable = options.selectable;
this.selectable = options.selectable; }
}
getKey() { getKey() {
return this.metadatum.key; return this.metadatum.key;
} }
getTitle() { getTitle() {
return this.metadatum.name; return this.metadatum.name;
} }
getMetadatum() { getMetadatum() {
return this.metadatum; return this.metadatum;
} }
hasValueForDatum(telemetryDatum) { hasValueForDatum(telemetryDatum) {
return Object.prototype.hasOwnProperty.call(telemetryDatum, this.metadatum.source); return Object.prototype.hasOwnProperty.call(telemetryDatum, this.metadatum.source);
} }
getRawValue(telemetryDatum) { getRawValue(telemetryDatum) {
return telemetryDatum[this.metadatum.source]; return telemetryDatum[this.metadatum.source];
} }
getFormattedValue(telemetryDatum) { getFormattedValue(telemetryDatum) {
let formattedValue = this.formatter.format(telemetryDatum); let formattedValue = this.formatter.format(telemetryDatum);
if (formattedValue !== undefined && typeof formattedValue !== 'string') { if (formattedValue !== undefined && typeof formattedValue !== 'string') {
return formattedValue.toString(); return formattedValue.toString();
} else { } else {
return formattedValue; return formattedValue;
}
}
getParsedValue(telemetryDatum) {
return this.formatter.parse(telemetryDatum);
} }
} }
return TelemetryTableColumn; getParsedValue(telemetryDatum) {
}); return this.formatter.parse(telemetryDatum);
}
}

View File

@ -20,149 +20,148 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['lodash', 'EventEmitter'], function (_, EventEmitter) { import EventEmitter from 'EventEmitter';
class TelemetryTableConfiguration extends EventEmitter { import _ from 'lodash';
constructor(domainObject, openmct) {
super();
this.domainObject = domainObject; export default class TelemetryTableConfiguration extends EventEmitter {
this.openmct = openmct; constructor(domainObject, openmct) {
this.columns = {}; super();
this.removeColumnsForObject = this.removeColumnsForObject.bind(this); this.domainObject = domainObject;
this.objectMutated = this.objectMutated.bind(this); this.openmct = openmct;
this.columns = {};
this.unlistenFromMutation = openmct.objects.observe( this.removeColumnsForObject = this.removeColumnsForObject.bind(this);
domainObject, this.objectMutated = this.objectMutated.bind(this);
'configuration',
this.objectMutated
);
}
getConfiguration() { this.unlistenFromMutation = openmct.objects.observe(
let configuration = this.domainObject.configuration || {}; domainObject,
configuration.hiddenColumns = configuration.hiddenColumns || {}; 'configuration',
configuration.columnWidths = configuration.columnWidths || {}; this.objectMutated
configuration.columnOrder = configuration.columnOrder || []; );
configuration.cellFormat = configuration.cellFormat || {}; }
configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize;
return configuration; getConfiguration() {
} let configuration = this.domainObject.configuration || {};
configuration.hiddenColumns = configuration.hiddenColumns || {};
configuration.columnWidths = configuration.columnWidths || {};
configuration.columnOrder = configuration.columnOrder || [];
configuration.cellFormat = configuration.cellFormat || {};
configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize;
updateConfiguration(configuration) { return configuration;
this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); }
}
/** updateConfiguration(configuration) {
* @private this.openmct.objects.mutate(this.domainObject, 'configuration', configuration);
* @param {*} object }
*/
objectMutated(configuration) {
if (configuration !== undefined) {
this.emit('change', configuration);
}
}
addSingleColumnForObject(telemetryObject, column, position) { /**
let objectKeyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); * @private
this.columns[objectKeyString] = this.columns[objectKeyString] || []; * @param {*} object
position = position || this.columns[objectKeyString].length; */
this.columns[objectKeyString].splice(position, 0, column); objectMutated(configuration) {
} if (configuration !== undefined) {
this.emit('change', configuration);
removeColumnsForObject(objectIdentifier) {
let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier);
let columnsToRemove = this.columns[objectKeyString];
delete this.columns[objectKeyString];
let configuration = this.domainObject.configuration;
let configurationChanged = false;
columnsToRemove.forEach((column) => {
//There may be more than one column with the same key (eg. time system columns)
if (!this.hasColumnWithKey(column.getKey())) {
delete configuration.hiddenColumns[column.getKey()];
configurationChanged = true;
}
});
if (configurationChanged) {
this.updateConfiguration(configuration);
}
}
hasColumnWithKey(columnKey) {
return _.flatten(Object.values(this.columns)).some((column) => column.getKey() === columnKey);
}
getColumns() {
return this.columns;
}
getAllHeaders() {
let flattenedColumns = _.flatten(Object.values(this.columns));
/* eslint-disable you-dont-need-lodash-underscore/uniq */
let headers = _.uniq(flattenedColumns, false, (column) => column.getKey()).reduce(
fromColumnsToHeadersMap,
{}
);
/* eslint-enable you-dont-need-lodash-underscore/uniq */
function fromColumnsToHeadersMap(headersMap, column) {
headersMap[column.getKey()] = column.getTitle();
return headersMap;
}
return headers;
}
getVisibleHeaders() {
let allHeaders = this.getAllHeaders();
let configuration = this.getConfiguration();
let orderedColumns = this.getColumnOrder();
let unorderedColumns = _.difference(Object.keys(allHeaders), orderedColumns);
return orderedColumns
.concat(unorderedColumns)
.filter((headerKey) => {
return configuration.hiddenColumns[headerKey] !== true;
})
.reduce((headers, headerKey) => {
headers[headerKey] = allHeaders[headerKey];
return headers;
}, {});
}
getColumnWidths() {
let configuration = this.getConfiguration();
return configuration.columnWidths;
}
setColumnWidths(columnWidths) {
let configuration = this.getConfiguration();
configuration.columnWidths = columnWidths;
this.updateConfiguration(configuration);
}
getColumnOrder() {
let configuration = this.getConfiguration();
return configuration.columnOrder;
}
setColumnOrder(columnOrder) {
let configuration = this.getConfiguration();
configuration.columnOrder = columnOrder;
this.updateConfiguration(configuration);
}
destroy() {
this.unlistenFromMutation();
} }
} }
return TelemetryTableConfiguration; addSingleColumnForObject(telemetryObject, column, position) {
}); let objectKeyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.columns[objectKeyString] = this.columns[objectKeyString] || [];
position = position || this.columns[objectKeyString].length;
this.columns[objectKeyString].splice(position, 0, column);
}
removeColumnsForObject(objectIdentifier) {
let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier);
let columnsToRemove = this.columns[objectKeyString];
delete this.columns[objectKeyString];
let configuration = this.domainObject.configuration;
let configurationChanged = false;
columnsToRemove.forEach((column) => {
//There may be more than one column with the same key (eg. time system columns)
if (!this.hasColumnWithKey(column.getKey())) {
delete configuration.hiddenColumns[column.getKey()];
configurationChanged = true;
}
});
if (configurationChanged) {
this.updateConfiguration(configuration);
}
}
hasColumnWithKey(columnKey) {
return _.flatten(Object.values(this.columns)).some((column) => column.getKey() === columnKey);
}
getColumns() {
return this.columns;
}
getAllHeaders() {
let flattenedColumns = _.flatten(Object.values(this.columns));
/* eslint-disable you-dont-need-lodash-underscore/uniq */
let headers = _.uniq(flattenedColumns, false, (column) => column.getKey()).reduce(
fromColumnsToHeadersMap,
{}
);
/* eslint-enable you-dont-need-lodash-underscore/uniq */
function fromColumnsToHeadersMap(headersMap, column) {
headersMap[column.getKey()] = column.getTitle();
return headersMap;
}
return headers;
}
getVisibleHeaders() {
let allHeaders = this.getAllHeaders();
let configuration = this.getConfiguration();
let orderedColumns = this.getColumnOrder();
let unorderedColumns = _.difference(Object.keys(allHeaders), orderedColumns);
return orderedColumns
.concat(unorderedColumns)
.filter((headerKey) => {
return configuration.hiddenColumns[headerKey] !== true;
})
.reduce((headers, headerKey) => {
headers[headerKey] = allHeaders[headerKey];
return headers;
}, {});
}
getColumnWidths() {
let configuration = this.getConfiguration();
return configuration.columnWidths;
}
setColumnWidths(columnWidths) {
let configuration = this.getConfiguration();
configuration.columnWidths = columnWidths;
this.updateConfiguration(configuration);
}
getColumnOrder() {
let configuration = this.getConfiguration();
return configuration.columnOrder;
}
setColumnOrder(columnOrder) {
let configuration = this.getConfiguration();
configuration.columnOrder = columnOrder;
this.updateConfiguration(configuration);
}
destroy() {
this.unlistenFromMutation();
}
}

View File

@ -19,22 +19,21 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./TelemetryTableColumn.js'], function (TelemetryTableColumn) {
class TelemetryTableNameColumn extends TelemetryTableColumn {
constructor(openmct, telemetryObject, metadatum) {
super(openmct, metadatum);
this.telemetryObject = telemetryObject; import TelemetryTableColumn from './TelemetryTableColumn';
}
getRawValue() { export default class TelemetryTableNameColumn extends TelemetryTableColumn {
return this.telemetryObject.name; constructor(openmct, telemetryObject, metadatum) {
} super(openmct, metadatum);
getFormattedValue() { this.telemetryObject = telemetryObject;
return this.telemetryObject.name;
}
} }
return TelemetryTableNameColumn; getRawValue() {
}); return this.telemetryObject.name;
}
getFormattedValue() {
return this.telemetryObject.name;
}
}

View File

@ -20,108 +20,104 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { export default class TelemetryTableRow {
class TelemetryTableRow { constructor(datum, columns, objectKeyString, limitEvaluator, inPlaceUpdateKey) {
constructor(datum, columns, objectKeyString, limitEvaluator, inPlaceUpdateKey) { this.columns = columns;
this.columns = columns;
this.datum = createNormalizedDatum(datum, columns); this.datum = createNormalizedDatum(datum, columns);
this.fullDatum = datum; this.fullDatum = datum;
this.limitEvaluator = limitEvaluator; this.limitEvaluator = limitEvaluator;
this.objectKeyString = objectKeyString; this.objectKeyString = objectKeyString;
this.inPlaceUpdateKey = inPlaceUpdateKey; this.inPlaceUpdateKey = inPlaceUpdateKey;
}
getFormattedDatum(headers) {
return Object.keys(headers).reduce((formattedDatum, columnKey) => {
formattedDatum[columnKey] = this.getFormattedValue(columnKey);
return formattedDatum;
}, {});
}
getFormattedValue(key) {
let column = this.columns[key];
return column && column.getFormattedValue(this.datum[key]);
}
getParsedValue(key) {
let column = this.columns[key];
return column && column.getParsedValue(this.datum[key]);
}
getCellComponentName(key) {
let column = this.columns[key];
return column && column.getCellComponentName && column.getCellComponentName();
}
getRowClass() {
if (!this.rowClass) {
let limitEvaluation = this.limitEvaluator.evaluate(this.datum);
this.rowClass = limitEvaluation && limitEvaluation.cssClass;
} }
getFormattedDatum(headers) { return this.rowClass;
return Object.keys(headers).reduce((formattedDatum, columnKey) => { }
formattedDatum[columnKey] = this.getFormattedValue(columnKey);
return formattedDatum; getCellLimitClasses() {
if (!this.cellLimitClasses) {
this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => {
if (!column.isUnit) {
let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum());
alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass;
}
return alarmStateMap;
}, {}); }, {});
} }
getFormattedValue(key) { return this.cellLimitClasses;
let column = this.columns[key];
return column && column.getFormattedValue(this.datum[key]);
}
getParsedValue(key) {
let column = this.columns[key];
return column && column.getParsedValue(this.datum[key]);
}
getCellComponentName(key) {
let column = this.columns[key];
return column && column.getCellComponentName && column.getCellComponentName();
}
getRowClass() {
if (!this.rowClass) {
let limitEvaluation = this.limitEvaluator.evaluate(this.datum);
this.rowClass = limitEvaluation && limitEvaluation.cssClass;
}
return this.rowClass;
}
getCellLimitClasses() {
if (!this.cellLimitClasses) {
this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => {
if (!column.isUnit) {
let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum());
alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass;
}
return alarmStateMap;
}, {});
}
return this.cellLimitClasses;
}
getContextualDomainObject(openmct, objectKeyString) {
return openmct.objects.get(objectKeyString);
}
getContextMenuActions() {
return ['viewDatumAction', 'viewHistoricalData'];
}
updateWithDatum(updatesToDatum) {
const normalizedUpdatesToDatum = createNormalizedDatum(updatesToDatum, this.columns);
this.datum = {
...this.datum,
...normalizedUpdatesToDatum
};
this.fullDatum = {
...this.fullDatum,
...updatesToDatum
};
}
} }
/** getContextualDomainObject(openmct, objectKeyString) {
* Normalize the structure of datums to assist sorting and merging of columns. return openmct.objects.get(objectKeyString);
* Maps all sources to keys.
* @private
* @param {*} telemetryDatum
* @param {*} metadataValues
*/
function createNormalizedDatum(datum, columns) {
const normalizedDatum = JSON.parse(JSON.stringify(datum));
Object.values(columns).forEach((column) => {
const rawValue = column.getRawValue(datum);
if (rawValue !== undefined) {
normalizedDatum[column.getKey()] = rawValue;
}
});
return normalizedDatum;
} }
return TelemetryTableRow; getContextMenuActions() {
}); return ['viewDatumAction', 'viewHistoricalData'];
}
updateWithDatum(updatesToDatum) {
const normalizedUpdatesToDatum = createNormalizedDatum(updatesToDatum, this.columns);
this.datum = {
...this.datum,
...normalizedUpdatesToDatum
};
this.fullDatum = {
...this.fullDatum,
...updatesToDatum
};
}
}
/**
* Normalize the structure of datums to assist sorting and merging of columns.
* Maps all sources to keys.
* @private
* @param {*} telemetryDatum
* @param {*} metadataValues
*/
function createNormalizedDatum(datum, columns) {
const normalizedDatum = JSON.parse(JSON.stringify(datum));
Object.values(columns).forEach((column) => {
const rawValue = column.getRawValue(datum);
if (rawValue !== undefined) {
normalizedDatum[column.getKey()] = rawValue;
}
});
return normalizedDatum;
}

View File

@ -20,19 +20,17 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(function () { export default {
return { name: 'Telemetry Table',
name: 'Telemetry Table', description:
description: 'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.',
'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.', creatable: true,
creatable: true, cssClass: 'icon-tabular-scrolling',
cssClass: 'icon-tabular-scrolling', initialize(domainObject) {
initialize(domainObject) { domainObject.composition = [];
domainObject.composition = []; domainObject.configuration = {
domainObject.configuration = { columnWidths: {},
columnWidths: {}, hiddenColumns: {}
hiddenColumns: {} };
}; }
} };
};
});

View File

@ -19,38 +19,38 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./TelemetryTableColumn.js'], function (TelemetryTableColumn) { import TelemetryTableColumn from './TelemetryTableColumn.js';
class TelemetryTableUnitColumn extends TelemetryTableColumn {
constructor(openmct, metadatum) {
super(openmct, metadatum);
this.isUnit = true;
this.titleValue += ' Unit';
this.formatter = {
format: (datum) => {
return this.metadatum.unit;
},
parse: (datum) => {
return this.metadatum.unit;
}
};
}
getKey() { class TelemetryTableUnitColumn extends TelemetryTableColumn {
return this.metadatum.key + '-unit'; constructor(openmct, metadatum) {
} super(openmct, metadatum);
this.isUnit = true;
getTitle() { this.titleValue += ' Unit';
return this.metadatum.name + ' Unit'; this.formatter = {
} format: (datum) => {
return this.metadatum.unit;
getRawValue(telemetryDatum) { },
return this.metadatum.unit; parse: (datum) => {
} return this.metadatum.unit;
}
getFormattedValue(telemetryDatum) { };
return this.formatter.format(telemetryDatum);
}
} }
return TelemetryTableUnitColumn; getKey() {
}); return this.metadatum.key + '-unit';
}
getTitle() {
return this.metadatum.name + ' Unit';
}
getRawValue(telemetryDatum) {
return this.metadatum.unit;
}
getFormattedValue(telemetryDatum) {
return this.formatter.format(telemetryDatum);
}
}
export default TelemetryTableUnitColumn;

View File

@ -19,371 +19,369 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import EventEmitter from 'EventEmitter';
import _ from 'lodash';
define(['lodash', 'EventEmitter'], function (_, EventEmitter) { /**
/** * @constructor
* @constructor */
*/ export default class TableRowCollection extends EventEmitter {
class TableRowCollection extends EventEmitter { constructor() {
constructor() { super();
super();
this.rows = []; this.rows = [];
this.columnFilters = {}; this.columnFilters = {};
this.addRows = this.addRows.bind(this); this.addRows = this.addRows.bind(this);
this.removeRowsByObject = this.removeRowsByObject.bind(this); this.removeRowsByObject = this.removeRowsByObject.bind(this);
this.removeRowsByData = this.removeRowsByData.bind(this); this.removeRowsByData = this.removeRowsByData.bind(this);
this.clear = this.clear.bind(this); this.clear = this.clear.bind(this);
} }
removeRowsByObject(keyString) { removeRowsByObject(keyString) {
let removed = []; let removed = [];
this.rows = this.rows.filter((row) => { this.rows = this.rows.filter((row) => {
if (row.objectKeyString === keyString) { if (row.objectKeyString === keyString) {
removed.push(row); removed.push(row);
return false;
} else {
return true;
}
});
this.emit('remove', removed);
}
addRows(rows) {
let rowsToAdd = this.filterRows(rows);
this.sortAndMergeRows(rowsToAdd);
// we emit filter no matter what to trigger
// an update of visible rows
if (rowsToAdd.length > 0) {
this.emit('add', rowsToAdd);
}
}
clearRowsFromTableAndFilter(rows) {
let rowsToAdd = this.filterRows(rows);
// Reset of all rows, need to wipe current rows
this.rows = [];
this.sortAndMergeRows(rowsToAdd);
// We emit filter and update of visible rows
this.emit('filter', rowsToAdd);
}
filterRows(rows) {
if (Object.keys(this.columnFilters).length > 0) {
return rows.filter(this.matchesFilters, this);
}
return rows;
}
sortAndMergeRows(rows) {
const sortedRows = this.sortCollection(rows);
if (this.rows.length === 0) {
this.rows = sortedRows;
return;
}
const firstIncomingRow = sortedRows[0];
const lastIncomingRow = sortedRows[sortedRows.length - 1];
const firstExistingRow = this.rows[0];
const lastExistingRow = this.rows[this.rows.length - 1];
if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow) === lastIncomingRow) {
this.insertOrUpdateRows(sortedRows, true);
} else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow) === lastExistingRow) {
this.insertOrUpdateRows(sortedRows, false);
} else {
this.mergeSortedRows(sortedRows);
}
}
getInPlaceUpdateIndex(row) {
const inPlaceUpdateKey = row.inPlaceUpdateKey;
if (!inPlaceUpdateKey) {
return -1;
}
const foundIndex = this.rows.findIndex(
(existingRow) =>
existingRow.datum[inPlaceUpdateKey] &&
existingRow.datum[inPlaceUpdateKey] === row.datum[inPlaceUpdateKey]
);
return foundIndex;
}
updateRowInPlace(row, index) {
const foundRow = this.rows[index];
foundRow.updateWithDatum(row.datum);
this.rows[index] = foundRow;
}
sortCollection(rows) {
const sortedRows = _.orderBy(
rows,
(row) => row.getParsedValue(this.sortOptions.key),
this.sortOptions.direction
);
return sortedRows;
}
insertOrUpdateRows(rowsToAdd, addToBeginning) {
rowsToAdd.forEach((row) => {
const index = this.getInPlaceUpdateIndex(row);
if (index > -1) {
this.updateRowInPlace(row, index);
} else {
if (addToBeginning) {
this.rows.unshift(row);
} else {
this.rows.push(row);
}
}
});
}
mergeSortedRows(incomingRows) {
const mergedRows = [];
let existingRowIndex = 0;
let incomingRowIndex = 0;
while (existingRowIndex < this.rows.length && incomingRowIndex < incomingRows.length) {
const existingRow = this.rows[existingRowIndex];
const incomingRow = incomingRows[incomingRowIndex];
const inPlaceIndex = this.getInPlaceUpdateIndex(incomingRow);
if (inPlaceIndex > -1) {
this.updateRowInPlace(incomingRow, inPlaceIndex);
incomingRowIndex++;
} else {
if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) {
mergedRows.push(existingRow);
existingRowIndex++;
} else {
mergedRows.push(incomingRow);
incomingRowIndex++;
}
}
}
// tail of existing rows is all that is left to merge
if (existingRowIndex < this.rows.length) {
for (existingRowIndex; existingRowIndex < this.rows.length; existingRowIndex++) {
mergedRows.push(this.rows[existingRowIndex]);
}
}
// tail of incoming rows is all that is left to merge
if (incomingRowIndex < incomingRows.length) {
for (incomingRowIndex; incomingRowIndex < incomingRows.length; incomingRowIndex++) {
mergedRows.push(incomingRows[incomingRowIndex]);
}
}
this.rows = mergedRows;
}
firstRowInSortOrder(row1, row2) {
const val1 = this.getValueForSortColumn(row1);
const val2 = this.getValueForSortColumn(row2);
if (this.sortOptions.direction === 'asc') {
return val1 <= val2 ? row1 : row2;
} else {
return val1 >= val2 ? row1 : row2;
}
}
removeRowsByData(data) {
let removed = [];
this.rows = this.rows.filter((row) => {
if (data.includes(row.fullDatum)) {
removed.push(row);
return false;
} else {
return true;
}
});
this.emit('remove', removed);
}
/**
* Sorts the telemetry collection based on the provided sort field
* specifier. Subsequent inserts are sorted to maintain specified sport
* order.
*
* @example
* // First build some mock telemetry for the purpose of an example
* let now = Date.now();
* let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) {
* return {
* // define an object property to demonstrate nested paths
* timestamp: {
* ms: now - value * 1000,
* text:
* },
* value: value
* }
* });
* let collection = new TelemetryCollection();
*
* collection.add(telemetry);
*
* // Sort by telemetry value
* collection.sortBy({
* key: 'value', direction: 'asc'
* });
*
* // Sort by ms since epoch
* collection.sort({
* key: 'timestamp.ms',
* direction: 'asc'
* });
*
* // Sort by 'text' attribute, descending
* collection.sort("timestamp.text");
*
*
* @param {object} sortOptions An object specifying a sort key, and direction.
*/
sortBy(sortOptions) {
if (arguments.length > 0) {
this.sortOptions = sortOptions;
this.rows = _.orderBy(
this.rows,
(row) => row.getParsedValue(sortOptions.key),
sortOptions.direction
);
this.emit('sort');
}
// Return duplicate to avoid direct modification of underlying object
return Object.assign({}, this.sortOptions);
}
setColumnFilter(columnKey, filter) {
filter = filter.trim().toLowerCase();
let wasBlank = this.columnFilters[columnKey] === undefined;
let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter);
if (filter.length === 0) {
delete this.columnFilters[columnKey];
} else {
this.columnFilters[columnKey] = filter;
}
if (isSubset || wasBlank) {
this.rows = this.rows.filter(this.matchesFilters, this);
this.emit('filter');
} else {
this.emit('resetRowsFromAllData');
}
}
setColumnRegexFilter(columnKey, filter) {
filter = filter.trim();
this.columnFilters[columnKey] = new RegExp(filter);
this.emit('resetRowsFromAllData');
}
getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns();
if (columns[objectKeyString]) {
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
return map;
}, {});
}
return {};
}
// /**
// * @private
// */
isSubsetOfCurrentFilter(columnKey, filter) {
if (this.columnFilters[columnKey] instanceof RegExp) {
return false; return false;
} else {
return true;
} }
});
return ( this.emit('remove', removed);
this.columnFilters[columnKey] && }
filter.startsWith(this.columnFilters[columnKey]) &&
// startsWith check will otherwise fail when filter cleared
// because anyString.startsWith('') === true
filter !== ''
);
}
/** addRows(rows) {
* @private let rowsToAdd = this.filterRows(rows);
*/
matchesFilters(row) {
let doesMatchFilters = true;
Object.keys(this.columnFilters).forEach((key) => {
if (!doesMatchFilters || !this.rowHasColumn(row, key)) {
return false;
}
let formattedValue = row.getFormattedValue(key); this.sortAndMergeRows(rowsToAdd);
if (formattedValue === undefined) {
return false;
}
if (this.columnFilters[key] instanceof RegExp) { // we emit filter no matter what to trigger
doesMatchFilters = this.columnFilters[key].test(formattedValue); // an update of visible rows
} else { if (rowsToAdd.length > 0) {
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1; this.emit('add', rowsToAdd);
}
});
return doesMatchFilters;
}
rowHasColumn(row, key) {
return Object.prototype.hasOwnProperty.call(row.columns, key);
}
getRows() {
return this.rows;
}
getRowsLength() {
return this.rows.length;
}
getValueForSortColumn(row) {
return row.getParsedValue(this.sortOptions.key);
}
clear() {
let removedRows = this.rows;
this.rows = [];
this.emit('remove', removedRows);
}
destroy() {
this.removeAllListeners();
} }
} }
return TableRowCollection; clearRowsFromTableAndFilter(rows) {
}); let rowsToAdd = this.filterRows(rows);
// Reset of all rows, need to wipe current rows
this.rows = [];
this.sortAndMergeRows(rowsToAdd);
// We emit filter and update of visible rows
this.emit('filter', rowsToAdd);
}
filterRows(rows) {
if (Object.keys(this.columnFilters).length > 0) {
return rows.filter(this.matchesFilters, this);
}
return rows;
}
sortAndMergeRows(rows) {
const sortedRows = this.sortCollection(rows);
if (this.rows.length === 0) {
this.rows = sortedRows;
return;
}
const firstIncomingRow = sortedRows[0];
const lastIncomingRow = sortedRows[sortedRows.length - 1];
const firstExistingRow = this.rows[0];
const lastExistingRow = this.rows[this.rows.length - 1];
if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow) === lastIncomingRow) {
this.insertOrUpdateRows(sortedRows, true);
} else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow) === lastExistingRow) {
this.insertOrUpdateRows(sortedRows, false);
} else {
this.mergeSortedRows(sortedRows);
}
}
getInPlaceUpdateIndex(row) {
const inPlaceUpdateKey = row.inPlaceUpdateKey;
if (!inPlaceUpdateKey) {
return -1;
}
const foundIndex = this.rows.findIndex(
(existingRow) =>
existingRow.datum[inPlaceUpdateKey] &&
existingRow.datum[inPlaceUpdateKey] === row.datum[inPlaceUpdateKey]
);
return foundIndex;
}
updateRowInPlace(row, index) {
const foundRow = this.rows[index];
foundRow.updateWithDatum(row.datum);
this.rows[index] = foundRow;
}
sortCollection(rows) {
const sortedRows = _.orderBy(
rows,
(row) => row.getParsedValue(this.sortOptions.key),
this.sortOptions.direction
);
return sortedRows;
}
insertOrUpdateRows(rowsToAdd, addToBeginning) {
rowsToAdd.forEach((row) => {
const index = this.getInPlaceUpdateIndex(row);
if (index > -1) {
this.updateRowInPlace(row, index);
} else {
if (addToBeginning) {
this.rows.unshift(row);
} else {
this.rows.push(row);
}
}
});
}
mergeSortedRows(incomingRows) {
const mergedRows = [];
let existingRowIndex = 0;
let incomingRowIndex = 0;
while (existingRowIndex < this.rows.length && incomingRowIndex < incomingRows.length) {
const existingRow = this.rows[existingRowIndex];
const incomingRow = incomingRows[incomingRowIndex];
const inPlaceIndex = this.getInPlaceUpdateIndex(incomingRow);
if (inPlaceIndex > -1) {
this.updateRowInPlace(incomingRow, inPlaceIndex);
incomingRowIndex++;
} else {
if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) {
mergedRows.push(existingRow);
existingRowIndex++;
} else {
mergedRows.push(incomingRow);
incomingRowIndex++;
}
}
}
// tail of existing rows is all that is left to merge
if (existingRowIndex < this.rows.length) {
for (existingRowIndex; existingRowIndex < this.rows.length; existingRowIndex++) {
mergedRows.push(this.rows[existingRowIndex]);
}
}
// tail of incoming rows is all that is left to merge
if (incomingRowIndex < incomingRows.length) {
for (incomingRowIndex; incomingRowIndex < incomingRows.length; incomingRowIndex++) {
mergedRows.push(incomingRows[incomingRowIndex]);
}
}
this.rows = mergedRows;
}
firstRowInSortOrder(row1, row2) {
const val1 = this.getValueForSortColumn(row1);
const val2 = this.getValueForSortColumn(row2);
if (this.sortOptions.direction === 'asc') {
return val1 <= val2 ? row1 : row2;
} else {
return val1 >= val2 ? row1 : row2;
}
}
removeRowsByData(data) {
let removed = [];
this.rows = this.rows.filter((row) => {
if (data.includes(row.fullDatum)) {
removed.push(row);
return false;
} else {
return true;
}
});
this.emit('remove', removed);
}
/**
* Sorts the telemetry collection based on the provided sort field
* specifier. Subsequent inserts are sorted to maintain specified sport
* order.
*
* @example
* // First build some mock telemetry for the purpose of an example
* let now = Date.now();
* let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) {
* return {
* // define an object property to demonstrate nested paths
* timestamp: {
* ms: now - value * 1000,
* text:
* },
* value: value
* }
* });
* let collection = new TelemetryCollection();
*
* collection.add(telemetry);
*
* // Sort by telemetry value
* collection.sortBy({
* key: 'value', direction: 'asc'
* });
*
* // Sort by ms since epoch
* collection.sort({
* key: 'timestamp.ms',
* direction: 'asc'
* });
*
* // Sort by 'text' attribute, descending
* collection.sort("timestamp.text");
*
*
* @param {object} sortOptions An object specifying a sort key, and direction.
*/
sortBy(sortOptions) {
if (arguments.length > 0) {
this.sortOptions = sortOptions;
this.rows = _.orderBy(
this.rows,
(row) => row.getParsedValue(sortOptions.key),
sortOptions.direction
);
this.emit('sort');
}
// Return duplicate to avoid direct modification of underlying object
return Object.assign({}, this.sortOptions);
}
setColumnFilter(columnKey, filter) {
filter = filter.trim().toLowerCase();
let wasBlank = this.columnFilters[columnKey] === undefined;
let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter);
if (filter.length === 0) {
delete this.columnFilters[columnKey];
} else {
this.columnFilters[columnKey] = filter;
}
if (isSubset || wasBlank) {
this.rows = this.rows.filter(this.matchesFilters, this);
this.emit('filter');
} else {
this.emit('resetRowsFromAllData');
}
}
setColumnRegexFilter(columnKey, filter) {
filter = filter.trim();
this.columnFilters[columnKey] = new RegExp(filter);
this.emit('resetRowsFromAllData');
}
getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns();
if (columns[objectKeyString]) {
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
return map;
}, {});
}
return {};
}
// /**
// * @private
// */
isSubsetOfCurrentFilter(columnKey, filter) {
if (this.columnFilters[columnKey] instanceof RegExp) {
return false;
}
return (
this.columnFilters[columnKey] &&
filter.startsWith(this.columnFilters[columnKey]) &&
// startsWith check will otherwise fail when filter cleared
// because anyString.startsWith('') === true
filter !== ''
);
}
/**
* @private
*/
matchesFilters(row) {
let doesMatchFilters = true;
Object.keys(this.columnFilters).forEach((key) => {
if (!doesMatchFilters || !this.rowHasColumn(row, key)) {
return false;
}
let formattedValue = row.getFormattedValue(key);
if (formattedValue === undefined) {
return false;
}
if (this.columnFilters[key] instanceof RegExp) {
doesMatchFilters = this.columnFilters[key].test(formattedValue);
} else {
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
}
});
return doesMatchFilters;
}
rowHasColumn(row, key) {
return Object.prototype.hasOwnProperty.call(row.columns, key);
}
getRows() {
return this.rows;
}
getRowsLength() {
return this.rows.length;
}
getValueForSortColumn(row) {
return row.getParsedValue(this.sortOptions.key);
}
clear() {
let removedRows = this.rows;
this.rows = [];
this.emit('remove', removedRows);
}
destroy() {
this.removeAllListeners();
}
}

View File

@ -20,17 +20,17 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { /**
* This time system supports UTC dates.
* @implements TimeSystem
* @constructor
*/
class UTCTimeSystem {
/** /**
* This time system supports UTC dates. * Metadata used to identify the time system in
* @implements TimeSystem * the UI
* @constructor
*/ */
function UTCTimeSystem() { constructor() {
/**
* Metadata used to identify the time system in
* the UI
*/
this.key = 'utc'; this.key = 'utc';
this.name = 'UTC'; this.name = 'UTC';
this.cssClass = 'icon-clock'; this.cssClass = 'icon-clock';
@ -38,6 +38,6 @@ define([], function () {
this.durationFormat = 'duration'; this.durationFormat = 'duration';
this.isUTCBased = true; this.isUTCBased = true;
} }
}
return UTCTimeSystem; export default UTCTimeSystem;
});

View File

@ -20,102 +20,98 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], function () { /**
/** * A ToolbarRegistry maintains the definitions for toolbars.
* A ToolbarRegistry maintains the definitions for toolbars. *
* * @interface ToolbarRegistry
* @interface ToolbarRegistry * @memberof module:openmct
* @memberof module:openmct */
*/ export default function ToolbarRegistry() {
function ToolbarRegistry() { this.providers = {};
this.providers = {}; }
/**
* Gets toolbar controls from providers which can provide a toolbar for this selection.
*
* @param {object} selection the selection object
* @returns {Object[]} an array of objects defining controls for the toolbar
* @private for platform-internal use
*/
ToolbarRegistry.prototype.get = function (selection) {
const providers = this.getAllProviders().filter(function (provider) {
return provider.forSelection(selection);
});
const structure = [];
providers.forEach((provider) => {
provider.toolbar(selection).forEach((item) => structure.push(item));
});
return structure;
};
/**
* @private
*/
ToolbarRegistry.prototype.getAllProviders = function () {
return Object.values(this.providers);
};
/**
* @private
*/
ToolbarRegistry.prototype.getByProviderKey = function (key) {
return this.providers[key];
};
/**
* Registers a new type of toolbar.
*
* @param {module:openmct.ToolbarRegistry} provider the provider for this toolbar
* @method addProvider
* @memberof module:openmct.ToolbarRegistry#
*/
ToolbarRegistry.prototype.addProvider = function (provider) {
const key = provider.key;
if (key === undefined) {
throw "Toolbar providers must have a unique 'key' property defined.";
} }
/** if (this.providers[key] !== undefined) {
* Gets toolbar controls from providers which can provide a toolbar for this selection. console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key);
* }
* @param {object} selection the selection object
* @returns {Object[]} an array of objects defining controls for the toolbar
* @private for platform-internal use
*/
ToolbarRegistry.prototype.get = function (selection) {
const providers = this.getAllProviders().filter(function (provider) {
return provider.forSelection(selection);
});
const structure = []; this.providers[key] = provider;
};
providers.forEach((provider) => { /**
provider.toolbar(selection).forEach((item) => structure.push(item)); * Exposes types of toolbars in Open MCT.
}); *
* @interface ToolbarProvider
* @property {string} key a unique identifier for this toolbar
* @property {string} name the human-readable name of this toolbar
* @property {string} [description] a longer-form description (typically
* a single sentence or short paragraph) of this kind of toolbar
* @memberof module:openmct
*/
return structure; /**
}; * Checks if this provider can supply toolbar for a selection.
*
* @method forSelection
* @memberof module:openmct.ToolbarProvider#
* @param {module:openmct.selection} selection
* @returns {boolean} 'true' if the toolbar applies to the provided selection,
* otherwise 'false'.
*/
/** /**
* @private * Provides controls that comprise a toolbar.
*/ *
ToolbarRegistry.prototype.getAllProviders = function () { * @method toolbar
return Object.values(this.providers); * @memberof module:openmct.ToolbarProvider#
}; * @param {object} selection the selection object
* @returns {Object[]} an array of objects defining controls for the toolbar.
/** */
* @private
*/
ToolbarRegistry.prototype.getByProviderKey = function (key) {
return this.providers[key];
};
/**
* Registers a new type of toolbar.
*
* @param {module:openmct.ToolbarRegistry} provider the provider for this toolbar
* @method addProvider
* @memberof module:openmct.ToolbarRegistry#
*/
ToolbarRegistry.prototype.addProvider = function (provider) {
const key = provider.key;
if (key === undefined) {
throw "Toolbar providers must have a unique 'key' property defined.";
}
if (this.providers[key] !== undefined) {
console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key);
}
this.providers[key] = provider;
};
/**
* Exposes types of toolbars in Open MCT.
*
* @interface ToolbarProvider
* @property {string} key a unique identifier for this toolbar
* @property {string} name the human-readable name of this toolbar
* @property {string} [description] a longer-form description (typically
* a single sentence or short paragraph) of this kind of toolbar
* @memberof module:openmct
*/
/**
* Checks if this provider can supply toolbar for a selection.
*
* @method forSelection
* @memberof module:openmct.ToolbarProvider#
* @param {module:openmct.selection} selection
* @returns {boolean} 'true' if the toolbar applies to the provided selection,
* otherwise 'false'.
*/
/**
* Provides controls that comprise a toolbar.
*
* @method toolbar
* @memberof module:openmct.ToolbarProvider#
* @param {object} selection the selection object
* @returns {Object[]} an array of objects defining controls for the toolbar.
*/
return ToolbarRegistry;
});

View File

@ -20,255 +20,253 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['EventEmitter'], function (EventEmitter) { import EventEmitter from 'EventEmitter';
const DEFAULT_VIEW_PRIORITY = 100;
/** const DEFAULT_VIEW_PRIORITY = 100;
* A ViewRegistry maintains the definitions for different kinds of views
* that may occur in different places in the user interface. /**
* @interface ViewRegistry * A ViewRegistry maintains the definitions for different kinds of views
* @memberof module:openmct * that may occur in different places in the user interface.
*/ * @interface ViewRegistry
function ViewRegistry() { * @memberof module:openmct
EventEmitter.apply(this); */
this.providers = {}; export default function ViewRegistry() {
EventEmitter.apply(this);
this.providers = {};
}
ViewRegistry.prototype = Object.create(EventEmitter.prototype);
/**
* @private for platform-internal use
* @param {*} item the object to be viewed
* @param {array} objectPath - The current contextual object path of the view object
* eg current domainObject is located under MyItems which is under Root
* @returns {module:openmct.ViewProvider[]} any providers
* which can provide views of this object
*/
ViewRegistry.prototype.get = function (item, objectPath) {
if (objectPath === undefined) {
throw 'objectPath must be provided to get applicable views for an object';
} }
ViewRegistry.prototype = Object.create(EventEmitter.prototype); function byPriority(providerA, providerB) {
let priorityA = providerA.priority ? providerA.priority(item) : DEFAULT_VIEW_PRIORITY;
let priorityB = providerB.priority ? providerB.priority(item) : DEFAULT_VIEW_PRIORITY;
/** return priorityB - priorityA;
* @private for platform-internal use }
* @param {*} item the object to be viewed
* @param {array} objectPath - The current contextual object path of the view object
* eg current domainObject is located under MyItems which is under Root
* @returns {module:openmct.ViewProvider[]} any providers
* which can provide views of this object
*/
ViewRegistry.prototype.get = function (item, objectPath) {
if (objectPath === undefined) {
throw 'objectPath must be provided to get applicable views for an object';
}
function byPriority(providerA, providerB) { return this.getAllProviders()
let priorityA = providerA.priority ? providerA.priority(item) : DEFAULT_VIEW_PRIORITY; .filter(function (provider) {
let priorityB = providerB.priority ? providerB.priority(item) : DEFAULT_VIEW_PRIORITY; return provider.canView(item, objectPath);
})
.sort(byPriority);
};
return priorityB - priorityA; /**
} * @private
*/
ViewRegistry.prototype.getAllProviders = function () {
return Object.values(this.providers);
};
return this.getAllProviders() /**
.filter(function (provider) { * Register a new type of view.
return provider.canView(item, objectPath); *
}) * @param {module:openmct.ViewProvider} provider the provider for this view
.sort(byPriority); * @method addProvider
}; * @memberof module:openmct.ViewRegistry#
*/
ViewRegistry.prototype.addProvider = function (provider) {
const key = provider.key;
if (key === undefined) {
throw "View providers must have a unique 'key' property defined";
}
/** if (this.providers[key] !== undefined) {
* @private console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key);
*/ }
ViewRegistry.prototype.getAllProviders = function () {
return Object.values(this.providers);
};
/** const wrappedView = provider.view.bind(provider);
* Register a new type of view. provider.view = (domainObject, objectPath) => {
* const viewObject = wrappedView(domainObject, objectPath);
* @param {module:openmct.ViewProvider} provider the provider for this view const wrappedShow = viewObject.show.bind(viewObject);
* @method addProvider viewObject.key = key; // provide access to provider key on view object
* @memberof module:openmct.ViewRegistry# viewObject.show = (element, isEditing, viewOptions) => {
*/ viewObject.parentElement = element.parentElement;
ViewRegistry.prototype.addProvider = function (provider) { wrappedShow(element, isEditing, viewOptions);
const key = provider.key;
if (key === undefined) {
throw "View providers must have a unique 'key' property defined";
}
if (this.providers[key] !== undefined) {
console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key);
}
const wrappedView = provider.view.bind(provider);
provider.view = (domainObject, objectPath) => {
const viewObject = wrappedView(domainObject, objectPath);
const wrappedShow = viewObject.show.bind(viewObject);
viewObject.key = key; // provide access to provider key on view object
viewObject.show = (element, isEditing, viewOptions) => {
viewObject.parentElement = element.parentElement;
wrappedShow(element, isEditing, viewOptions);
};
return viewObject;
}; };
this.providers[key] = provider; return viewObject;
}; };
/** this.providers[key] = provider;
* @private };
*/
ViewRegistry.prototype.getByProviderKey = function (key) {
return this.providers[key];
};
/** /**
* Used internally to support seamless usage of new views with old * @private
* views. */
* @private ViewRegistry.prototype.getByProviderKey = function (key) {
*/ return this.providers[key];
ViewRegistry.prototype.getByVPID = function (vpid) { };
return this.providers.filter(function (p) {
return p.vpid === vpid;
})[0];
};
/** /**
* A View is used to provide displayable content, and to react to * Used internally to support seamless usage of new views with old
* associated life cycle events. * views.
* * @private
* @name View */
* @interface ViewRegistry.prototype.getByVPID = function (vpid) {
* @memberof module:openmct return this.providers.filter(function (p) {
*/ return p.vpid === vpid;
})[0];
};
/** /**
* Populate the supplied DOM element with the contents of this view. * A View is used to provide displayable content, and to react to
* * associated life cycle events.
* View implementations should use this method to attach any *
* listeners or acquire other resources that are necessary to keep * @name View
* the contents of this view up-to-date. * @interface
* * @memberof module:openmct
* @param {HTMLElement} container the DOM element to populate */
* @method show
* @memberof module:openmct.View#
*/
/** /**
* Indicates whether or not the application is in edit mode. This supports * Populate the supplied DOM element with the contents of this view.
* views that have distinct visual and behavioral elements when the *
* navigated object is being edited. * View implementations should use this method to attach any
* * listeners or acquire other resources that are necessary to keep
* For cases where a completely separate view is desired for editing purposes, * the contents of this view up-to-date.
* see {@link openmct.ViewProvider#edit} *
* * @param {HTMLElement} container the DOM element to populate
* @param {boolean} isEditing * @method show
* @method show * @memberof module:openmct.View#
* @memberof module:openmct.View# */
*/
/** /**
* Release any resources associated with this view. * Indicates whether or not the application is in edit mode. This supports
* * views that have distinct visual and behavioral elements when the
* View implementations should use this method to detach any * navigated object is being edited.
* listeners or release other resources that are no longer necessary *
* once a view is no longer used. * For cases where a completely separate view is desired for editing purposes,
* * see {@link openmct.ViewProvider#edit}
* @method destroy *
* @memberof module:openmct.View# * @param {boolean} isEditing
*/ * @method show
* @memberof module:openmct.View#
*/
/** /**
* Returns the selection context. * Release any resources associated with this view.
* *
* View implementations should use this method to customize * View implementations should use this method to detach any
* the selection context. * listeners or release other resources that are no longer necessary
* * once a view is no longer used.
* @method getSelectionContext *
* @memberof module:openmct.View# * @method destroy
*/ * @memberof module:openmct.View#
*/
/** /**
* Exposes types of views in Open MCT. * Returns the selection context.
* *
* @interface ViewProvider * View implementations should use this method to customize
* @property {string} key a unique identifier for this view * the selection context.
* @property {string} name the human-readable name of this view *
* @property {string} [description] a longer-form description (typically * @method getSelectionContext
* a single sentence or short paragraph) of this kind of view * @memberof module:openmct.View#
* @property {string} [cssClass] the CSS class to apply to labels for this */
* view (to add icons, for instance)
* @memberof module:openmct
*/
/** /**
* Check if this provider can supply views for a domain object. * Exposes types of views in Open MCT.
* *
* When called by Open MCT, this may include additional arguments * @interface ViewProvider
* which are on the path to the object to be viewed; for instance, * @property {string} key a unique identifier for this view
* when viewing "A Folder" within "My Items", this method will be * @property {string} name the human-readable name of this view
* invoked with "A Folder" (as a domain object) as the first argument * @property {string} [description] a longer-form description (typically
* * a single sentence or short paragraph) of this kind of view
* @method canView * @property {string} [cssClass] the CSS class to apply to labels for this
* @memberof module:openmct.ViewProvider# * view (to add icons, for instance)
* @param {module:openmct.DomainObject} domainObject the domain object * @memberof module:openmct
* to be viewed */
* @param {array} objectPath - The current contextual object path of the view object
* eg current domainObject is located under MyItems which is under Root
* @returns {boolean} 'true' if the view applies to the provided object,
* otherwise 'false'.
*/
/** /**
* An optional function that defines whether or not this view can be used to edit a given object. * Check if this provider can supply views for a domain object.
* If not provided, will default to `false` and the view will not support editing. To support editing, *
* return true from this function and then - * When called by Open MCT, this may include additional arguments
* * Return a {@link openmct.View} from the `view` function, using the `onEditModeChange` callback to * which are on the path to the object to be viewed; for instance,
* add and remove editing elements from the view * when viewing "A Folder" within "My Items", this method will be
* OR * invoked with "A Folder" (as a domain object) as the first argument
* * Return a {@link openmct.View} from the `view` function defining a read-only view. *
* AND * @method canView
* * Define an {@link openmct.ViewProvider#Edit} function on the view provider that returns an * @memberof module:openmct.ViewProvider#
* editing-specific view. * @param {module:openmct.DomainObject} domainObject the domain object
* * to be viewed
* @method canEdit * @param {array} objectPath - The current contextual object path of the view object
* @memberof module:openmct.ViewProvider# * eg current domainObject is located under MyItems which is under Root
* @param {module:openmct.DomainObject} domainObject the domain object * @returns {boolean} 'true' if the view applies to the provided object,
* to be edited * otherwise 'false'.
* @param {array} objectPath - The current contextual object path of the view object */
* eg current domainObject is located under MyItems which is under Root
* @returns {boolean} 'true' if the view can be used to edit the provided object,
* otherwise 'false'.
*/
/** /**
* Optional method determining the priority of a given view. If this * An optional function that defines whether or not this view can be used to edit a given object.
* function is not defined on a view provider, then a default priority * If not provided, will default to `false` and the view will not support editing. To support editing,
* of 100 will be applicable for all objects supported by this view. * return true from this function and then -
* * * Return a {@link openmct.View} from the `view` function, using the `onEditModeChange` callback to
* @method priority * add and remove editing elements from the view
* @memberof module:openmct.ViewProvider# * OR
* @param {module:openmct.DomainObject} domainObject the domain object * * Return a {@link openmct.View} from the `view` function defining a read-only view.
* to be viewed * AND
* @returns {number} The priority of the view. If multiple views could apply * * Define an {@link openmct.ViewProvider#Edit} function on the view provider that returns an
* to an object, the view that returns the lowest number will be * editing-specific view.
* the default view. *
*/ * @method canEdit
* @memberof module:openmct.ViewProvider#
* @param {module:openmct.DomainObject} domainObject the domain object
* to be edited
* @param {array} objectPath - The current contextual object path of the view object
* eg current domainObject is located under MyItems which is under Root
* @returns {boolean} 'true' if the view can be used to edit the provided object,
* otherwise 'false'.
*/
/** /**
* Provide a view of this object. * Optional method determining the priority of a given view. If this
* * function is not defined on a view provider, then a default priority
* When called by Open MCT, the following arguments will be passed to it: * of 100 will be applicable for all objects supported by this view.
* @param {object} domainObject - the domainObject that the view is provided for *
* @param {array} objectPath - The current contextual object path of the view object * @method priority
* eg current domainObject is located under MyItems which is under Root * @memberof module:openmct.ViewProvider#
* * @param {module:openmct.DomainObject} domainObject the domain object
* @method view * to be viewed
* @memberof module:openmct.ViewProvider# * @returns {number} The priority of the view. If multiple views could apply
* @param {*} object the object to be viewed * to an object, the view that returns the lowest number will be
* @returns {module:openmct.View} a view of this domain object * the default view.
*/ */
/** /**
* Provide an edit-mode specific view of this object. * Provide a view of this object.
* *
* If optionally specified, this function will be called when the application * When called by Open MCT, the following arguments will be passed to it:
* enters edit mode. This will cause the active non-edit mode view and its * @param {object} domainObject - the domainObject that the view is provided for
* dom element to be destroyed. * @param {array} objectPath - The current contextual object path of the view object
* * eg current domainObject is located under MyItems which is under Root
* @method edit *
* @memberof module:openmct.ViewProvider# * @method view
* @param {*} object the object to be edit * @memberof module:openmct.ViewProvider#
* @returns {module:openmct.View} an editable view of this domain object * @param {*} object the object to be viewed
*/ * @returns {module:openmct.View} a view of this domain object
*/
return ViewRegistry; /**
}); * Provide an edit-mode specific view of this object.
*
* If optionally specified, this function will be called when the application
* enters edit mode. This will cause the active non-edit mode view and its
* dom element to be destroyed.
*
* @method edit
* @memberof module:openmct.ViewProvider#
* @param {*} object the object to be edit
* @returns {module:openmct.View} an editable view of this domain object
*/

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