diff --git a/package.json b/package.json index 50a8d53cd9..bd876eb67e 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "request": "^2.69.0", "split": "^1.0.0", "style-loader": "^1.0.1", + "uuid": "^3.3.3", "v8-compile-cache": "^1.1.0", "vue": "2.5.6", "vue-loader": "^15.2.6", diff --git a/src/plugins/condition/Condition.js b/src/plugins/condition/Condition.js new file mode 100644 index 0000000000..ab3bc0858d --- /dev/null +++ b/src/plugins/condition/Condition.js @@ -0,0 +1,247 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import * as EventEmitter from 'eventemitter3'; +import uuid from 'uuid'; +import TelemetryCriterion from "@/plugins/condition/criterion/TelemetryCriterion"; +import { TRIGGER } from "@/plugins/condition/utils/constants"; +import {computeCondition} from "@/plugins/condition/utils/evaluator"; + +/* +* conditionDefinition = { +* identifier: { +* key: '', +* namespace: '' +* }, +* trigger: 'any'/'all', +* criteria: [ +* { +* operation: '', +* input: '', +* metaDataKey: '', +* key: 'someTelemetryObjectKey' +* } +* ] +* } +*/ +export default class ConditionClass extends EventEmitter { + + /** + * Manages criteria and emits the result of - true or false - based on criteria evaluated. + * @constructor + * @param conditionDefinition: {identifier: {domainObject.identifier},trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} } + * @param openmct + */ + constructor(conditionDefinition, openmct) { + super(); + + this.openmct = openmct; + this.id = this.openmct.objects.makeKeyString(conditionDefinition.identifier); + this.criteria = []; + this.criteriaResults = {}; + if (conditionDefinition.definition.criteria) { + this.createCriteria(conditionDefinition.definition.criteria); + } + this.trigger = conditionDefinition.definition.trigger; + this.result = null; + this.openmct.objects.get(this.id).then(obj => this.observeForChanges(obj)); + } + + observeForChanges(conditionDO) { + this.stopObservingForChanges = this.openmct.objects.observe(conditionDO, '*', this.update.bind(this)); + } + + update(newDomainObject) { + this.updateTrigger(newDomainObject.definition.trigger); + this.updateCriteria(newDomainObject.definition.criteria); + } + + updateTrigger(trigger) { + if (this.trigger !== trigger) { + this.trigger = trigger; + this.handleConditionUpdated(); + } + } + + generateCriterion(criterionDefinition) { + return { + id: uuid(), + operation: criterionDefinition.operation || '', + input: criterionDefinition.input === undefined ? [] : criterionDefinition.input, + metaDataKey: criterionDefinition.metaDataKey || '', + key: criterionDefinition.key || '' + }; + } + + createCriteria(criterionDefinitions) { + criterionDefinitions.forEach((criterionDefinition) => { + this.addCriterion(criterionDefinition); + }); + } + + updateCriteria(criterionDefinitions) { + this.destroyCriteria(); + this.createCriteria(criterionDefinitions); + } + + /** + * adds criterion to the condition. + */ + addCriterion(criterionDefinition) { + let criterionDefinitionWithId = this.generateCriterion(criterionDefinition || null); + let criterion = new TelemetryCriterion(criterionDefinitionWithId, this.openmct); + criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); + criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); + if (!this.criteria) { + this.criteria = []; + } + this.criteria.push(criterion); + //Do we need this here? + this.handleConditionUpdated(); + return criterionDefinitionWithId.id; + } + + findCriterion(id) { + let criterion; + + for (let i=0, ii=this.criteria.length; i < ii; i ++) { + if (this.criteria[i].id === id) { + criterion = { + item: this.criteria[i], + index: i + } + } + } + + return criterion; + } + + updateCriterion(id, criterionDefinition) { + let found = this.findCriterion(id); + if (found) { + const newCriterionDefinition = this.generateCriterion(criterionDefinition); + let newCriterion = new TelemetryCriterion(newCriterionDefinition, this.openmct); + newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); + newCriterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); + + let criterion = found.item; + criterion.unsubscribe(); + criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); + criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); + this.criteria.splice(found.index, 1, newCriterion); + if (this.criteriaResults[criterion.id] !== undefined) { + delete this.criteriaResults[criterion.id]; + } + this.handleConditionUpdated(); + } + } + + removeCriterion(id) { + if (this.destroyCriterion(id)) { + this.handleConditionUpdated(); + } + } + + destroyCriterion(id) { + let found = this.findCriterion(id); + if (found) { + let criterion = found.item; + criterion.destroy(); + criterion.off('criterionUpdated', (result) => { + this.handleCriterionUpdated(id, result); + }); + this.criteria.splice(found.index, 1); + if (this.criteriaResults[criterion.id] !== undefined) { + delete this.criteriaResults[criterion.id]; + } + return true; + } + return false; + } + + handleCriterionUpdated(criterion) { + let found = this.findCriterion(criterion.id); + if (found) { + this.criteria[found.index] = criterion.data; + //Most likely don't need this. + this.subscribe(); + this.emitEvent('conditionUpdated', { + trigger: this.trigger, + criteria: this.criteria + }); + } + } + + handleCriterionResult(eventData) { + let id = eventData.id; + let result = eventData.data.result; + let found = this.findCriterion(id); + if (found) { + this.criteriaResults[id] = result; + } + this.handleConditionUpdated(); + } + + subscribe() { + this.criteria.forEach((criterion) => { + criterion.subscribe(); + }) + } + + handleConditionUpdated() { + // trigger an updated event so that consumers can react accordingly + this.evaluate(); + this.emitEvent('conditionResultUpdated', {result: this.result}); + } + + getCriteria() { + return this.criteria; + } + + destroyCriteria() { + let success = true; + //looping through the array backwards since destroyCriterion modifies the criteria array + for (let i=this.criteria.length-1; i >= 0; i--) { + success = success && this.destroyCriterion(this.criteria[i].id); + } + return success; + } + + //TODO: implement as part of the evaluator class task. + evaluate() { + this.result = computeCondition(this.criteriaResults, this.trigger === TRIGGER.ALL); + } + + emitEvent(eventName, data) { + this.emit(eventName, { + id: this.id, + data: data + }); + } + + destroy() { + if (typeof this.stopObservingForChanges === 'function') { + this.stopObservingForChanges(); + } + this.destroyCriteria(); + } +} diff --git a/src/plugins/condition/ConditionSpec.js b/src/plugins/condition/ConditionSpec.js new file mode 100644 index 0000000000..18334bcada --- /dev/null +++ b/src/plugins/condition/ConditionSpec.js @@ -0,0 +1,121 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import Condition from "./Condition"; +import {TRIGGER} from "./utils/constants"; +import TelemetryCriterion from "./criterion/TelemetryCriterion"; + +let openmct = {}, + mockListener, + testConditionDefinition, + testTelemetryObject, + conditionObj; + +describe("The condition", function () { + + beforeEach (() => { + mockListener = jasmine.createSpy('listener'); + testTelemetryObject = { + identifier:{ namespace: "", key: "test-object"}, + type: "test-object", + name: "Test Object", + telemetry: { + values: [{ + key: "some-key", + name: "Some attribute", + hints: { + domain: 1 + } + }, { + key: "some-other-key", + name: "Another attribute", + hints: { + range: 1 + } + }] + } + }; + openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']); + openmct.objects.get.and.returnValue(new Promise(function (resolve, reject) { + resolve(testTelemetryObject); + })); openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key); + openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', 'subscribe', 'getMetadata']); + openmct.telemetry.isTelemetryObject.and.returnValue(true); + openmct.telemetry.subscribe.and.returnValue(function () {}); + openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values); + + testConditionDefinition = { + definition: { + trigger: TRIGGER.ANY, + criteria: [ + { + operation: 'equalTo', + input: false, + metaDataKey: 'value', + key: testTelemetryObject.identifier + } + ] + } + }; + + conditionObj = new Condition( + testConditionDefinition, + openmct + ); + + conditionObj.on('conditionUpdated', mockListener); + + }); + + it("generates criteria with an id", function () { + const testCriterion = testConditionDefinition.definition.criteria[0]; + let criterion = conditionObj.generateCriterion(testCriterion); + expect(criterion.id).toBeDefined(); + expect(criterion.operation).toEqual(testCriterion.operation); + expect(criterion.input).toEqual(testCriterion.input); + expect(criterion.metaDataKey).toEqual(testCriterion.metaDataKey); + expect(criterion.key).toEqual(testCriterion.key); + }); + + it("initializes with an id", function () { + expect(conditionObj.id).toBeDefined(); + }); + + it("initializes with criteria from the condition definition", function () { + expect(conditionObj.criteria.length).toEqual(1); + let criterion = conditionObj.criteria[0]; + expect(criterion instanceof TelemetryCriterion).toBeTrue(); + expect(criterion.operator).toEqual(testConditionDefinition.definition.criteria[0].operator); + expect(criterion.input).toEqual(testConditionDefinition.definition.criteria[0].input); + expect(criterion.metaDataKey).toEqual(testConditionDefinition.definition.criteria[0].metaDataKey); + }); + + it("initializes with the trigger from the condition definition", function () { + expect(conditionObj.trigger).toEqual(testConditionDefinition.definition.trigger); + }); + + it("destroys all criteria for a condition", function () { + const result = conditionObj.destroyCriteria(); + expect(result).toBeTrue(); + expect(conditionObj.criteria.length).toEqual(0); + }); +}); diff --git a/src/plugins/condition/StyleRuleManager.js b/src/plugins/condition/StyleRuleManager.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugins/condition/components/Condition.vue b/src/plugins/condition/components/Condition.vue index f8e527cf7b..1e37bc6479 100644 --- a/src/plugins/condition/components/Condition.vue +++ b/src/plugins/condition/components/Condition.vue @@ -1,53 +1,66 @@ diff --git a/src/plugins/condition/components/ConditionCollection.vue b/src/plugins/condition/components/ConditionCollection.vue index ddc1ff3ad5..778482dd6b 100644 --- a/src/plugins/condition/components/ConditionCollection.vue +++ b/src/plugins/condition/components/ConditionCollection.vue @@ -1,3 +1,25 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +