From 241d75e3934a276ea1a69633bd074e1834f7efa6 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Fri, 20 Apr 2018 15:46:09 -0700 Subject: [PATCH] Summary widget telemetry provider (#1943) * Summary Widgets produce telemetry Adds a summary widget telemetry provider and metadata provider to the summary widget plugin. Supports subscribing to realtime summary widget evaluations without needing the summary widget UI. Fixes https://github.com/nasa/openmct/issues/1893 * Use metadata to determine telemetry types Update summary widgets to use metadata to determine telemetry types. fixes https://github.com/nasa/openmct/issues/1801 fixes https://github.com/nasa/openmct/issues/1883 * shared evaluators, more telemetry values Share summary widget evaluators to reduce number of times a object needs to be loaded when dealing with multiple queries. Fixes https://github.com/nasa/openmct/issues/1893 * Separate view for editing fixes https://github.com/nasa/openmct/issues/1827 * Update summary widget tests * Workaround incorrect telemetry capability application In the case where an object support telemetry but does not support the specific type of telemetry request i.e. a summary widget has request for lad but not for historical, an error will be thrown. * use makeKeyString use makeKeyString when storing configuration of objects in summary widgets. Otherwise, namespace information would not be properly tracked. Fixes https://github.com/nasa/openmct/issues/1949 * [Tests] coverage for EvaluatorPool Add tests for EvaluatorPool and fix a bug where the same evaluator was returned for different objects. * Add copyright headers * Update metadata provider registration * attach title to a element * Only evaluate realtime when all data available * Prevent update after destroy * Don't error when no telemetry exists * Don't mutate on view destroy Improper removal of listeners was triggering a mutation on view destroy, which happens after the initial persist call to server that occurs when saving. This mutation occurs after the edit transaction has been closed, which would result in an immediate persist call. This can cause a race condition when the first persist call has not completed, which causes a 409 conflict and a persistence error. Fix #1827 * Spec for telemetryProvider * update on time system change Summary Widgets now resubscribe and requery for data when time system changes, in order to ensure they're showing the correct data to the user. * link to telemetry request details * rename variables, update jsdoc Addresses comments in https://github.com/nasa/openmct/pull/1943 --- API.md | 11 +- src/plugins/summaryWidget/plugin.js | 64 ++- .../summaryWidget/src/ConditionManager.js | 86 ++-- src/plugins/summaryWidget/src/TestDataItem.js | 7 + .../summaryWidget/src/TestDataManager.js | 13 +- .../summaryWidget/src/input/ObjectSelect.js | 12 +- .../src/telemetry/EvaluatorPool.js | 64 +++ .../src/telemetry/EvaluatorPoolSpec.js | 102 ++++ .../src/telemetry/SummaryWidgetCondition.js | 80 +++ .../telemetry/SummaryWidgetConditionSpec.js | 142 ++++++ .../src/telemetry/SummaryWidgetEvaluator.js | 281 +++++++++++ .../SummaryWidgetMetadataProvider.js | 119 +++++ .../src/telemetry/SummaryWidgetRule.js | 73 +++ .../src/telemetry/SummaryWidgetRuleSpec.js | 163 ++++++ .../SummaryWidgetTelemetryProvider.js | 64 +++ .../SummaryWidgetTelemetryProviderSpec.js | 475 ++++++++++++++++++ .../summaryWidget/src/telemetry/operations.js | 197 ++++++++ .../src/views/SummaryWidgetView.js | 92 ++++ .../src/views/SummaryWidgetViewProvider.js | 42 ++ .../src/views/summary-widget.html | 5 + .../test/ConditionManagerSpec.js | 116 +++-- 21 files changed, 2092 insertions(+), 116 deletions(-) create mode 100644 src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js create mode 100644 src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js create mode 100644 src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js create mode 100644 src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js create mode 100644 src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js create mode 100644 src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js create mode 100644 src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js create mode 100644 src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js create mode 100644 src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js create mode 100644 src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js create mode 100644 src/plugins/summaryWidget/src/telemetry/operations.js create mode 100644 src/plugins/summaryWidget/src/views/SummaryWidgetView.js create mode 100644 src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js create mode 100644 src/plugins/summaryWidget/src/views/summary-widget.html diff --git a/API.md b/API.md index 7a106b7666..0ec11959f6 100644 --- a/API.md +++ b/API.md @@ -23,7 +23,7 @@ - [Value Hints](#value-hints) - [The Time Conductor and Telemetry](#the-time-conductor-and-telemetry) - [Telemetry Providers](#telemetry-providers) - - [Telemetry Requests](#telemetry-requests) + - [Telemetry Requests and Responses.](#telemetry-requests-and-responses) - [Request Strategies **draft**](#request-strategies-draft) - [`latest` request strategy](#latest-request-strategy) - [`minmax` request strategy](#minmax-request-strategy) @@ -32,7 +32,7 @@ - [Telemetry Data](#telemetry-data) - [Telemetry Datums](#telemetry-datums) - [Limit Evaluators **draft**](#limit-evaluators-draft) - - [Telemetry Visualization APIs **draft**](#telemetry-visualization-apis-draft) + - [Telemetry Consumer APIs **draft**](#telemetry-consumer-apis-draft) - [Time API](#time-api) - [Time Systems and Bounds](#time-systems-and-bounds) - [Defining and Registering Time Systems](#defining-and-registering-time-systems) @@ -449,7 +449,7 @@ A telemetry provider is a javascript object with up to four methods: * `supportsSubscribe(domainObject, callback, options)` optional. Must be implemented to provide realtime telemetry. Should return `true` if the provider supports subscriptions for the given domain object (and request options). * `subscribe(domainObject, callback, options)` required if `supportsSubscribe` is implemented. Establish a subscription for realtime data for the given domain object. Should invoke `callback` with a single telemetry datum every time data is received. Must return an unsubscribe function. Multiple views can subscribe to the same telemetry object, so it should always return a new unsubscribe function. * `supportsRequest(domainObject, options)` optional. Must be implemented to provide historical telemetry. Should return `true` if the provider supports historical requests for the given domain object. -* `request(domainObject, options)` required if `supportsRequest` is implemented. Must return a promise for an array of telemetry datums that fulfills the request. The `options` argument will include a `start`, `end`, and `domain` attribute representing the query bounds. For more request properties, see Request Properties below. +* `request(domainObject, options)` required if `supportsRequest` is implemented. Must return a promise for an array of telemetry datums that fulfills the request. The `options` argument will include a `start`, `end`, and `domain` attribute representing the query bounds. See [Telemetry Requests and Responses](#telemetry-requests-and-responses) for more info on how to respond to requests. * `supportsMetadata(domainObject)` optional. Implement and return `true` for objects that you want to provide dynamic metadata for. * `getMetadata(domainObject)` required if `supportsMetadata` is implemented. Must return a valid telemetry metadata definition that includes at least one valueMetadata definition. * `supportsLimits(domainObject)` optional. Implement and return `true` for domain objects that you want to provide a limit evaluator for. @@ -466,7 +466,7 @@ openmct.telemetry.addProvider({ Note: it is not required to implement all of the methods on every provider. Depending on the complexity of your implementation, it may be helpful to instantiate and register your realtime, historical, and metadata providers separately. -#### Telemetry Requests +#### Telemetry Requests and Responses. Telemetry requests support time bounded queries. A call to a _Telemetry Provider_'s `request` function will include an `options` argument. These are simply javascript objects with attributes for the request parameters. An example of a telemetry request object with a start and end time is included below: @@ -480,8 +480,7 @@ Telemetry requests support time bounded queries. A call to a _Telemetry Provider In this case, the `domain` is the currently selected time-system, and the start and end dates are valid dates in that time system. -The response to a telemetry request is an array of telemetry datums. -These datums must be sorted by `domain` in ascending order. +A telemetry provider's `request` method should return a promise for an array of telemetry datums. These datums must be sorted by `domain` in ascending order. #### Request Strategies **draft** diff --git a/src/plugins/summaryWidget/plugin.js b/src/plugins/summaryWidget/plugin.js index 45df7e22ba..60ae749583 100755 --- a/src/plugins/summaryWidget/plugin.js +++ b/src/plugins/summaryWidget/plugin.js @@ -1,4 +1,14 @@ -define(['./src/SummaryWidget', './SummaryWidgetsCompositionPolicy'], function (SummaryWidget, SummaryWidgetsCompositionPolicy) { +define([ + './SummaryWidgetsCompositionPolicy', + './src/telemetry/SummaryWidgetMetadataProvider', + './src/telemetry/SummaryWidgetTelemetryProvider', + './src/views/SummaryWidgetViewProvider' +], function ( + SummaryWidgetsCompositionPolicy, + SummaryWidgetMetadataProvider, + SummaryWidgetTelemetryProvider, + SummaryWidgetViewProvider +) { function plugin() { @@ -9,8 +19,40 @@ define(['./src/SummaryWidget', './SummaryWidgetsCompositionPolicy'], function (S cssClass: 'icon-summary-widget', initialize: function (domainObject) { domainObject.composition = []; - domainObject.configuration = {}; + domainObject.configuration = { + ruleOrder: ['default'], + ruleConfigById: { + default: { + name: 'Default', + 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: [ { @@ -40,26 +82,14 @@ define(['./src/SummaryWidget', './SummaryWidgetsCompositionPolicy'], function (S ] }; - function initViewProvider(openmct) { - return { - name: 'Widget View', - view: function (domainObject) { - return new SummaryWidget(domainObject, openmct); - }, - canView: function (domainObject) { - return (domainObject.type === 'summary-widget'); - }, - editable: true, - key: 'summaryWidgets' - }; - } - return function install(openmct) { openmct.types.addType('summary-widget', widgetType); - openmct.objectViews.addProvider(initViewProvider(openmct)); openmct.legacyExtension('policies', {category: 'composition', implementation: SummaryWidgetsCompositionPolicy, depends: ['openmct'] }); + openmct.telemetry.addProvider(new SummaryWidgetMetadataProvider(openmct)); + openmct.telemetry.addProvider(new SummaryWidgetTelemetryProvider(openmct)); + openmct.objectViews.addProvider(new SummaryWidgetViewProvider(openmct)); }; } diff --git a/src/plugins/summaryWidget/src/ConditionManager.js b/src/plugins/summaryWidget/src/ConditionManager.js index 385e34770f..120dd954ec 100644 --- a/src/plugins/summaryWidget/src/ConditionManager.js +++ b/src/plugins/summaryWidget/src/ConditionManager.js @@ -1,10 +1,12 @@ define ([ './ConditionEvaluator', + '../../../api/objects/object-utils', 'EventEmitter', 'zepto', 'lodash' ], function ( ConditionEvaluator, + objectUtils, EventEmitter, $, _ @@ -123,21 +125,23 @@ define ([ * has completed and types have been parsed */ ConditionManager.prototype.parsePropertyTypes = function (object) { - var telemetryAPI = this.openmct.telemetry, - key, - type, - self = this; + var objectId = objectUtils.makeKeyString(object.identifier); - self.telemetryTypesById[object.identifier.key] = {}; - return telemetryAPI.request(object, {size: 1, strategy: 'latest'}).then(function (telemetry) { - Object.entries(telemetry[telemetry.length - 1]).forEach(function (telem) { - key = telem[0]; - type = typeof telem[1]; - self.telemetryTypesById[object.identifier.key][key] = type; - self.subscriptionCache[object.identifier.key][key] = telem[1]; - self.addGlobalPropertyType(key, type); - }); - }); + this.telemetryTypesById[objectId] = {}; + Object.values(this.telemetryMetadataById[objectId]).forEach(function (valueMetadata) { + var type; + if (valueMetadata.hints.hasOwnProperty('range')) { + type = 'number'; + } else if (valueMetadata.hints.hasOwnProperty('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); }; /** @@ -147,23 +151,9 @@ define ([ * and property types parsed */ ConditionManager.prototype.parseAllPropertyTypes = function () { - var self = this, - index = 0, - objs = Object.values(self.compositionObjs), - promise = new Promise(function (resolve, reject) { - if (objs.length === 0) { - resolve(); - } - objs.forEach(function (obj) { - self.parsePropertyTypes(obj).then(function () { - if (index === objs.length - 1) { - resolve(); - } - index += 1; - }); - }); - }); - return promise; + Object.values(this.compositionObjs).forEach(this.parsePropertyTypes, this); + this.metadataLoadComplete = true; + this.eventEmitter.emit('metadata'); }; /** @@ -187,7 +177,7 @@ define ([ ConditionManager.prototype.onCompositionAdd = function (obj) { var compositionKeys, telemetryAPI = this.openmct.telemetry, - objId = obj.identifier.key, + objId = objectUtils.makeKeyString(obj.identifier), telemetryMetadata, self = this; @@ -195,10 +185,9 @@ define ([ self.compositionObjs[objId] = obj; self.telemetryMetadataById[objId] = {}; - compositionKeys = self.domainObject.composition.map(function (object) { - return object.key; - }); - if (!compositionKeys.includes(obj.identifier.key)) { + // 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); } @@ -212,6 +201,12 @@ define ([ self.subscriptions[objId] = telemetryAPI.subscribe(obj, function (datum) { self.handleSubscriptionCallback(objId, datum); }, {}); + 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 @@ -234,11 +229,14 @@ define ([ * @private */ ConditionManager.prototype.onCompositionRemove = function (identifier) { + var objectId = objectUtils.makeKeyString(identifier); + // FIXME: this should just update by listener. _.remove(this.domainObject.composition, function (id) { - return id.key === identifier.key; + return id.key === identifier.key && + id.namespace === identifier.namespace; }); - delete this.compositionObjs[identifier.key]; - this.subscriptions[identifier.key](); //unsubscribe from telemetry source + delete this.compositionObjs[objectId]; + this.subscriptions[objectId](); //unsubscribe from telemetry source this.eventEmitter.emit('remove', identifier); if (_.isEmpty(this.compositionObjs)) { @@ -253,13 +251,9 @@ define ([ * @private */ ConditionManager.prototype.onCompositionLoad = function () { - var self = this; - self.loadComplete = true; - self.eventEmitter.emit('load'); - self.parseAllPropertyTypes().then(function () { - self.metadataLoadComplete = true; - self.eventEmitter.emit('metadata'); - }); + this.loadComplete = true; + this.eventEmitter.emit('load'); + this.parseAllPropertyTypes(); }; /** diff --git a/src/plugins/summaryWidget/src/TestDataItem.js b/src/plugins/summaryWidget/src/TestDataItem.js index a4af9ff7bd..94f8b4aa1e 100644 --- a/src/plugins/summaryWidget/src/TestDataItem.js +++ b/src/plugins/summaryWidget/src/TestDataItem.js @@ -126,6 +126,13 @@ define([ } }; + /** + * 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 */ diff --git a/src/plugins/summaryWidget/src/TestDataManager.js b/src/plugins/summaryWidget/src/TestDataManager.js index 30923795a9..8f5b631bac 100644 --- a/src/plugins/summaryWidget/src/TestDataManager.js +++ b/src/plugins/summaryWidget/src/TestDataManager.js @@ -131,15 +131,20 @@ define([ */ TestDataManager.prototype.refreshItems = function () { var self = this; + if (this.items) { + this.items.forEach(function (item) { + this.stopListening(item); + }, this); + } self.items = []; $('.t-test-data-item', this.domElement).remove(); this.config.forEach(function (item, index) { var newItem = new TestDataItem(item, index, self.manager); - newItem.on('remove', self.removeItem, self); - newItem.on('duplicate', self.initItem, self); - newItem.on('change', self.onItemChange, self); + 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); }); @@ -190,10 +195,10 @@ define([ }; TestDataManager.prototype.destroy = function () { + this.stopListening(); this.items.forEach(function (item) { item.remove(); }); - this.stopListening(); }; return TestDataManager; diff --git a/src/plugins/summaryWidget/src/input/ObjectSelect.js b/src/plugins/summaryWidget/src/input/ObjectSelect.js index 6254ec4f64..33af478c76 100644 --- a/src/plugins/summaryWidget/src/input/ObjectSelect.js +++ b/src/plugins/summaryWidget/src/input/ObjectSelect.js @@ -1,4 +1,10 @@ -define(['./Select'], function (Select) { +define([ + './Select', + '../../../../api/objects/object-utils' +], function ( + Select, + objectUtils +) { /** * Create a {Select} element whose composition is dynamically updated with @@ -37,7 +43,7 @@ define(['./Select'], function (Select) { * @private */ function onCompositionAdd(obj) { - self.select.addOption(obj.identifier.key, obj.name); + self.select.addOption(objectUtils.makeKeyString(obj.identifier), obj.name); } /** @@ -75,7 +81,7 @@ define(['./Select'], function (Select) { */ ObjectSelect.prototype.generateOptions = function () { var items = Object.values(this.compositionObjs).map(function (obj) { - return [obj.identifier.key, obj.name]; + return [objectUtils.makeKeyString(obj.identifier), obj.name]; }); this.baseOptions.forEach(function (option, index) { items.splice(index, 0, option); diff --git a/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js b/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js new file mode 100644 index 0000000000..f42ff25893 --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js @@ -0,0 +1,64 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + './SummaryWidgetEvaluator', + '../../../../api/objects/object-utils' +], function ( + SummaryWidgetEvaluator, + objectUtils +) { + + function EvaluatorPool(openmct) { + this.openmct = openmct; + this.byObjectId = {}; + this.byEvaluator = new WeakMap(); + } + + EvaluatorPool.prototype.get = function (domainObject) { + var objectId = objectUtils.makeKeyString(domainObject.identifier); + var 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; + }; + + EvaluatorPool.prototype.release = function (evaluator) { + var poolEntry = this.byEvaluator.get(evaluator); + poolEntry.leases -= 1; + if (poolEntry.leases === 0) { + evaluator.destroy(); + this.byEvaluator.delete(evaluator); + delete this.byObjectId[poolEntry.objectId]; + } + }; + + return EvaluatorPool; +}); diff --git a/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js b/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js new file mode 100644 index 0000000000..bb0ea33fb0 --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js @@ -0,0 +1,102 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + './EvaluatorPool', + './SummaryWidgetEvaluator' +], function ( + EvaluatorPool, + SummaryWidgetEvaluator +) { + describe('EvaluatorPool', function () { + var pool; + var openmct; + var objectA; + var objectB; + + beforeEach(function () { + openmct = { + composition: jasmine.createSpyObj('compositionAPI', ['get']), + objects: jasmine.createSpyObj('objectAPI', ['observe']) + }; + openmct.composition.get.andCallFake(function () { + var compositionCollection = jasmine.createSpyObj( + 'compositionCollection', + [ + 'load', + 'on', + 'off' + ] + ); + compositionCollection.load.andReturn(Promise.resolve()); + return compositionCollection; + }); + openmct.objects.observe.andCallFake(function () { + return function () {}; + }); + pool = new EvaluatorPool(openmct); + objectA = { + identifier: { + namespace: 'someNamespace', + key: 'someKey' + }, + configuration: { + ruleOrder: [] + } + }; + objectB = { + identifier: { + namespace: 'otherNamespace', + key: 'otherKey' + }, + configuration: { + ruleOrder: [] + } + }; + }); + + it('returns new evaluators for different objects', function () { + var evaluatorA = pool.get(objectA); + var evaluatorB = pool.get(objectB); + expect(evaluatorA).not.toBe(evaluatorB); + }); + + it('returns the same evaluator for the same object', function () { + var evaluatorA = pool.get(objectA); + var evaluatorB = pool.get(objectA); + expect(evaluatorA).toBe(evaluatorB); + + var evaluatorC = pool.get(JSON.parse(JSON.stringify(objectA))); + expect(evaluatorA).toBe(evaluatorC); + }); + + it('returns new evaluator when old is released', function () { + var evaluatorA = pool.get(objectA); + var evaluatorB = pool.get(objectA); + expect(evaluatorA).toBe(evaluatorB); + pool.release(evaluatorA); + pool.release(evaluatorB); + var evaluatorC = pool.get(objectA); + expect(evaluatorA).not.toBe(evaluatorC); + }); + }); +}); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js new file mode 100644 index 0000000000..689dbbf1ea --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js @@ -0,0 +1,80 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + './operations' +], function ( + 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) { + var stateKeys = Object.keys(telemetryState); + var state; + var result; + var 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; + } + } + return true; + } else { + return this.evaluateState(telemetryState[this.object]); + } + }; + + SummaryWidgetCondition.prototype.evaluateState = function (state) { + var testValues = [ + state.formats[this.key].parse(state.lastDatum) + ].concat(this.values); + return this.comparator(testValues); + }; + + return SummaryWidgetCondition; +}); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js new file mode 100644 index 0000000000..1db4f60da9 --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js @@ -0,0 +1,142 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + './SummaryWidgetCondition' +], function ( + SummaryWidgetCondition +) { + + describe('SummaryWidgetCondition', function () { + var condition; + var telemetryState; + + beforeEach(function () { + // Format map intentionally uses different keys than those present + // in datum, which serves to verify conditions use format map to get + // data. + var formatMap = { + adjusted: { + parse: function (datum) { + return datum.value + 10; + } + }, + raw: { + parse: function (datum) { + return datum.value; + } + } + }; + + telemetryState = { + objectId: { + formats: formatMap, + lastDatum: { + } + }, + otherObjectId: { + formats: formatMap, + lastDatum: { + } + } + }; + + }); + + it('can evaluate if a single object matches', function () { + condition = new SummaryWidgetCondition({ + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [ + 10 + ] + }); + 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 a single object matches (alternate keys)', function () { + condition = new SummaryWidgetCondition({ + object: 'objectId', + key: 'adjusted', + operation: 'greaterThan', + values: [ + 10 + ] + }); + 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 all objects match"', function () { + condition = new SummaryWidgetCondition({ + object: 'all', + 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(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 evalute "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); + }); + + }); +}); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js new file mode 100644 index 0000000000..7ae94d3e42 --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js @@ -0,0 +1,281 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + './SummaryWidgetRule', + '../eventHelpers', + '../../../../api/objects/object-utils', + 'lodash' +], function ( + SummaryWidgetRule, + 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); + this.removeObserver = openmct.objects.observe( + domainObject, + '*', + this.updateRules.bind(this) + ); + + var 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) { + var active = true; + var unsubscribes = []; + + this.getBaseStateClone() + .then(function (realtimeStates) { + if (!active) { + return; + } + var updateCallback = function () { + var datum = this.evaluateState( + realtimeStates, + this.openmct.time.timeSystem().key + ); + if (datum) { + callback(datum); + } + }.bind(this); + + unsubscribes = _.map( + realtimeStates, + this.subscribeToObjectState.bind(this, updateCallback) + ); + }.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) { + var 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) { + var childId = objectUtils.makeKeyString(childObject.identifier); + var metadata = this.openmct.telemetry.getMetadata(childObject); + var formats = this.openmct.telemetry.getFormatMap(metadata); + + this.baseState[childId] = { + id: childId, + domainObject: childObject, + metadata: metadata, + formats: formats + }; + }; + + SummaryWidgetEvaluator.prototype.removeChild = function (childObject) { + var 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 () { + return _(this.baseState) + .values() + .map(_.clone) + .indexBy('id') + .value(); + }.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 = _.extend({}, 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) { + var 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) { + var 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) { + var hasRequiredData = Object.keys(state).reduce(function (itDoes, k) { + return itDoes && state[k].lastDatum; + }, true); + if (!hasRequiredData) { + return; + } + + for (var i = this.rules.length - 1; i > 0; i--) { + if (this.rules[i].evaluate(state, false)) { + break; + } + } + + var latestTimestamp = _(state) + .map('timestamps') + .sortBy(timestampKey) + .last(); + + if (!latestTimestamp) { + latestTimestamp = {}; + } + + var baseDatum = _.clone(latestTimestamp); + return this.makeDatumFromRule(i, baseDatum); + }; + + /** + * remove all listeners and clean up any resources. + */ + SummaryWidgetEvaluator.prototype.destroy = function () { + this.stopListening(); + this.removeObserver(); + }; + + return SummaryWidgetEvaluator; + +}); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js new file mode 100644 index 0000000000..d7ae377e02 --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js @@ -0,0 +1,119 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + +], function ( + +) { + + function SummaryWidgetMetadataProvider(openmct) { + this.openmct = openmct; + } + + SummaryWidgetMetadataProvider.prototype.supportsMetadata = function (domainObject) { + return domainObject.type === 'summary-widget'; + }; + + SummaryWidgetMetadataProvider.prototype.getDomains = function (domainObject) { + return this.openmct.time.getAllTimeSystems().map(function (ts, i) { + return { + key: ts.key, + name: 'UTC', + format: ts.timeFormat, + hints: { + domain: i + } + }; + }); + }; + + SummaryWidgetMetadataProvider.prototype.getMetadata = function (domainObject) { + var ruleOrder = domainObject.configuration.ruleOrder || []; + var enumerations = ruleOrder + .filter(function (ruleId) { + return !!domainObject.configuration.ruleConfigById[ruleId]; + }) + .map(function (ruleId, ruleIndex) { + return { + string: domainObject.configuration.ruleConfigById[ruleId].label, + value: ruleIndex + }; + }); + + var 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' + } + ]) + }; + + return metadata; + }; + + return SummaryWidgetMetadataProvider; + +}); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js new file mode 100644 index 0000000000..e2f9a936f3 --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js @@ -0,0 +1,73 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + './SummaryWidgetCondition' +], function ( + 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; + } + + /** + * Evaluate the given rule against a telemetryState and return true if it + * matches. + */ + SummaryWidgetRule.prototype.evaluate = function (telemetryState) { + var i; + var 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; +}); + diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js new file mode 100644 index 0000000000..2a527715f8 --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js @@ -0,0 +1,163 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + './SummaryWidgetRule' +], function ( + SummaryWidgetRule +) { + describe('SummaryWidgetRule', function () { + + var rule; + var telemetryState; + + beforeEach(function () { + var formatMap = { + raw: { + parse: function (datum) { + return datum.value; + } + } + }; + + telemetryState = { + objectId: { + formats: formatMap, + 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 + ] + }, { + 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); + }); + }); +}); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js new file mode 100644 index 0000000000..4f835dffcf --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js @@ -0,0 +1,64 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + './EvaluatorPool' +], function ( + EvaluatorPool +) { + + 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([]); + } + + var evaluator = this.pool.get(domainObject); + return evaluator.requestLatest(options) + .then(function (latestDatum) { + this.pool.release(evaluator); + return [latestDatum]; + }.bind(this)); + }; + + SummaryWidgetTelemetryProvider.prototype.supportsSubscribe = function (domainObject) { + return domainObject.type === 'summary-widget'; + }; + + SummaryWidgetTelemetryProvider.prototype.subscribe = function (domainObject, callback) { + var evaluator = this.pool.get(domainObject); + var unsubscribe = evaluator.subscribe(callback); + return function () { + this.pool.release(evaluator); + unsubscribe(); + }.bind(this); + }; + + return SummaryWidgetTelemetryProvider; +}); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js new file mode 100644 index 0000000000..4b6ab18105 --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js @@ -0,0 +1,475 @@ +define([ + './SummaryWidgetTelemetryProvider' +], function ( + SummaryWidgetTelemetryProvider +) { + + describe('SummaryWidgetTelemetryProvider', function () { + var telemObjectA; + var telemObjectB; + var summaryWidgetObject; + var openmct; + var telemUnsubscribes; + var unobserver; + var composition; + var telemetryProvider; + var 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.andReturn([{key: 'timestamp'}]); + openmct.time.timeSystem.andReturn({key: 'timestamp'}); + + + unobserver = jasmine.createSpy('unobserver'); + openmct.objects.observe.andReturn(unobserver); + + + composition = jasmine.createSpyObj('compositionCollection', [ + 'on', + 'off', + 'load' + ]); + + function notify(eventName, a, b) { + composition.on.calls.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.andCallFake(function () { + setTimeout(function () { + notify('add', telemObjectA); + setTimeout(function () { + notify('add', telemObjectB); + setTimeout(function () { + loader.resolve(); + setTimeout(function () { + loader.loaded = true; + }); + }); + }); + }); + return loader.promise; + }); + openmct.composition.get.andReturn(composition); + + + telemUnsubscribes = []; + openmct.telemetry.subscribe.andCallFake(function () { + var unsubscriber = jasmine.createSpy('unsubscriber' + telemUnsubscribes.length); + telemUnsubscribes.push(unsubscriber); + return unsubscriber; + }); + + openmct.telemetry.getMetadata.andCallFake(function (object) { + return { + name: 'fake metadata manager', + object: object, + keys: ['timestamp', 'measurement'] + }; + }); + + openmct.telemetry.getFormatMap.andCallFake(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 () { + var result; + telemetryProvider.request(summaryWidgetObject, {}) + .then(function (r) { + result = r; + }); + waitsFor(function () { + return !!result; + }); + runs(function () { + expect(result).toEqual([]); + }); + }); + + it('provides realtime telemetry', function () { + var callback = jasmine.createSpy('callback'); + telemetryProvider.subscribe(summaryWidgetObject, callback); + + waitsFor(function () { + return loader.loaded; + }); + + runs(function () { + expect(openmct.telemetry.subscribe.calls.length).toBe(2); + expect(openmct.telemetry.subscribe) + .toHaveBeenCalledWith(telemObjectA, jasmine.any(Function)); + expect(openmct.telemetry.subscribe) + .toHaveBeenCalledWith(telemObjectB, jasmine.any(Function)); + + var aCallback = openmct.telemetry.subscribe.calls[0].args[1]; + var bCallback = openmct.telemetry.subscribe.calls[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' + }); + callback.reset(); + 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' + }); + callback.reset(); + 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' + }); + callback.reset(); + 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' + }); + callback.reset(); + 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 () { + var isResolved; + var resolver; + var responseDatums; + var resultsShouldBe; + + beforeEach(function () { + isResolved = false; + resolver = jasmine.createSpy('resolved') + .andCallFake(function () { + isResolved = true; + }); + + openmct.telemetry.request.andCallFake(function (rObj, options) { + expect(rObj).toEqual(jasmine.any(Object)); + expect(options).toEqual({size: 1, strategy: 'latest', domain: 'timestamp'}); + expect(responseDatums[rObj.identifier.namespace]).toBeDefined(); + return Promise.resolve([responseDatums[rObj.identifier.namespace]]); + }); + responseDatums = {}; + + resultsShouldBe = function (results) { + telemetryProvider + .request(summaryWidgetObject, {size: 1, strategy: 'latest', domain: 'timestamp'}) + .then(resolver); + + waitsFor(function () { + return isResolved; + }); + + runs(function () { + expect(resolver).toHaveBeenCalledWith(results); + }); + }; + }); + + it("returns default when no rule matches", function () { + responseDatums = { + a: { + t: 122, + m: 25 + }, + b: { + t: 111, + m: 25 + } + }; + + resultsShouldBe([{ + timestamp: 122, + 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' + }]); + }); + + it("returns highest priority when multiple match", function () { + responseDatums = { + a: { + t: 131, + m: 55 + }, + b: { + t: 139, + m: 5 + } + }; + + resultsShouldBe([{ + timestamp: 139, + ruleLabel: "WORRY!", + ruleName: "B Low", + message: "B is Low", + ruleIndex: 2, + backgroundColor: '#ff0000', + textColor: '#ff00ff', + borderColor: 'rgba(1,0,0,0)', + icon: 'b-low' + }]); + }); + + it("returns matching rule", function () { + responseDatums = { + a: { + t: 144, + m: 55 + }, + b: { + t: 141, + m: 15 + } + }; + + 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' + }]); + }); + + }); + + }); +}); diff --git a/src/plugins/summaryWidget/src/telemetry/operations.js b/src/plugins/summaryWidget/src/telemetry/operations.js new file mode 100644 index 0000000000..7202d5f52c --- /dev/null +++ b/src/plugins/summaryWidget/src/telemetry/operations.js @@ -0,0 +1,197 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + +], function ( + +) { + var 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: { + 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'], + inputCount: 0, + getDescription: function () { + return ' is undefined'; + } + }, + isDefined: { + operation: function (input) { + return typeof input[0] !== 'undefined'; + }, + text: 'is defined', + appliesTo: ['string', 'number'], + inputCount: 0, + getDescription: function () { + return ' is defined'; + } + } + }; + + return OPERATIONS; +}); diff --git a/src/plugins/summaryWidget/src/views/SummaryWidgetView.js b/src/plugins/summaryWidget/src/views/SummaryWidgetView.js new file mode 100644 index 0000000000..c6c70f0e76 --- /dev/null +++ b/src/plugins/summaryWidget/src/views/SummaryWidgetView.js @@ -0,0 +1,92 @@ +define([ + 'text!./summary-widget.html' +], function ( + summaryWidgetTemplate +) { + function SummaryWidgetView(domainObject, openmct) { + this.openmct = openmct; + this.domainObject = domainObject; + this.hasUpdated = false; + this.render = this.render.bind(this); + } + + SummaryWidgetView.prototype.updateState = function (datum) { + this.hasUpdated = true; + this.widget.style.color = datum.textColor; + this.widget.style.backgroundColor = datum.backgroundColor; + this.widget.style.borderColor = datum.borderColor; + this.widget.title = datum.message; + this.label.title = datum.message; + this.label.innerHTML = datum.ruleLabel; + this.label.className = 'label widget-label ' + datum.icon; + }; + + SummaryWidgetView.prototype.render = function () { + if (this.unsubscribe) { + this.unsubscribe(); + } + this.hasUpdated = false; + + this.container.innerHTML = summaryWidgetTemplate; + this.widget = this.container.querySelector('a'); + this.label = this.container.querySelector('.widget-label'); + + + if (this.domainObject.url) { + this.widget.setAttribute('href', this.domainObject.url); + } else { + this.widget.removeAttribute('href'); + } + + if (this.domainObject.openNewTab === 'newTab') { + this.widget.setAttribute('target', '_blank'); + } else { + this.widget.removeAttribute('target'); + } + var renderTracker = {}; + this.renderTracker = renderTracker; + this.openmct.telemetry.request(this.domainObject, { + strategy: 'latest', + size: 1 + }).then(function (results) { + if (this.destroyed || this.hasUpdated || this.renderTracker !== renderTracker) { + return; + } + this.updateState(results[results.length - 1]); + }.bind(this)); + + this.unsubscribe = this.openmct + .telemetry + .subscribe(this.domainObject, this.updateState.bind(this)); + }; + + SummaryWidgetView.prototype.show = function (container) { + this.container = container; + this.render(); + this.removeMutationListener = this.openmct.objects.observe( + this.domainObject, + '*', + this.onMutation.bind(this) + ); + this.openmct.time.on('timeSystem', this.render); + }; + + SummaryWidgetView.prototype.onMutation = function (domainObject) { + this.domainObject = domainObject; + this.render(); + }; + + SummaryWidgetView.prototype.destroy = function (container) { + this.unsubscribe(); + this.removeMutationListener(); + this.openmct.time.off('timeSystem', this.render); + this.destroyed = true; + delete this.widget; + delete this.label; + delete this.openmct; + delete this.domainObject; + }; + + return SummaryWidgetView; + +}); diff --git a/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js b/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js new file mode 100644 index 0000000000..b0aa7c01a0 --- /dev/null +++ b/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js @@ -0,0 +1,42 @@ +define([ + '../SummaryWidget', + './SummaryWidgetView', + '../../../../api/objects/object-utils' +], function ( + SummaryWidgetEditView, + SummaryWidgetView, + objectUtils +) { + + + /** + * + */ + function SummaryWidgetViewProvider(openmct) { + return { + key: 'summary-widget-viewer', + name: 'Widget View', + canView: function (domainObject) { + return domainObject.type === 'summary-widget'; + }, + view: function (domainObject) { + var statusService = openmct.$injector.get('statusService'); + var objectId = objectUtils.makeKeyString(domainObject.identifier); + var statuses = statusService.listStatuses(objectId); + var isEditing = statuses.indexOf('editing') !== -1; + + if (isEditing) { + return new SummaryWidgetEditView(domainObject, openmct); + } else { + return new SummaryWidgetView(domainObject, openmct); + } + }, + editable: true, + priority: function (domainObject) { + return 1; + } + }; + } + + return SummaryWidgetViewProvider; +}); diff --git a/src/plugins/summaryWidget/src/views/summary-widget.html b/src/plugins/summaryWidget/src/views/summary-widget.html new file mode 100644 index 0000000000..b8675649cd --- /dev/null +++ b/src/plugins/summaryWidget/src/views/summary-widget.html @@ -0,0 +1,5 @@ +
+ + Loading... + +
diff --git a/src/plugins/summaryWidget/test/ConditionManagerSpec.js b/src/plugins/summaryWidget/test/ConditionManagerSpec.js index ceeeea442b..b8a8722161 100644 --- a/src/plugins/summaryWidget/test/ConditionManagerSpec.js +++ b/src/plugins/summaryWidget/test/ConditionManagerSpec.js @@ -19,6 +19,7 @@ define(['../src/ConditionManager'], function (ConditionManager) { removeCallbackSpy, telemetryCallbackSpy, metadataCallbackSpy, + telemetryRequests, mockTelemetryValues, mockTelemetryValues2, mockConditionEvaluator; @@ -61,31 +62,43 @@ define(['../src/ConditionManager'], function (ConditionManager) { mockCompObject1: { property1: { key: 'property1', - name: 'Property 1' + name: 'Property 1', + format: 'string', + hints: {} }, property2: { key: 'property2', - name: 'Property 2' + name: 'Property 2', + hints: { + domain: 1 + } } }, mockCompObject2: { property3: { key: 'property3', - name: 'Property 3' + name: 'Property 3', + format: 'string', + hints: {} }, property4: { key: 'property4', - name: 'Property 4' + name: 'Property 4', + hints: { + range: 1 + } } }, mockCompObject3: { property1: { key: 'property1', - name: 'Property 1' + name: 'Property 1', + hints: {} }, property2: { key: 'property2', - name: 'Property 2' + name: 'Property 2', + hints: {} } } }; @@ -160,22 +173,20 @@ define(['../src/ConditionManager'], function (ConditionManager) { unregisterSpies[event](); }); mockComposition.load.andCallFake(function () { - mockEventCallbacks.add(mockCompObject1); - mockEventCallbacks.add(mockCompObject2); - mockEventCallbacks.load(); + mockComposition.triggerCallback('add', mockCompObject1); + mockComposition.triggerCallback('add', mockCompObject2); + mockComposition.triggerCallback('load'); }); - mockComposition.triggerCallback.andCallFake(function (event) { + mockComposition.triggerCallback.andCallFake(function (event, obj) { if (event === 'add') { - mockEventCallbacks.add(mockCompObject3); + mockEventCallbacks.add(obj); } else if (event === 'remove') { - mockEventCallbacks.remove({ - key: 'mockCompObject2' - }); + mockEventCallbacks.remove(obj.identifier); } else { mockEventCallbacks[event](); } }); - + telemetryRequests = []; mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [ 'request', 'isTelemetryObject', @@ -184,9 +195,15 @@ define(['../src/ConditionManager'], function (ConditionManager) { 'triggerTelemetryCallback' ]); mockTelemetryAPI.request.andCallFake(function (obj) { - return new Promise(function (resolve, reject) { - resolve(mockTelemetryValues[obj.identifer.key]); + var req = { + object: obj + }; + req.promise = new Promise(function (resolve, reject) { + req.resolve = resolve; + req.reject = reject; }); + telemetryRequests.push(req); + return req.promise; }); mockTelemetryAPI.isTelemetryObject.andReturn(true); mockTelemetryAPI.getMetadata.andCallFake(function (obj) { @@ -245,41 +262,50 @@ define(['../src/ConditionManager'], function (ConditionManager) { var allKeys = { property1: { key: 'property1', - name: 'Property 1' + name: 'Property 1', + format: 'string', + hints: {} }, property2: { key: 'property2', - name: 'Property 2' + name: 'Property 2', + hints: { + domain: 1 + } }, property3: { key: 'property3', - name: 'Property 3' + name: 'Property 3', + format: 'string', + hints: {} }, property4: { key: 'property4', - name: 'Property 4' + name: 'Property 4', + hints: { + range: 1 + } } }; expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); - mockComposition.triggerCallback('add'); + mockComposition.triggerCallback('add', mockCompObject3); expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); }); it('loads and gets telemetry property types', function () { - conditionManager.parseAllPropertyTypes().then(function () { - expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1')) - .toEqual('string'); - expect(conditionManager.getTelemetryPropertyType('mockCompObject2', 'property4')) - .toEqual('number'); - expect(conditionManager.metadataLoadComplete()).toEqual(true); - expect(metadataCallbackSpy).toHaveBeenCalled(); - }); + conditionManager.parseAllPropertyTypes(); + expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1')) + .toEqual('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'); + mockComposition.triggerCallback('add', mockCompObject3); expect(addCallbackSpy).toHaveBeenCalledWith(mockCompObject3); expect(conditionManager.getComposition()).toEqual({ mockCompObject1: mockCompObject1, @@ -289,7 +315,7 @@ define(['../src/ConditionManager'], function (ConditionManager) { }); it('responds to a composition remove event and invokes the appropriate handlers', function () { - mockComposition.triggerCallback('remove'); + mockComposition.triggerCallback('remove', mockCompObject2); expect(removeCallbackSpy).toHaveBeenCalledWith({ key: 'mockCompObject2' }); @@ -300,7 +326,7 @@ define(['../src/ConditionManager'], function (ConditionManager) { }); it('unregisters telemetry subscriptions and composition listeners on destroy', function () { - mockComposition.triggerCallback('add'); + mockComposition.triggerCallback('add', mockCompObject3); conditionManager.destroy(); Object.values(unsubscribeSpies).forEach(function (spy) { expect(spy).toHaveBeenCalled(); @@ -311,7 +337,19 @@ define(['../src/ConditionManager'], function (ConditionManager) { }); it('populates its LAD cache with historial data on load, if available', function () { - conditionManager.parseAllPropertyTypes().then(function () { + expect(telemetryRequests.length).toBe(2); + expect(telemetryRequests[0].object).toBe(mockCompObject1); + expect(telemetryRequests[1].object).toBe(mockCompObject2); + + expect(telemetryCallbackSpy).not.toHaveBeenCalled(); + + telemetryRequests[0].resolve([mockTelemetryValues.mockCompObject1]); + telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]); + + waitsFor(function () { + return telemetryCallbackSpy.calls.length === 2; + }); + runs(function () { expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a string'); expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(66); }); @@ -352,12 +390,10 @@ define(['../src/ConditionManager'], function (ConditionManager) { }); it('gets the human-readable name of a telemetry field', function () { - conditionManager.parseAllPropertyTypes().then(function () { - expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1')) - .toEqual('Property 1'); - expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4')) - .toEqual('Property 4'); - }); + expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1')) + .toEqual('Property 1'); + expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4')) + .toEqual('Property 4'); }); it('gets its associated ConditionEvaluator', function () {