diff --git a/.webpack/webpack.common.mjs b/.webpack/webpack.common.mjs index 7290ce999e5..f4862099ef9 100644 --- a/.webpack/webpack.common.mjs +++ b/.webpack/webpack.common.mjs @@ -50,7 +50,8 @@ const config = { inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js', espressoTheme: './src/plugins/themes/espresso-theme.scss', snowTheme: './src/plugins/themes/snow-theme.scss', - darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss' + darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss', + historicalTelemetryWorker: './src/plugins/condition/historicalTelemetryWorker.js', }, output: { globalObject: 'this', diff --git a/e2e/appActions.js b/e2e/appActions.js index 105854b4cc7..c6fd8b98ade 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -127,6 +127,22 @@ async function createDomainObjectWithDefaults( }; } +/** + * Retrieves the properties of an OpenMCT domain object by its identifier. + * + * @param {import('@playwright/test').Page} page - The Playwright page object. + * @param {string | identifier - The identifier or UUID of the domain object. + * @returns {Promise} An object containing the properties of the domain object. + */ +async function getDomainObject(page, identifier) { + const domainObject = await page.evaluate(async (objIdentifier) => { + const object = await window.openmct.objects.get(objIdentifier); + return object; + }, identifier); + + return domainObject; +} + /** * Generate a notification with the given options. * @param {import('@playwright/test').Page} page @@ -716,6 +732,7 @@ export { createStableStateTelemetry, expandEntireTree, getCanvasPixels, + getDomainObject, linkParameterToObject, navigateToObjectWithFixedTimeBounds, navigateToObjectWithRealTime, diff --git a/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js index 6b76088b395..9ebe193431b 100644 --- a/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js +++ b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js @@ -29,7 +29,8 @@ import { fileURLToPath } from 'url'; import { createDomainObjectWithDefaults, - createExampleTelemetryObject + createExampleTelemetryObject, + getDomainObject } from '../../../../appActions.js'; import { expect, test } from '../../../../pluginFixtures.js'; @@ -468,6 +469,34 @@ test.describe('Basic Condition Set Use', () => { description: 'https://github.com/nasa/openmct/issues/7484' }); }); + + test('should toggle shouldFetchHistorical property in inspector', async ({ page }) => { + await page.goto(conditionSet.url); + await page.getByLabel('Edit Object').click(); + await page.getByRole('tab', { name: 'Config' }).click(); + let toggleSwitch = page.getByLabel('condition-historical-toggle'); + const initialState = await toggleSwitch.isChecked(); + expect(initialState).toBe(false); + + await toggleSwitch.click(); + let toggledState = await toggleSwitch.isChecked(); + expect(toggledState).toBe(true); + await page.click('button[title="Save"]'); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + let conditionSetObject = await getDomainObject(page, conditionSet.uuid); + expect(conditionSetObject.configuration.shouldFetchHistorical).toBe(true); + + await page.getByLabel('Edit Object').click(); + await page.getByRole('tab', { name: 'Config' }).click(); + toggleSwitch = page.getByLabel('condition-historical-toggle'); + await toggleSwitch.click(); + toggledState = await toggleSwitch.isChecked(); + expect(toggledState).toBe(false); + await page.click('button[title="Save"]'); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + conditionSetObject = await getDomainObject(page, conditionSet.uuid); + expect(conditionSetObject.configuration.shouldFetchHistorical).toBe(false); + }); }); test.describe('Condition Set Composition', () => { diff --git a/karma.conf.cjs b/karma.conf.cjs index e96836acbbc..43046624027 100644 --- a/karma.conf.cjs +++ b/karma.conf.cjs @@ -66,6 +66,10 @@ module.exports = async (config) => { { pattern: 'dist/generatorWorker.js*', included: false + }, + { + pattern: 'dist/historicalTelemetryWorker.js*', + included: false } ], port: 9876, diff --git a/src/plugins/condition/ConditionInspectorViewProvider.js b/src/plugins/condition/ConditionInspectorViewProvider.js new file mode 100644 index 00000000000..5f7f0dbec08 --- /dev/null +++ b/src/plugins/condition/ConditionInspectorViewProvider.js @@ -0,0 +1,73 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, 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 mount from 'utils/mount'; + +import ConditionConfigView from './components/ConditionInspectorConfigView.vue'; + +export default function ConditionInspectorView(openmct) { + return { + key: 'condition-config', + name: 'Config', + canView: function (selection) { + return selection.length > 0 && selection[0][0].context.item.type === 'conditionSet'; + }, + view: function (selection) { + let _destroy = null; + const domainObject = selection[0][0].context.item; + + return { + show: function (element) { + const { destroy } = mount( + { + el: element, + components: { + ConditionConfigView: ConditionConfigView + }, + provide: { + openmct, + domainObject + }, + template: '' + }, + { + app: openmct.app, + element + } + ); + _destroy = destroy; + }, + showTab: function (isEditing) { + return isEditing; + }, + priority: function () { + return 1; + }, + destroy: function () { + if (_destroy) { + _destroy(); + } + } + }; + } + }; +} diff --git a/src/plugins/condition/ConditionManager.js b/src/plugins/condition/ConditionManager.js index 34960b2f3c6..c2a75914e1f 100644 --- a/src/plugins/condition/ConditionManager.js +++ b/src/plugins/condition/ConditionManager.js @@ -24,6 +24,8 @@ import { EventEmitter } from 'eventemitter3'; import { v4 as uuid } from 'uuid'; import Condition from './Condition.js'; +import HistoricalTelemetryProvider from './HistoricalTelemetryProvider.js'; +import { TELEMETRY_VALUE } from './utils/constants.js'; import { getLatestTimestamp } from './utils/time.js'; export default class ConditionManager extends EventEmitter { @@ -52,6 +54,8 @@ export default class ConditionManager extends EventEmitter { applied: false }; this.initialize(); + this.telemetryBuffer = []; + this.isProcessing = false; } subscribeToTelemetry(telemetryObject) { @@ -310,6 +314,39 @@ export default class ConditionManager extends EventEmitter { this.persistConditions(); } + getCurrentCondition() { + const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; + let currentCondition = conditionCollection[conditionCollection.length - 1]; + + for (let i = 0; i < conditionCollection.length - 1; i++) { + const condition = this.findConditionById(conditionCollection[i].id); + if (condition.result) { + //first condition to be true wins + currentCondition = conditionCollection[i]; + break; + } + } + + return currentCondition; + } + + getHistoricalData(options) { + if (!this.conditionSetDomainObject.configuration.shouldFetchHistorical) { + return []; + } + let historicalTelemetry = new HistoricalTelemetryProvider( + this.openmct, + this.telemetryObjects, + this.conditions, + this.conditionSetDomainObject, + options + ); + const historicalData = historicalTelemetry.getHistoricalData(); + historicalTelemetry = null; + + return historicalData; + } + getCurrentConditionLAD(conditionResults) { const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; let currentCondition = conditionCollection[conditionCollection.length - 1]; @@ -365,14 +402,36 @@ export default class ConditionManager extends EventEmitter { } const currentCondition = this.getCurrentConditionLAD(conditionResults); + let output = currentCondition?.configuration?.output; + + if (output === TELEMETRY_VALUE) { + const { outputTelemetry, outputMetadata } = currentCondition.configuration; + const outputTelemetryObject = await this.openmct.objects.get(outputTelemetry); + const telemetryOptions = { + size: 1, + strategy: 'latest', + timeContext: this.openmct.time.getContextForView([]) + }; + const latestData = await this.openmct.telemetry.request( + outputTelemetryObject, + telemetryOptions + ); + if (latestData?.[0]?.[outputMetadata]) { + output = latestData?.[0]?.[outputMetadata]; + } + } + + let result = currentCondition?.isDefault ? false : conditionResults[currentCondition.id]; const currentOutput = { - output: currentCondition.configuration.output, - id: this.conditionSetDomainObject.identifier, conditionId: currentCondition.id, - ...latestTimestamp + id: this.conditionSetDomainObject.identifier, + output: output, + ...latestTimestamp, + result, + isDefault: currentCondition?.isDefault }; - return [currentOutput]; + return output !== undefined ? [currentOutput] : []; } isTelemetryUsed(endpoint) { @@ -407,8 +466,8 @@ export default class ConditionManager extends EventEmitter { this.#latestDataTable.set(normalizedDatum.id, normalizedDatum); if (this.shouldEvaluateNewTelemetry(currentTimestamp)) { - const matchingCondition = this.updateConditionResults(normalizedDatum.id); - this.updateCurrentCondition(timestamp, matchingCondition); + this.updateConditionResults(normalizedDatum); + this.updateCurrentCondition(timestamp, endpoint, datum); } } @@ -423,23 +482,80 @@ export default class ConditionManager extends EventEmitter { return matchingCondition; } - updateCurrentCondition(timestamp, matchingCondition) { - const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; - const defaultCondition = conditionCollection[conditionCollection.length - 1]; + emitConditionSetResult(currentCondition, timestamp, outputValue, result, isDefault) { + this.emit('conditionSetResultUpdated', { + conditionId: currentCondition.id, + id: this.conditionSetDomainObject.identifier, + output: outputValue, + ...timestamp, + result, + isDefault + }); + } - const currentCondition = matchingCondition || defaultCondition; + updateCurrentCondition(timestamp, telemetryObject, telemetryData) { + this.telemetryBuffer.push({ timestamp, telemetryObject, telemetryData }); - this.emit( - 'conditionSetResultUpdated', - Object.assign( - { - output: currentCondition.configuration.output, - id: this.conditionSetDomainObject.identifier, - conditionId: currentCondition.id - }, - timestamp - ) - ); + if (!this.isProcessing) { + this.processBuffer(); + } + } + + async processBuffer() { + this.isProcessing = true; + + while (this.telemetryBuffer.length > 0) { + const { timestamp, telemetryObject, telemetryData } = this.telemetryBuffer.shift(); + await this.processCondition(timestamp, telemetryObject, telemetryData); + } + + this.isProcessing = false; + } + + async processCondition(timestamp, telemetryObject, telemetryData) { + const currentCondition = this.getCurrentCondition(); + const conditionDetails = this.conditions.filter( + (condition) => condition.id === currentCondition.id + )?.[0]; + const conditionResult = currentCondition?.isDefault ? false : conditionDetails?.result; + let telemetryValue = currentCondition.configuration.output; + + if (telemetryValue !== undefined && currentCondition?.configuration?.outputTelemetry) { + const selectedOutputIdentifier = currentCondition?.configuration?.outputTelemetry; + const outputMetadata = currentCondition?.configuration?.outputMetadata; + const telemetryKeystring = this.openmct.objects.makeKeyString(telemetryObject.identifier); + + if (selectedOutputIdentifier === telemetryKeystring) { + telemetryValue = telemetryData[outputMetadata]; + } else { + const outputTelemetryObject = await this.openmct.objects.get(selectedOutputIdentifier); + const telemetryOptions = { + size: 1, + strategy: 'latest', + start: timestamp?.utc - 1000, + end: timestamp?.utc + 1000 + }; + const outputTelemetryData = await this.openmct.telemetry.request( + outputTelemetryObject, + telemetryOptions + ); + const outputTelemetryValue = + outputTelemetryData?.length > 0 ? outputTelemetryData.slice(-1)[0] : null; + if (outputTelemetryData.length && outputTelemetryValue?.[outputMetadata]) { + telemetryValue = outputTelemetryValue?.[outputMetadata]; + } else { + telemetryValue = undefined; + } + } + + this.emitConditionSetResult( + currentCondition, + timestamp, + telemetryValue, + conditionResult, + currentCondition?.isDefault + ); + } } getTestData(metadatum, identifier) { diff --git a/src/plugins/condition/ConditionSetMetadataProvider.js b/src/plugins/condition/ConditionSetMetadataProvider.js index 0d3190d49b2..dbaadfff12b 100644 --- a/src/plugins/condition/ConditionSetMetadataProvider.js +++ b/src/plugins/condition/ConditionSetMetadataProvider.js @@ -43,29 +43,43 @@ export default class ConditionSetMetadataProvider { } getMetadata(domainObject) { - const enumerations = domainObject.configuration.conditionCollection.map((condition, index) => { - return { - string: condition.configuration.output, - value: index - }; + const format = {}; + domainObject.configuration.conditionCollection.forEach((condition, index) => { + if (condition?.configuration?.valueMetadata?.enumerations) { + delete format.formatString; + format.format = 'enum'; + format.enumerations = condition?.configuration?.valueMetadata?.enumerations; + } }); + const resultEnum = [ + { + string: 'true', + value: true + }, + { + string: 'false', + value: false + } + ]; + return { values: this.getDomains().concat([ { - key: 'state', + key: 'value', source: 'output', - name: 'State', - format: 'enum', - enumerations: enumerations, + name: 'Value', + ...format, hints: { range: 1 } }, { - key: 'output', - name: 'Value', - format: 'string', + key: 'result', + source: 'result', + name: 'Result', + format: 'enum', + enumerations: resultEnum, hints: { range: 2 } diff --git a/src/plugins/condition/ConditionSetTelemetryProvider.js b/src/plugins/condition/ConditionSetTelemetryProvider.js index 38e7d4cb838..df9ddc53d68 100644 --- a/src/plugins/condition/ConditionSetTelemetryProvider.js +++ b/src/plugins/condition/ConditionSetTelemetryProvider.js @@ -42,8 +42,10 @@ export default class ConditionSetTelemetryProvider { async request(domainObject, options) { let conditionManager = this.getConditionManager(domainObject); + const formattedHistoricalData = await conditionManager.getHistoricalData(options); let latestOutput = await conditionManager.requestLADConditionSetOutput(options); - return latestOutput; + + return [...formattedHistoricalData, ...latestOutput]; } subscribe(domainObject, callback) { diff --git a/src/plugins/condition/HistoricalTelemetryProvider.js b/src/plugins/condition/HistoricalTelemetryProvider.js new file mode 100644 index 00000000000..eb832ae2077 --- /dev/null +++ b/src/plugins/condition/HistoricalTelemetryProvider.js @@ -0,0 +1,281 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +export default class HistoricalTelemetryProvider { + constructor(openmct, telemetryObjects, conditions, conditionSetDomainObject, options) { + this.openmct = openmct; + this.telemetryObjects = telemetryObjects; + this.bounds = { start: null, end: null }; + this.telemetryList = []; + this.conditions = conditions; + this.conditionSetDomainObject = conditionSetDomainObject; + this.historicalTelemetryPoolMap = new Map(); + this.historicalTelemetryDateMap = new Map(); + this.index = 0; + this.options = options; + } + + setTimeBounds(bounds) { + this.bounds = bounds; + } + + async refreshHistoricalTelemetry(domainObject, identifier) { + if (!domainObject && identifier) { + domainObject = await this.openmct.objects.get(identifier); + } + const id = this.openmct.objects.makeKeyString(domainObject.identifier); + const telemetryOptions = { ...this.bounds, ...this.options }; + const historicalTelemetry = await this.openmct.telemetry.request( + domainObject, + telemetryOptions + ); + this.historicalTelemetryPoolMap.set(id, { domainObject, historicalTelemetry }); + return { domainObject, historicalTelemetry }; + } + + evaluateCondition(historicalDateMap, timestamp, condition, conditionCollectionMap) { + const telemetryData = historicalDateMap.get(timestamp); + const conditionConfiguration = conditionCollectionMap.get(condition.id)?.configuration; + const { outputTelemetry, outputMetadata } = conditionConfiguration; + let output = outputTelemetry || conditionConfiguration?.output ? {} : undefined; + + if (outputTelemetry) { + const outputTelemetryID = this.openmct.objects.makeKeyString(outputTelemetry); + const outputTelemetryData = telemetryData.get(outputTelemetryID); + output.condition = condition; + output.telemetry = outputTelemetryData; + output.value = outputTelemetryData?.[outputMetadata]; + } else if (conditionConfiguration?.output) { + output.condition = condition; + output.telemetry = null; + output.value = conditionConfiguration?.output; + } + + return output; + } + + async getAllTelemetries(conditionCollection) { + const conditionCollectionMap = new Map(); + const inputTelemetries = []; + const outputTelemetries = []; + const historicalTelemetryPoolPromises = []; + + conditionCollection.forEach((condition, index) => { + const { criteria, outputTelemetry } = condition.configuration; + const inputTelemetry = criteria?.[0]?.telemetry; + conditionCollectionMap.set(condition?.id, condition); + if (inputTelemetry) { + const inputTelemetryId = this.openmct.objects.makeKeyString(inputTelemetry); + if (![...inputTelemetries, ...outputTelemetries].includes(inputTelemetryId)) { + historicalTelemetryPoolPromises.push( + this.refreshHistoricalTelemetry(null, inputTelemetry) + ); + } + inputTelemetries.push(inputTelemetryId); + } else { + inputTelemetries.push(null); + } + if (outputTelemetry) { + if (![...inputTelemetries, ...outputTelemetries].includes(outputTelemetry)) { + historicalTelemetryPoolPromises.push( + this.refreshHistoricalTelemetry(null, outputTelemetry) + ); + } + outputTelemetries.push(outputTelemetry); + } else { + outputTelemetries.push(null); + } + }); + + const historicalTelemetriesPool = await Promise.all(historicalTelemetryPoolPromises); + return { + historicalTelemetriesPool, + inputTelemetries, + outputTelemetries, + conditionCollectionMap + }; + } + + sortTelemetriesByDate(historicalTelemetriesPool) { + const historicalTelemetryDateMap = new Map(); + historicalTelemetriesPool.forEach((historicalTelemetryList) => { + const { historicalTelemetry, domainObject } = historicalTelemetryList; + const { identifier } = domainObject; + const telemetryIdentifier = this.openmct.objects.makeKeyString(identifier); + historicalTelemetry.forEach((historicalTelemetryItem) => { + if (!historicalTelemetryDateMap.get(historicalTelemetryItem.utc)) { + const telemetryMap = new Map(); + telemetryMap.set(telemetryIdentifier, historicalTelemetryItem); + historicalTelemetryDateMap.set(historicalTelemetryItem.utc, telemetryMap); + } else { + const telemetryMap = historicalTelemetryDateMap.get(historicalTelemetryItem.utc); + telemetryMap.set(telemetryIdentifier, historicalTelemetryItem); + historicalTelemetryDateMap.set(historicalTelemetryItem.utc, telemetryMap); + } + }); + }); + return historicalTelemetryDateMap; + } + + async sortTelemetriesInWorker(historicalTelemetriesPool) { + const sortedTelemetries = await this.startWorker('sortTelemetries', { + historicalTelemetriesPool + }); + return sortedTelemetries; + } + + async startWorker(type, data) { + // eslint-disable-next-line no-undef + const workerUrl = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}historicalTelemetryWorker.js`; + const worker = new Worker(workerUrl); + + try { + const result = await this.getDataFromWorker(worker, type, data); + return result; + } catch (error) { + console.error('Error in condition manager getHistoricalData:', error); + throw error; + } finally { + worker.terminate(); + } + } + + getDataFromWorker(worker, type, data) { + return new Promise((resolve, reject) => { + worker.onmessage = (e) => { + if (e.data.type === 'result') { + resolve(e.data.data); + } else if (e.data.type === 'error') { + reject(new Error(e.data.error)); + } + }; + + worker.onerror = (error) => { + reject(error); + }; + + worker.postMessage({ + type, + data + }); + }); + } + + evaluateConditionsByDate(historicalTelemetryDateMap, conditionCollectionMap) { + const outputTelemetryDateMap = new Map(); + + historicalTelemetryDateMap.forEach((historicalTelemetryMap, timestamp) => { + let isConditionValid = false; + + this.conditions.forEach((condition) => { + if (isConditionValid) { + return; + } + + const conditionCriteria = condition.criteria[0]; + let result; + + if (conditionCriteria?.telemetry) { + const conditionInputTelemetryId = this.openmct.objects.makeKeyString( + conditionCriteria.telemetry + ); + const inputTelemetry = historicalTelemetryMap.get(conditionInputTelemetryId); + result = conditionCriteria.computeResult(inputTelemetry); + } else if (!conditionCriteria) { + const conditionDetails = conditionCollectionMap.get(condition.id); + const { isDefault, outputTelemetry } = conditionDetails; + + if (isDefault && (outputTelemetry || condition.configuration.output)) { + const conditionOutput = this.evaluateCondition( + historicalTelemetryDateMap, + timestamp, + condition, + conditionCollectionMap + ); + conditionOutput.result = false; + conditionOutput.isDefault = true; + outputTelemetryDateMap.set(timestamp, conditionOutput); + } + } + + if (result === true) { + isConditionValid = true; + const conditionOutput = this.evaluateCondition( + historicalTelemetryDateMap, + timestamp, + condition, + conditionCollectionMap + ); + conditionOutput.result = true; + outputTelemetryDateMap.set(timestamp, conditionOutput); + } + }); + }); + + return outputTelemetryDateMap; + } + + async getHistoricalInputsByDate() { + const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; + const { historicalTelemetriesPool, conditionCollectionMap } = + await this.getAllTelemetries(conditionCollection); + const historicalTelemetryDateMap = + await this.sortTelemetriesInWorker(historicalTelemetriesPool); + const outputTelemetryDateMap = this.evaluateConditionsByDate( + historicalTelemetryDateMap, + conditionCollectionMap + ); + + return outputTelemetryDateMap; + } + + addItemToHistoricalTelemetryMap(telemetryMap, item, type, index) { + if (type === 'input') { + telemetryMap.set(); + } + } + + async getHistoricalData() { + this.setTimeBounds(this.openmct.time.getBounds()); + const outputTelemetryMap = await this.getHistoricalInputsByDate(); + const formattedOutputTelemetry = this.formatOutputData(outputTelemetryMap); + + return formattedOutputTelemetry; + } + + formatOutputData(outputTelemetryMap) { + const outputTelemetryList = []; + const domainObject = this.conditionSetDomainObject; + outputTelemetryMap.forEach((outputMetadata, timestamp) => { + const { condition, value, result, isDefault } = outputMetadata; + outputTelemetryList.push({ + conditionId: condition.id, + id: domainObject.identifier, + output: value, + utc: timestamp, + result, + isDefault + }); + }); + return outputTelemetryList; + } +} diff --git a/src/plugins/condition/components/ConditionCollection.vue b/src/plugins/condition/components/ConditionCollection.vue index 84b5aff0084..146955002fb 100644 --- a/src/plugins/condition/components/ConditionCollection.vue +++ b/src/plugins/condition/components/ConditionCollection.vue @@ -235,10 +235,11 @@ export default { return arr; }, - addTelemetryObject(domainObject) { + async addTelemetryObject(domainObject) { const keyString = this.openmct.objects.makeKeyString(domainObject.identifier); + const telemetryPath = await this.getFullTelemetryPath(domainObject); - this.telemetryObjs.push(domainObject); + this.telemetryObjs.push({ ...domainObject, path: telemetryPath }); this.$emit('telemetry-updated', this.telemetryObjs); this.subscribeToStaleness(domainObject, (stalenessResponse) => { @@ -248,6 +249,19 @@ export default { }); }); }, + async getFullTelemetryPath(telemetry) { + const keyString = this.openmct.objects.makeKeyString(telemetry.identifier); + const originalPathObjects = await this.openmct.objects.getOriginalPath(keyString, []); + + const telemetryPath = originalPathObjects.reverse().map((pathObject) => { + if (pathObject.type !== 'root') { + return pathObject.name; + } + return undefined; + }); + + return telemetryPath.join('/'); + }, removeTelemetryObject(identifier) { const keyString = this.openmct.objects.makeKeyString(identifier); const index = this.telemetryObjs.findIndex((obj) => { diff --git a/src/plugins/condition/components/ConditionInspectorConfigView.vue b/src/plugins/condition/components/ConditionInspectorConfigView.vue new file mode 100644 index 00000000000..02c0539ec0b --- /dev/null +++ b/src/plugins/condition/components/ConditionInspectorConfigView.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/plugins/condition/components/ConditionItem.vue b/src/plugins/condition/components/ConditionItem.vue index 9341c42b6fe..ffcc36579b9 100644 --- a/src/plugins/condition/components/ConditionItem.vue +++ b/src/plugins/condition/components/ConditionItem.vue @@ -101,13 +101,13 @@ @change="setOutputValue" > + + + + + + -
Match @@ -185,7 +218,12 @@ {{ condition.configuration.name }} - Output: {{ condition.configuration.output }} + + Output: + {{ + condition.configuration.output === undefined ? 'none' : condition.configuration.output + }} +
@@ -199,6 +237,7 @@ import { v4 as uuid } from 'uuid'; import { TRIGGER, TRIGGER_LABEL } from '@/plugins/condition/utils/constants'; +import { TELEMETRY_VALUE } from '../utils/constants.js'; import ConditionDescription from './ConditionDescription.vue'; import Criterion from './CriterionItem.vue'; @@ -254,10 +293,13 @@ export default { expanded: true, trigger: 'all', selectedOutputSelection: '', - outputOptions: ['false', 'true', 'string'], + telemetryValueString: TELEMETRY_VALUE, + outputOptions: ['none', 'false', 'true', 'string', TELEMETRY_VALUE], criterionIndex: 0, draggingOver: false, - isDefault: this.condition.isDefault + isDefault: this.condition.isDefault, + telemetryMetadataOptions: {}, + telemetryFormats: new Map() }; }, computed: { @@ -302,32 +344,63 @@ export default { return false; } }, + watch: { + condition: { + handler() { + const config = this.condition?.configuration; + if (config?.output !== TELEMETRY_VALUE) { + config.outputTelemetry = null; + config.outputMetadata = null; + } + }, + deep: true + }, + telemetry: { + handler() { + this.initializeMetadata(); + }, + deep: true + } + }, unmounted() { this.destroy(); }, mounted() { this.setOutputSelection(); + this.initializeMetadata(); }, methods: { setOutputSelection() { let conditionOutput = this.condition.configuration.output; - if (conditionOutput) { - if (conditionOutput !== 'false' && conditionOutput !== 'true') { - this.selectedOutputSelection = 'string'; - } else { - this.selectedOutputSelection = conditionOutput; - } + if (conditionOutput === undefined) { + this.selectedOutputSelection = 'none'; + } else if (['false', 'true', TELEMETRY_VALUE].includes(conditionOutput)) { + this.selectedOutputSelection = conditionOutput; + } else { + this.selectedOutputSelection = 'string'; } }, setOutputValue() { if (this.selectedOutputSelection === 'string') { this.condition.configuration.output = ''; + } else if (this.selectedOutputSelection === 'none') { + this.condition.configuration.output = undefined; } else { this.condition.configuration.output = this.selectedOutputSelection; } this.persist(); }, + getOutputMetadata() { + const config = this.condition.configuration; + let valueMetadata; + if (config?.outputTelemetry && config?.outputMetadata) { + valueMetadata = this.telemetryFormats.get( + `${config?.outputTelemetry}_${config?.outputMetadata}` + ); + } + return valueMetadata; + }, addCriteria() { const criteriaObject = { id: uuid(), @@ -410,12 +483,37 @@ export default { this.persist(); }, persist() { + const valueMetadata = this.getOutputMetadata(); + if (valueMetadata) { + this.condition.configuration.valueMetadata = valueMetadata; + } this.$emit('update-condition', { condition: this.condition }); }, initCap(str) { return str.charAt(0).toUpperCase() + str.slice(1); + }, + initializeMetadata() { + this.telemetry.forEach((telemetryObject) => { + const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); + let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); + if (telemetryMetadata) { + this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice(); + telemetryMetadata.values().forEach((telemetryValue) => { + this.telemetryFormats.set(`${id}_${telemetryValue.key}`, telemetryValue); + }); + } else { + this.telemetryMetadataOptions[id] = []; + } + }); + }, + getId(identifier) { + if (identifier) { + return this.openmct.objects.makeKeyString(identifier); + } + + return []; } } }; diff --git a/src/plugins/condition/components/CriterionItem.vue b/src/plugins/condition/components/CriterionItem.vue index a632d675a1a..4d0b94e094d 100644 --- a/src/plugins/condition/components/CriterionItem.vue +++ b/src/plugins/condition/components/CriterionItem.vue @@ -40,7 +40,7 @@ :key="telemetryOption.identifier.key" :value="telemetryOption.identifier" > - {{ telemetryOption.name }} + {{ telemetryOption.path }} diff --git a/src/plugins/condition/components/TestData.vue b/src/plugins/condition/components/TestData.vue index 5e3e108cf40..f81a81cec57 100644 --- a/src/plugins/condition/components/TestData.vue +++ b/src/plugins/condition/components/TestData.vue @@ -63,7 +63,7 @@ :key="index" :value="telemetryOption.identifier" > - {{ telemetryOption.name }} + {{ telemetryOption.path || telemetryOption.name }} @@ -147,7 +147,8 @@ export default { expanded: true, isApplied: false, testInputs: [], - telemetryMetadataOptions: {} + telemetryMetadataOptions: {}, + telemetryPaths: [] }; }, watch: { @@ -244,6 +245,22 @@ export default { applied: this.isApplied, conditionTestInputs: this.testInputs }); + }, + async getFullTelemetryPath(telemetry) { + const keyStringForObject = this.openmct.objects.makeKeyString(telemetry.identifier); + const originalPathObjects = await this.openmct.objects.getOriginalPath( + keyStringForObject, + [] + ); + + const telemetryPath = originalPathObjects.reverse().map((pathObject) => { + if (pathObject.type !== 'root') { + return pathObject.name; + } + return undefined; + }); + + return telemetryPath.join('/'); } } }; diff --git a/src/plugins/condition/historicalTelemetryWorker.js b/src/plugins/condition/historicalTelemetryWorker.js new file mode 100644 index 00000000000..71b6dda2434 --- /dev/null +++ b/src/plugins/condition/historicalTelemetryWorker.js @@ -0,0 +1,61 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, 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 { makeKeyString } from '../../api/objects/object-utils.js'; + +(function () { + function sortTelemetriesByDate(historicalTelemetriesPool) { + const historicalTelemetryDateMap = new Map(); + historicalTelemetriesPool.forEach((historicalTelemetryList) => { + const { historicalTelemetry, domainObject } = historicalTelemetryList; + const { identifier } = domainObject; + const telemetryIdentifier = makeKeyString(identifier); + historicalTelemetry.forEach((historicalTelemetryItem) => { + let telemetryTimestamp = historicalTelemetryItem.utc; + if (historicalTelemetryItem.timestamp) { + telemetryTimestamp = new Date(historicalTelemetryItem.timestamp)?.getTime(); + } + if (!historicalTelemetryDateMap.get(telemetryTimestamp)) { + const telemetryMap = new Map(); + telemetryMap.set(telemetryIdentifier, historicalTelemetryItem); + historicalTelemetryDateMap.set(telemetryTimestamp, telemetryMap); + } else { + const telemetryMap = historicalTelemetryDateMap.get(telemetryTimestamp); + telemetryMap.set(telemetryIdentifier, historicalTelemetryItem); + historicalTelemetryDateMap.set(telemetryTimestamp, telemetryMap); + } + }); + }); + return historicalTelemetryDateMap; + } + + self.onmessage = function (e) { + const { type, data } = e.data; + + if (type === 'sortTelemetries') { + const sortedTelemetries = sortTelemetriesByDate(data.historicalTelemetriesPool); + self.postMessage({ type: 'result', data: sortedTelemetries }); + } else { + self.postMessage({ type: 'error', error: 'Unknown message type' }); + } + }; +})(); diff --git a/src/plugins/condition/plugin.js b/src/plugins/condition/plugin.js index 365386ef68d..5540746206c 100644 --- a/src/plugins/condition/plugin.js +++ b/src/plugins/condition/plugin.js @@ -21,6 +21,7 @@ *****************************************************************************/ import { v4 as uuid } from 'uuid'; +import ConditionInspectorViewProvider from './ConditionInspectorViewProvider.js'; import ConditionSetCompositionPolicy from './ConditionSetCompositionPolicy.js'; import ConditionSetMetadataProvider from './ConditionSetMetadataProvider.js'; import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider.js'; @@ -37,6 +38,7 @@ export default function ConditionPlugin() { cssClass: 'icon-conditional', initialize: function (domainObject) { domainObject.configuration = { + shouldFetchHistorical: false, conditionTestData: [], conditionCollection: [ { @@ -61,5 +63,6 @@ export default function ConditionPlugin() { openmct.telemetry.addProvider(new ConditionSetMetadataProvider(openmct)); openmct.telemetry.addProvider(new ConditionSetTelemetryProvider(openmct)); openmct.objectViews.addProvider(new ConditionSetViewProvider(openmct)); + openmct.inspectorViews.addProvider(new ConditionInspectorViewProvider(openmct)); }; } diff --git a/src/plugins/condition/utils/constants.js b/src/plugins/condition/utils/constants.js index 1609dc80fc5..0b53cccc343 100644 --- a/src/plugins/condition/utils/constants.js +++ b/src/plugins/condition/utils/constants.js @@ -62,3 +62,5 @@ export const ERROR = { export const IS_OLD_KEY = 'isStale'; export const IS_STALE_KEY = 'isStale.new'; + +export const TELEMETRY_VALUE = 'telemetry value'; diff --git a/src/plugins/plot/stackedPlot/StackedPlotItem.vue b/src/plugins/plot/stackedPlot/StackedPlotItem.vue index 1541b8c9806..4a9c7e28ada 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotItem.vue +++ b/src/plugins/plot/stackedPlot/StackedPlotItem.vue @@ -243,6 +243,7 @@ export default { domainObject: { ...this.childObject, configuration: { + ...this.childObject.configuration, series: [ { identifier: this.childObject.identifier, diff --git a/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js b/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js index 0caa57c68ba..011123a1473 100644 --- a/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js +++ b/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js @@ -139,12 +139,14 @@ export default { styleObj.isStyleInvisible = null; } - Object.entries(styleObj).forEach(([key, value]) => { - if (typeof value !== 'string' || !value.includes('__no_value')) { - elemToStyle.style[key] = value; - } else { - elemToStyle.style[key] = ''; // remove the property - } + requestAnimationFrame(() => { + Object.entries(styleObj).forEach(([key, value]) => { + if (typeof value !== 'string' || !value.includes('__no_value')) { + elemToStyle.style[key] = value; + } else { + elemToStyle.style[key] = ''; // remove the property + } + }); }); } } diff --git a/src/plugins/plot/tickUtils.js b/src/plugins/plot/tickUtils.js index fff48cb546b..80e19b5dce2 100644 --- a/src/plugins/plot/tickUtils.js +++ b/src/plugins/plot/tickUtils.js @@ -94,7 +94,7 @@ export function ticks(start, stop, count) { } export function commonPrefix(a, b) { - const maxLen = Math.min(a.length, b.length); + const maxLen = Math.min(a.length, b?.length); let breakpoint = 0; for (let i = 0; i < maxLen; i++) { if (a[i] !== b[i]) { @@ -110,7 +110,7 @@ export function commonPrefix(a, b) { } export function commonSuffix(a, b) { - const maxLen = Math.min(a.length, b.length); + const maxLen = Math.min(a.length, b?.length); let breakpoint = 0; for (let i = 0; i <= maxLen; i++) { if (a[a.length - i] !== b[b.length - i]) {