mirror of
https://github.com/nasa/openmct.git
synced 2025-03-22 03:55:31 +00:00
Merged in conditional stuff and whatnot
This commit is contained in:
commit
cbc04c3fa9
@ -14,7 +14,8 @@ const config = {
|
||||
__OPENMCT_VERSION__: 'readonly',
|
||||
__OPENMCT_BUILD_DATE__: 'readonly',
|
||||
__OPENMCT_REVISION__: 'readonly',
|
||||
__OPENMCT_BUILD_BRANCH__: 'readonly'
|
||||
__OPENMCT_BUILD_BRANCH__: 'readonly',
|
||||
__OPENMCT_ROOT_RELATIVE__: 'readonly'
|
||||
},
|
||||
plugins: ['prettier', 'unicorn', 'simple-import-sort'],
|
||||
extends: [
|
||||
|
@ -48,6 +48,7 @@ const config = {
|
||||
generatorWorker: './example/generator/generatorWorker.js',
|
||||
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
|
||||
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
|
||||
compsMathWorker: './src/plugins/comps/CompsMathWorker.js',
|
||||
espressoTheme: './src/plugins/themes/espresso-theme.scss',
|
||||
snowTheme: './src/plugins/themes/snow-theme.scss',
|
||||
darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss'
|
||||
@ -89,7 +90,8 @@ const config = {
|
||||
__OPENMCT_REVISION__: `'${gitRevision}'`,
|
||||
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,
|
||||
__VUE_OPTIONS_API__: true, // enable/disable Options API support, default: true
|
||||
__VUE_PROD_DEVTOOLS__: false // enable/disable devtools support in production, default: false
|
||||
__VUE_PROD_DEVTOOLS__: false, // enable/disable devtools support in production, default: false
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // enable/disable hydration mismatch details in production, default: false
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
|
@ -116,6 +116,22 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Object>} 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
|
||||
@ -705,6 +721,7 @@ export {
|
||||
createStableStateTelemetry,
|
||||
expandEntireTree,
|
||||
getCanvasPixels,
|
||||
getDomainObject,
|
||||
linkParameterToObject,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
navigateToObjectWithRealTime,
|
||||
|
111
e2e/tests/functional/plugins/comps/comps.e2e.spec.js
Normal file
111
e2e/tests/functional/plugins/comps/comps.e2e.spec.js
Normal file
@ -0,0 +1,111 @@
|
||||
/*****************************************************************************
|
||||
* 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 {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
setRealTimeMode
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Comps', () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Basic Functionality Works', async ({ page, openmctConfig }) => {
|
||||
const folder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
});
|
||||
|
||||
// Create the comps with defaults
|
||||
const comp = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Derived Telemetry',
|
||||
parent: folder.uuid
|
||||
});
|
||||
|
||||
const telemetryObject = await createExampleTelemetryObject(page, comp.uuid);
|
||||
|
||||
// Check that expressions can be edited
|
||||
await page.goto(comp.url);
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByPlaceholder('Enter an expression').fill('a*2');
|
||||
await page.getByText('Current Output').click();
|
||||
await expect(page.getByText('Expression valid')).toBeVisible();
|
||||
|
||||
// Check that expressions are marked invalid
|
||||
await page.getByLabel('Reference Name Input for a').fill('b');
|
||||
await page.getByText('Current Output').click();
|
||||
await expect(page.getByText('Invalid: Undefined symbol a')).toBeVisible();
|
||||
|
||||
// Check that test data works
|
||||
await page.getByPlaceholder('Enter an expression').fill('b*2');
|
||||
await page.getByLabel('Reference Test Value for b').fill('5');
|
||||
await page.getByLabel('Apply Test Data').click();
|
||||
let testValue = await page.getByLabel('Current Output Value').textContent();
|
||||
expect(testValue).toBe('10');
|
||||
|
||||
// Check that real data works
|
||||
await page.getByLabel('Apply Test Data').click();
|
||||
await setRealTimeMode(page);
|
||||
testValue = await page.getByLabel('Current Output Value').textContent();
|
||||
expect(testValue).not.toBe('10');
|
||||
// should be a number
|
||||
expect(parseFloat(testValue)).not.toBeNaN();
|
||||
|
||||
// Check that object path is correct
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
let objectPath = await page.getByLabel(`${telemetryObject.name} Object Path`).textContent();
|
||||
const expectedObjectPath = `/${myItemsFolderName}/${folder.name}/${comp.name}/${telemetryObject.name}`;
|
||||
expect(objectPath).toBe(expectedObjectPath);
|
||||
|
||||
// Check that the comps are saved
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
const expression = await page.getByLabel('Expression', { exact: true }).textContent();
|
||||
expect(expression).toBe('b*2');
|
||||
|
||||
// Check that object path is still correct after save
|
||||
objectPath = await page.getByLabel(`${telemetryObject.name} Object Path`).textContent();
|
||||
expect(objectPath).toBe(expectedObjectPath);
|
||||
|
||||
// Check that comps work after being saved
|
||||
testValue = await page.getByLabel('Current Output Value').textContent();
|
||||
expect(testValue).not.toBe('10');
|
||||
// should be a number
|
||||
expect(parseFloat(testValue)).not.toBeNaN();
|
||||
|
||||
// Check that output format can be changed
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page.getByLabel('Output Format').click();
|
||||
await page.getByLabel('Output Format').fill('%d');
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
// Ensure we only have one digit
|
||||
await expect(page.getByLabel('Current Output Value')).toHaveText(/^-1$|^0$|^1$/);
|
||||
// And that it persists post save
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await expect(page.getByLabel('Current Output Value')).toHaveText(/^-1$|^0$|^1$/);
|
||||
});
|
||||
});
|
@ -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', () => {
|
||||
|
@ -66,6 +66,10 @@ module.exports = async (config) => {
|
||||
{
|
||||
pattern: 'dist/generatorWorker.js*',
|
||||
included: false
|
||||
},
|
||||
{
|
||||
pattern: 'dist/historicalTelemetryWorker.js*',
|
||||
included: false
|
||||
}
|
||||
],
|
||||
port: 9876,
|
||||
|
101
package-lock.json
generated
101
package-lock.json
generated
@ -62,6 +62,7 @@
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "12.0.0",
|
||||
"mathjs": "13.1.1",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"moment": "2.30.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
@ -643,6 +644,18 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
|
||||
"integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
|
||||
@ -3088,6 +3101,19 @@
|
||||
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/complex.js": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz",
|
||||
"integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/infusion"
|
||||
}
|
||||
},
|
||||
"node_modules/compressible": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||
@ -4033,6 +4059,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
|
||||
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@ -4483,6 +4515,12 @@
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/escape-latex": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
|
||||
"integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
@ -5817,6 +5855,19 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"type": "patreon",
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
@ -7063,6 +7114,12 @@
|
||||
"integrity": "sha512-UrzO3fL7nnxlQXlvTynNAenL+21oUQRlzqQFsA2U11ryb4+NLOCOePZ70PTojEaUKhiFugh7dG0Q+I58xlPdWg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/javascript-natural-sort": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
|
||||
@ -7708,6 +7765,29 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mathjs": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-13.1.1.tgz",
|
||||
"integrity": "sha512-duaSAy7m4F+QtP1Dyv8MX2XuxcqpNDDlGly0SdVTCqpAmwdOFWilDdQKbLdo9RfD6IDNMOdo9tIsEaTXkconlQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.4",
|
||||
"complex.js": "^2.1.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"escape-latex": "^1.2.0",
|
||||
"fraction.js": "^4.3.7",
|
||||
"javascript-natural-sort": "^0.7.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"tiny-emitter": "^2.1.0",
|
||||
"typed-function": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"mathjs": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@ -9491,6 +9571,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/regex-parser": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz",
|
||||
@ -9847,6 +9933,12 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/seedrandom": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
|
||||
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/select-hose": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||
@ -10833,6 +10925,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-function": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",
|
||||
"integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/typedarray-to-buffer": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||
|
@ -65,6 +65,7 @@
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "12.0.0",
|
||||
"mathjs": "13.1.1",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"moment": "2.30.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
|
@ -306,6 +306,7 @@ export class MCT extends EventEmitter {
|
||||
this.install(this.plugins.UserIndicator());
|
||||
this.install(this.plugins.Gauge());
|
||||
this.install(this.plugins.InspectorViews());
|
||||
this.install(this.plugins.Comps());
|
||||
}
|
||||
/**
|
||||
* Set path to where assets are hosted. This should be the path to main.js.
|
||||
|
@ -760,6 +760,15 @@ export default class TelemetryAPI {
|
||||
return this.metadataCache.get(domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a domain object from the telemetry metadata cache.
|
||||
* @param {import('openmct').DomainObject} domainObject
|
||||
*/
|
||||
|
||||
removeMetadataFromCache(domainObject) {
|
||||
this.metadataCache.delete(domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value formatter for a given valueMetadata.
|
||||
*
|
||||
|
@ -86,14 +86,23 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
}
|
||||
this._setTimeSystem(this.options.timeContext.getTimeSystem());
|
||||
this.lastBounds = this.options.timeContext.getBounds();
|
||||
// prioritize passed options over time bounds
|
||||
if (this.options.start) {
|
||||
this.lastBounds.start = this.options.start;
|
||||
}
|
||||
if (this.options.end) {
|
||||
this.lastBounds.end = this.options.end;
|
||||
}
|
||||
this._watchBounds();
|
||||
this._watchTimeSystem();
|
||||
this._watchTimeModeChange();
|
||||
|
||||
this._requestHistoricalTelemetry();
|
||||
const historicalTelemetryLoadedPromise = this._requestHistoricalTelemetry();
|
||||
this._initiateSubscriptionTelemetry();
|
||||
|
||||
this.loaded = true;
|
||||
|
||||
return historicalTelemetryLoadedPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,6 +122,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
}
|
||||
|
||||
this.removeAllListeners();
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,7 +178,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this._processNewTelemetry(historicalData);
|
||||
this._processNewTelemetry(historicalData, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -182,10 +192,9 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
const options = { ...this.options };
|
||||
//We always want to receive all available values in telemetry tables.
|
||||
options.strategy = this.openmct.telemetry.SUBSCRIBE_STRATEGY.BATCH;
|
||||
|
||||
this.unsubscribe = this.openmct.telemetry.subscribe(
|
||||
this.domainObject,
|
||||
(datum) => this._processNewTelemetry(datum),
|
||||
(datum) => this._processNewTelemetry(datum, true),
|
||||
options
|
||||
);
|
||||
}
|
||||
@ -196,9 +205,10 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
*
|
||||
* @param {(Object|Object[])} telemetryData - telemetry data object or
|
||||
* array of telemetry data objects
|
||||
* @param {boolean} isSubscriptionData - `true` if the telemetry data is new subscription data,
|
||||
* @private
|
||||
*/
|
||||
_processNewTelemetry(telemetryData) {
|
||||
_processNewTelemetry(telemetryData, isSubscriptionData = false) {
|
||||
if (telemetryData === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -213,12 +223,19 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
let hasDataBeforeStartBound = false;
|
||||
let size = this.options.size;
|
||||
let enforceSize = size !== undefined && this.options.enforceSize;
|
||||
const boundsToUse = this.lastBounds;
|
||||
if (!isSubscriptionData && this.options.start) {
|
||||
boundsToUse.start = this.options.start;
|
||||
}
|
||||
if (!isSubscriptionData && this.options.end) {
|
||||
boundsToUse.end = this.options.end;
|
||||
}
|
||||
|
||||
// loop through, sort and dedupe
|
||||
for (let datum of data) {
|
||||
parsedValue = this.parseTime(datum);
|
||||
beforeStartOfBounds = parsedValue < this.lastBounds.start;
|
||||
afterEndOfBounds = parsedValue > this.lastBounds.end;
|
||||
beforeStartOfBounds = parsedValue < boundsToUse.start;
|
||||
afterEndOfBounds = parsedValue > boundsToUse.end;
|
||||
|
||||
if (
|
||||
!afterEndOfBounds &&
|
||||
@ -397,7 +414,10 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this.emit('add', added, [this.boundedTelemetry.length]);
|
||||
}
|
||||
} else {
|
||||
// user bounds change, reset
|
||||
// user bounds change, reset and remove initial requested bounds (we're using new bounds)
|
||||
delete this.options?.start;
|
||||
delete this.options?.end;
|
||||
this.lastBounds = bounds;
|
||||
this._reset();
|
||||
}
|
||||
}
|
||||
@ -477,9 +497,9 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this.boundedTelemetry = [];
|
||||
this.futureBuffer = [];
|
||||
|
||||
this.emit('clear');
|
||||
const telemetryLoadPromise = this._requestHistoricalTelemetry();
|
||||
|
||||
this._requestHistoricalTelemetry();
|
||||
this.emit('clear', telemetryLoadPromise);
|
||||
}
|
||||
|
||||
/**
|
||||
|
85
src/plugins/comps/CompsInspectorViewProvider.js
Normal file
85
src/plugins/comps/CompsInspectorViewProvider.js
Normal file
@ -0,0 +1,85 @@
|
||||
/*****************************************************************************
|
||||
* 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 CompsInspectorView from './components/CompsInspectorView.vue';
|
||||
|
||||
export default class ConditionSetViewProvider {
|
||||
constructor(openmct, compsManagerPool) {
|
||||
this.openmct = openmct;
|
||||
this.name = 'Config';
|
||||
this.key = 'comps-configuration';
|
||||
this.compsManagerPool = compsManagerPool;
|
||||
}
|
||||
|
||||
canView(selection) {
|
||||
if (selection.length !== 1 || selection[0].length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let object = selection[0][0].context.item;
|
||||
return object && object.type === 'comps';
|
||||
}
|
||||
|
||||
view(selection) {
|
||||
let _destroy = null;
|
||||
const domainObject = selection[0][0].context.item;
|
||||
const openmct = this.openmct;
|
||||
const compsManagerPool = this.compsManagerPool;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
const { destroy } = mount(
|
||||
{
|
||||
el: element,
|
||||
components: {
|
||||
CompsInspectorView: CompsInspectorView
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
compsManagerPool
|
||||
},
|
||||
template: '<comps-inspector-view></comps-inspector-view>'
|
||||
},
|
||||
{
|
||||
app: openmct.app,
|
||||
element
|
||||
}
|
||||
);
|
||||
_destroy = destroy;
|
||||
},
|
||||
showTab: function (isEditing) {
|
||||
return isEditing;
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
},
|
||||
destroy: function () {
|
||||
if (_destroy) {
|
||||
_destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
379
src/plugins/comps/CompsManager.js
Normal file
379
src/plugins/comps/CompsManager.js
Normal file
@ -0,0 +1,379 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
|
||||
export default class CompsManager extends EventEmitter {
|
||||
#openmct;
|
||||
#domainObject;
|
||||
#composition;
|
||||
#telemetryObjects = {};
|
||||
#telemetryCollections = {};
|
||||
#telemetryLoadedPromises = [];
|
||||
#telemetryOptions = {};
|
||||
#loaded = false;
|
||||
#compositionLoaded = false;
|
||||
#telemetryProcessors = {};
|
||||
#loadVersion = 0;
|
||||
#currentLoadPromise = null;
|
||||
|
||||
constructor(openmct, domainObject) {
|
||||
super();
|
||||
this.#openmct = openmct;
|
||||
this.#domainObject = domainObject;
|
||||
this.clearData = this.clearData.bind(this);
|
||||
}
|
||||
|
||||
#getNextAlphabeticalParameterName() {
|
||||
const parameters = this.#domainObject.configuration.comps.parameters;
|
||||
const existingNames = new Set(parameters.map((p) => p.name));
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
|
||||
let suffix = '';
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
for (let letter of alphabet) {
|
||||
const proposedName = letter + suffix;
|
||||
if (!existingNames.has(proposedName)) {
|
||||
return proposedName;
|
||||
}
|
||||
}
|
||||
// Increment suffix after exhausting the alphabet
|
||||
suffix = (parseInt(suffix, 10) || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
addParameter(telemetryObject) {
|
||||
const keyString = this.#openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
const metaData = this.#openmct.telemetry.getMetadata(telemetryObject);
|
||||
const timeSystem = this.#openmct.time.getTimeSystem();
|
||||
const domains = metaData?.valuesForHints(['domain']);
|
||||
const timeMetaData = domains.find((d) => d.key === timeSystem.key);
|
||||
// in the valuesMetadata, find the first numeric data type
|
||||
const rangeItems = metaData.valueMetadatas.filter(
|
||||
(metaDatum) => metaDatum.hints && metaDatum.hints.range
|
||||
);
|
||||
rangeItems.sort((a, b) => a.hints.range - b.hints.range);
|
||||
let valueToUse = rangeItems[0]?.key;
|
||||
if (!valueToUse) {
|
||||
// if no numeric data type, just use the first one
|
||||
valueToUse = metaData.valueMetadatas[0]?.key;
|
||||
}
|
||||
this.#domainObject.configuration.comps.parameters.push({
|
||||
keyString,
|
||||
name: `${this.#getNextAlphabeticalParameterName()}`,
|
||||
valueToUse,
|
||||
testValue: 0,
|
||||
timeMetaData,
|
||||
accumulateValues: false,
|
||||
sampleSize: 10
|
||||
});
|
||||
this.emit('parameterAdded', this.#domainObject);
|
||||
}
|
||||
|
||||
getParameters() {
|
||||
const parameters = this.#domainObject.configuration.comps.parameters;
|
||||
const parametersWithTimeKey = parameters.map((parameter) => {
|
||||
return {
|
||||
...parameter,
|
||||
timeKey: this.#telemetryCollections[parameter.keyString]?.timeKey
|
||||
};
|
||||
});
|
||||
return parametersWithTimeKey;
|
||||
}
|
||||
|
||||
getTelemetryObjectForParameter(keyString) {
|
||||
return this.#telemetryObjects[keyString];
|
||||
}
|
||||
|
||||
getMetaDataValuesForParameter(keyString) {
|
||||
const telemetryObject = this.getTelemetryObjectForParameter(keyString);
|
||||
const metaData = this.#openmct.telemetry.getMetadata(telemetryObject);
|
||||
return metaData.valueMetadatas;
|
||||
}
|
||||
|
||||
deleteParameter(keyString) {
|
||||
this.#domainObject.configuration.comps.parameters =
|
||||
this.#domainObject.configuration.comps.parameters.filter(
|
||||
(parameter) => parameter.keyString !== keyString
|
||||
);
|
||||
// if there are no parameters referencing this parameter keyString, remove the telemetry object too
|
||||
const parameterExists = this.#domainObject.configuration.comps.parameters.some(
|
||||
(parameter) => parameter.keyString === keyString
|
||||
);
|
||||
if (!parameterExists) {
|
||||
this.emit('parameterRemoved', this.#domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
setDomainObject(passedDomainObject) {
|
||||
this.#domainObject = passedDomainObject;
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this.#loaded;
|
||||
}
|
||||
|
||||
async load(telemetryOptions) {
|
||||
// Increment the load version to mark a new load operation
|
||||
const loadVersion = ++this.#loadVersion;
|
||||
|
||||
if (!_.isEqual(this.#telemetryOptions, telemetryOptions)) {
|
||||
this.#destroy();
|
||||
}
|
||||
|
||||
this.#telemetryOptions = telemetryOptions;
|
||||
|
||||
// Start the load process and store the promise
|
||||
this.#currentLoadPromise = (async () => {
|
||||
// Load composition if not already loaded
|
||||
if (!this.#compositionLoaded) {
|
||||
await this.#loadComposition();
|
||||
// Check if a newer load has been initiated
|
||||
if (loadVersion !== this.#loadVersion) {
|
||||
await this.#currentLoadPromise;
|
||||
return;
|
||||
}
|
||||
this.#compositionLoaded = true;
|
||||
}
|
||||
|
||||
// Start listening to telemetry if not already done
|
||||
if (!this.#loaded) {
|
||||
await this.#startListeningToUnderlyingTelemetry();
|
||||
// Check again for newer load
|
||||
if (loadVersion !== this.#loadVersion) {
|
||||
await this.#currentLoadPromise;
|
||||
return;
|
||||
}
|
||||
this.#loaded = true;
|
||||
}
|
||||
})();
|
||||
|
||||
// Await the load process
|
||||
await this.#currentLoadPromise;
|
||||
}
|
||||
|
||||
async #startListeningToUnderlyingTelemetry() {
|
||||
Object.keys(this.#telemetryCollections).forEach((keyString) => {
|
||||
if (!this.#telemetryCollections[keyString].loaded) {
|
||||
this.#telemetryCollections[keyString].on('add', this.#getTelemetryProcessor(keyString));
|
||||
this.#telemetryCollections[keyString].on('clear', this.clearData);
|
||||
const telemetryLoadedPromise = this.#telemetryCollections[keyString].load();
|
||||
this.#telemetryLoadedPromises.push(telemetryLoadedPromise);
|
||||
}
|
||||
});
|
||||
await Promise.all(this.#telemetryLoadedPromises);
|
||||
this.#telemetryLoadedPromises = [];
|
||||
}
|
||||
|
||||
#destroy() {
|
||||
this.stopListeningToUnderlyingTelemetry();
|
||||
this.#composition = null;
|
||||
this.#telemetryCollections = {};
|
||||
this.#compositionLoaded = false;
|
||||
this.#loaded = false;
|
||||
this.#telemetryObjects = {};
|
||||
}
|
||||
|
||||
stopListeningToUnderlyingTelemetry() {
|
||||
this.#loaded = false;
|
||||
Object.keys(this.#telemetryCollections).forEach((keyString) => {
|
||||
const specificTelemetryProcessor = this.#telemetryProcessors[keyString];
|
||||
delete this.#telemetryProcessors[keyString];
|
||||
this.#telemetryCollections[keyString].off('add', specificTelemetryProcessor);
|
||||
this.#telemetryCollections[keyString].off('clear', this.clearData);
|
||||
this.#telemetryCollections[keyString].destroy();
|
||||
});
|
||||
}
|
||||
|
||||
getTelemetryObjects() {
|
||||
return this.#telemetryObjects;
|
||||
}
|
||||
|
||||
async #loadComposition() {
|
||||
this.#composition = this.#openmct.composition.get(this.#domainObject);
|
||||
if (this.#composition) {
|
||||
this.#composition.on('add', this.#addTelemetryObject);
|
||||
this.#composition.on('remove', this.#removeTelemetryObject);
|
||||
await this.#composition.load();
|
||||
}
|
||||
}
|
||||
|
||||
#getParameterForKeyString(keyString) {
|
||||
return this.#domainObject.configuration.comps.parameters.find(
|
||||
(parameter) => parameter.keyString === keyString
|
||||
);
|
||||
}
|
||||
|
||||
#getImputedDataUsingLOCF(datum, telemetryCollection) {
|
||||
const telemetryCollectionData = telemetryCollection.getAll();
|
||||
let insertionPointForNewData = telemetryCollection._sortedIndex(datum);
|
||||
if (insertionPointForNewData && insertionPointForNewData >= telemetryCollectionData.length) {
|
||||
insertionPointForNewData = telemetryCollectionData.length - 1;
|
||||
}
|
||||
// get the closest datum to the new datum
|
||||
const closestDatum = telemetryCollectionData[insertionPointForNewData];
|
||||
// clone the closest datum and replace the time key with the new time
|
||||
const imputedData = {
|
||||
...closestDatum,
|
||||
[telemetryCollection.timeKey]: datum[telemetryCollection.timeKey]
|
||||
};
|
||||
return imputedData;
|
||||
}
|
||||
|
||||
getDataFrameForRequest() {
|
||||
// Step 1: Collect all unique timestamps from all telemetry collections
|
||||
const allTimestampsSet = new Set();
|
||||
|
||||
Object.values(this.#telemetryCollections).forEach((collection) => {
|
||||
collection.getAll().forEach((dataPoint) => {
|
||||
allTimestampsSet.add(dataPoint.timestamp);
|
||||
});
|
||||
});
|
||||
|
||||
// Convert the set to a sorted array
|
||||
const allTimestamps = Array.from(allTimestampsSet).sort((a, b) => a - b);
|
||||
|
||||
// Step 2: Initialize the result object
|
||||
const telemetryForComps = {};
|
||||
|
||||
// Step 3: Iterate through each telemetry collection to align data
|
||||
Object.keys(this.#telemetryCollections).forEach((keyString) => {
|
||||
const telemetryCollection = this.#telemetryCollections[keyString];
|
||||
const alignedValues = [];
|
||||
|
||||
// Iterate through each common timestamp
|
||||
allTimestamps.forEach((timestamp) => {
|
||||
const timeKey = telemetryCollection.timeKey;
|
||||
const fakeData = { [timeKey]: timestamp };
|
||||
const imputedDatum = this.#getImputedDataUsingLOCF(fakeData, telemetryCollection);
|
||||
if (imputedDatum) {
|
||||
alignedValues.push(imputedDatum);
|
||||
}
|
||||
});
|
||||
|
||||
telemetryForComps[keyString] = alignedValues;
|
||||
});
|
||||
|
||||
return telemetryForComps;
|
||||
}
|
||||
|
||||
getDataFrameForSubscription(newTelemetry) {
|
||||
const telemetryForComps = {};
|
||||
const newTelemetryKey = Object.keys(newTelemetry)[0];
|
||||
const newTelemetryParameter = this.#getParameterForKeyString(newTelemetryKey);
|
||||
const newTelemetryData = newTelemetry[newTelemetryKey];
|
||||
const otherTelemetryKeys = Object.keys(this.#telemetryCollections).slice(0);
|
||||
if (newTelemetryParameter.accumulateValues) {
|
||||
telemetryForComps[newTelemetryKey] = this.#telemetryCollections[newTelemetryKey].getAll();
|
||||
} else {
|
||||
telemetryForComps[newTelemetryKey] = newTelemetryData;
|
||||
}
|
||||
otherTelemetryKeys.forEach((keyString) => {
|
||||
telemetryForComps[keyString] = [];
|
||||
});
|
||||
|
||||
const otherTelemetryKeysNotAccumulating = otherTelemetryKeys.filter(
|
||||
(keyString) => !this.#getParameterForKeyString(keyString).accumulateValues
|
||||
);
|
||||
const otherTelemetryKeysAccumulating = otherTelemetryKeys.filter(
|
||||
(keyString) => this.#getParameterForKeyString(keyString).accumulateValues
|
||||
);
|
||||
|
||||
// if we're accumulating, just add all the data
|
||||
otherTelemetryKeysAccumulating.forEach((keyString) => {
|
||||
telemetryForComps[keyString] = this.#telemetryCollections[keyString].getAll();
|
||||
});
|
||||
|
||||
// for the others, march through the new telemetry data and add data to the frame from the other telemetry objects
|
||||
// using LOCF
|
||||
newTelemetryData.forEach((newDatum) => {
|
||||
otherTelemetryKeysNotAccumulating.forEach((otherKeyString) => {
|
||||
const otherCollection = this.#telemetryCollections[otherKeyString];
|
||||
const imputedDatum = this.#getImputedDataUsingLOCF(newDatum, otherCollection);
|
||||
if (imputedDatum) {
|
||||
telemetryForComps[otherKeyString].push(imputedDatum);
|
||||
}
|
||||
});
|
||||
});
|
||||
return telemetryForComps;
|
||||
}
|
||||
|
||||
#removeTelemetryObject = (telemetryObjectIdentifier) => {
|
||||
const keyString = this.#openmct.objects.makeKeyString(telemetryObjectIdentifier);
|
||||
delete this.#telemetryObjects[keyString];
|
||||
this.#telemetryCollections[keyString]?.destroy();
|
||||
delete this.#telemetryCollections[keyString];
|
||||
// remove all parameters that reference this telemetry object
|
||||
this.deleteParameter(keyString);
|
||||
};
|
||||
|
||||
#requestUnderlyingTelemetry() {
|
||||
const underlyingTelemetry = {};
|
||||
Object.keys(this.#telemetryCollections).forEach((collectionKey) => {
|
||||
const collection = this.#telemetryCollections[collectionKey];
|
||||
underlyingTelemetry[collectionKey] = collection.getAll();
|
||||
});
|
||||
return underlyingTelemetry;
|
||||
}
|
||||
|
||||
#getTelemetryProcessor(keyString) {
|
||||
if (this.#telemetryProcessors[keyString]) {
|
||||
return this.#telemetryProcessors[keyString];
|
||||
}
|
||||
|
||||
const telemetryProcessor = (newTelemetry) => {
|
||||
this.emit('underlyingTelemetryUpdated', { [keyString]: newTelemetry });
|
||||
};
|
||||
this.#telemetryProcessors[keyString] = telemetryProcessor;
|
||||
return telemetryProcessor;
|
||||
}
|
||||
|
||||
#telemetryProcessor = (newTelemetry, keyString) => {
|
||||
this.emit('underlyingTelemetryUpdated', { [keyString]: newTelemetry });
|
||||
};
|
||||
|
||||
clearData(telemetryLoadedPromise) {
|
||||
this.#loaded = false;
|
||||
if (telemetryLoadedPromise) {
|
||||
this.#telemetryLoadedPromises.push(telemetryLoadedPromise);
|
||||
}
|
||||
}
|
||||
|
||||
setOutputFormat(outputFormat) {
|
||||
this.#domainObject.configuration.comps.outputFormat = outputFormat;
|
||||
this.emit('outputFormatChanged', outputFormat);
|
||||
}
|
||||
|
||||
getOutputFormat() {
|
||||
return this.#domainObject.configuration.comps.outputFormat;
|
||||
}
|
||||
|
||||
getExpression() {
|
||||
return this.#domainObject.configuration.comps.expression;
|
||||
}
|
||||
|
||||
#addTelemetryObject = (telemetryObject) => {
|
||||
const keyString = this.#openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
this.#telemetryObjects[keyString] = telemetryObject;
|
||||
this.#telemetryCollections[keyString] = this.#openmct.telemetry.requestCollection(
|
||||
telemetryObject,
|
||||
this.#telemetryOptions
|
||||
);
|
||||
|
||||
// check to see if we have a corresponding parameter
|
||||
// if not, add one
|
||||
const parameterExists = this.#domainObject.configuration.comps.parameters.some(
|
||||
(parameter) => parameter.keyString === keyString
|
||||
);
|
||||
if (!parameterExists) {
|
||||
this.addParameter(telemetryObject);
|
||||
}
|
||||
};
|
||||
|
||||
static getCompsManager(domainObject, openmct, compsManagerPool) {
|
||||
const id = openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
if (!compsManagerPool[id]) {
|
||||
compsManagerPool[id] = new CompsManager(openmct, domainObject);
|
||||
}
|
||||
|
||||
return compsManagerPool[id];
|
||||
}
|
||||
}
|
139
src/plugins/comps/CompsMathWorker.js
Normal file
139
src/plugins/comps/CompsMathWorker.js
Normal file
@ -0,0 +1,139 @@
|
||||
import { evaluate } from 'mathjs';
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
onconnect = function (e) {
|
||||
const port = e.ports[0];
|
||||
|
||||
port.onmessage = function (event) {
|
||||
const { type, callbackID, telemetryForComps, expression, parameters, newTelemetry } =
|
||||
event.data;
|
||||
let responseType = 'unknown';
|
||||
let error = null;
|
||||
let result = [];
|
||||
try {
|
||||
if (type === 'calculateRequest') {
|
||||
responseType = 'calculationRequestResult';
|
||||
console.debug(`📫 Received new calculation request with callback ID ${callbackID}`);
|
||||
result = calculateRequest(telemetryForComps, parameters, expression);
|
||||
} else if (type === 'calculateSubscription') {
|
||||
responseType = 'calculationSubscriptionResult';
|
||||
result = calculateSubscription(telemetryForComps, newTelemetry, parameters, expression);
|
||||
} else if (type === 'init') {
|
||||
port.postMessage({ type: 'ready' });
|
||||
return;
|
||||
} else {
|
||||
throw new Error('Invalid message type');
|
||||
}
|
||||
} catch (errorInCalculation) {
|
||||
error = errorInCalculation;
|
||||
}
|
||||
console.debug(`📭 Sending response for callback ID ${callbackID}`, result);
|
||||
port.postMessage({ type: responseType, callbackID, result, error });
|
||||
};
|
||||
};
|
||||
|
||||
function getFullDataFrame(telemetryForComps, parameters) {
|
||||
const dataFrame = {};
|
||||
Object.keys(telemetryForComps)?.forEach((key) => {
|
||||
const parameter = parameters.find((p) => p.keyString === key);
|
||||
const dataSet = telemetryForComps[key];
|
||||
const telemetryMap = new Map(dataSet.map((item) => [item[parameter.timeKey], item]));
|
||||
dataFrame[key] = telemetryMap;
|
||||
});
|
||||
return dataFrame;
|
||||
}
|
||||
|
||||
function calculateSubscription(telemetryForComps, newTelemetry, parameters, expression) {
|
||||
const dataFrame = getFullDataFrame(telemetryForComps, parameters);
|
||||
const calculation = calculate(dataFrame, parameters, expression);
|
||||
const newTelemetryKey = Object.keys(newTelemetry)[0];
|
||||
const newTelemetrySize = newTelemetry[newTelemetryKey].length;
|
||||
let trimmedCalculation = calculation;
|
||||
if (calculation.length > newTelemetrySize) {
|
||||
trimmedCalculation = calculation.slice(calculation.length - newTelemetrySize);
|
||||
}
|
||||
return trimmedCalculation;
|
||||
}
|
||||
|
||||
function calculateRequest(telemetryForComps, parameters, expression) {
|
||||
const dataFrame = getFullDataFrame(telemetryForComps, parameters);
|
||||
return calculate(dataFrame, parameters, expression);
|
||||
}
|
||||
|
||||
function calculate(dataFrame, parameters, expression) {
|
||||
const sumResults = [];
|
||||
// ensure all parameter keyStrings have corresponding telemetry data
|
||||
if (!expression) {
|
||||
return sumResults;
|
||||
}
|
||||
// set up accumulated data structure
|
||||
const accumulatedData = {};
|
||||
parameters.forEach((parameter) => {
|
||||
if (parameter.accumulateValues) {
|
||||
accumulatedData[parameter.name] = [];
|
||||
}
|
||||
});
|
||||
|
||||
// take the first parameter keyString as the reference
|
||||
const referenceParameter = parameters[0];
|
||||
const otherParameters = parameters.slice(1);
|
||||
// iterate over the reference telemetry data
|
||||
const referenceTelemetry = dataFrame[referenceParameter.keyString];
|
||||
referenceTelemetry?.forEach((referenceTelemetryItem) => {
|
||||
let referenceValue = referenceTelemetryItem[referenceParameter.valueToUse];
|
||||
if (referenceParameter.accumulateValues) {
|
||||
accumulatedData[referenceParameter.name].push(referenceValue);
|
||||
referenceValue = accumulatedData[referenceParameter.name];
|
||||
}
|
||||
if (
|
||||
referenceParameter.accumulateValues &&
|
||||
referenceParameter.sampleSize &&
|
||||
referenceParameter.sampleSize > 0
|
||||
) {
|
||||
// enforce sample size by ensuring referenceValue has the latest n elements
|
||||
// if we don't have at least the sample size, skip this iteration
|
||||
if (!referenceValue.length || referenceValue.length < referenceParameter.sampleSize) {
|
||||
return;
|
||||
}
|
||||
referenceValue = referenceValue.slice(-referenceParameter.sampleSize);
|
||||
}
|
||||
|
||||
const scope = {
|
||||
[referenceParameter.name]: referenceValue
|
||||
};
|
||||
const referenceTime = referenceTelemetryItem[referenceParameter.timeKey];
|
||||
// iterate over the other parameters to set the scope
|
||||
let missingData = false;
|
||||
otherParameters.forEach((parameter) => {
|
||||
const otherDataFrame = dataFrame[parameter.keyString];
|
||||
const otherTelemetry = otherDataFrame.get(referenceTime);
|
||||
if (otherTelemetry === undefined || otherTelemetry === null) {
|
||||
missingData = true;
|
||||
return;
|
||||
}
|
||||
let otherValue = otherTelemetry[parameter.valueToUse];
|
||||
if (parameter.accumulateValues) {
|
||||
accumulatedData[parameter.name].push(referenceValue);
|
||||
otherValue = accumulatedData[referenceParameter.name];
|
||||
}
|
||||
scope[parameter.name] = otherValue;
|
||||
});
|
||||
if (missingData) {
|
||||
console.debug('🤦♂️ Missing data for some parameters, skipping calculation');
|
||||
return;
|
||||
}
|
||||
const rawComputedValue = evaluate(expression, scope);
|
||||
let computedValue = rawComputedValue;
|
||||
if (computedValue.entries) {
|
||||
// if there aren't any entries, return with nothing
|
||||
if (computedValue.entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
console.debug('📊 Computed value is an array of entries', computedValue.entries);
|
||||
// make array of arrays of entries
|
||||
computedValue = computedValue.entries?.[0];
|
||||
}
|
||||
sumResults.push({ [referenceParameter.timeKey]: referenceTime, value: computedValue });
|
||||
});
|
||||
return sumResults;
|
||||
}
|
79
src/plugins/comps/CompsMetadataProvider.js
Normal file
79
src/plugins/comps/CompsMetadataProvider.js
Normal file
@ -0,0 +1,79 @@
|
||||
/*****************************************************************************
|
||||
* 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 CompsManager from './CompsManager.js';
|
||||
|
||||
export default class CompsMetadataProvider {
|
||||
#openmct = null;
|
||||
#compsManagerPool = null;
|
||||
|
||||
constructor(openmct, compsManagerPool) {
|
||||
this.#openmct = openmct;
|
||||
this.#compsManagerPool = compsManagerPool;
|
||||
}
|
||||
|
||||
supportsMetadata(domainObject) {
|
||||
return domainObject.type === 'comps';
|
||||
}
|
||||
|
||||
getDefaultDomains(domainObject) {
|
||||
return this.#openmct.time.getAllTimeSystems().map(function (ts, i) {
|
||||
return {
|
||||
key: ts.key,
|
||||
name: ts.name,
|
||||
format: ts.timeFormat,
|
||||
hints: {
|
||||
domain: i
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getMetadata(domainObject) {
|
||||
const specificCompsManager = CompsManager.getCompsManager(
|
||||
domainObject,
|
||||
this.#openmct,
|
||||
this.#compsManagerPool
|
||||
);
|
||||
// if there are any parameters, grab the first one's timeMetaData
|
||||
const timeMetaData = specificCompsManager?.getParameters()[0]?.timeMetaData;
|
||||
const metaDataToReturn = {
|
||||
values: [
|
||||
{
|
||||
key: 'value',
|
||||
name: 'Value',
|
||||
derived: true,
|
||||
formatString: specificCompsManager.getOutputFormat(),
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
if (timeMetaData) {
|
||||
metaDataToReturn.values.push(timeMetaData);
|
||||
} else {
|
||||
const defaultDomains = this.getDefaultDomains(domainObject);
|
||||
metaDataToReturn.values.push(...defaultDomains);
|
||||
}
|
||||
return metaDataToReturn;
|
||||
}
|
||||
}
|
175
src/plugins/comps/CompsTelemetryProvider.js
Normal file
175
src/plugins/comps/CompsTelemetryProvider.js
Normal file
@ -0,0 +1,175 @@
|
||||
/*****************************************************************************
|
||||
* 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 CompsManager from './CompsManager.js';
|
||||
|
||||
export default class CompsTelemetryProvider {
|
||||
#openmct = null;
|
||||
#sharedWorker = null;
|
||||
#compsManagerPool = null;
|
||||
#lastUniqueID = 1;
|
||||
#requestPromises = {};
|
||||
#subscriptionCallbacks = {};
|
||||
// id is random 4 digit number
|
||||
#id = Math.floor(Math.random() * 9000) + 1000;
|
||||
|
||||
constructor(openmct, compsManagerPool) {
|
||||
this.#openmct = openmct;
|
||||
this.#compsManagerPool = compsManagerPool;
|
||||
this.#openmct.on('start', this.#startSharedWorker.bind(this));
|
||||
}
|
||||
|
||||
isTelemetryObject(domainObject) {
|
||||
return domainObject.type === 'comps';
|
||||
}
|
||||
|
||||
supportsRequest(domainObject) {
|
||||
return domainObject.type === 'comps';
|
||||
}
|
||||
|
||||
supportsSubscribe(domainObject) {
|
||||
return domainObject.type === 'comps';
|
||||
}
|
||||
|
||||
#getCallbackID() {
|
||||
return this.#lastUniqueID++;
|
||||
}
|
||||
|
||||
request(domainObject, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const specificCompsManager = CompsManager.getCompsManager(
|
||||
domainObject,
|
||||
this.#openmct,
|
||||
this.#compsManagerPool
|
||||
);
|
||||
specificCompsManager.load(options).then(() => {
|
||||
const callbackID = this.#getCallbackID();
|
||||
const telemetryForComps = JSON.parse(
|
||||
JSON.stringify(specificCompsManager.getDataFrameForRequest())
|
||||
);
|
||||
const expression = specificCompsManager.getExpression();
|
||||
const parameters = JSON.parse(JSON.stringify(specificCompsManager.getParameters()));
|
||||
if (!expression || !parameters) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
this.#requestPromises[callbackID] = { resolve, reject };
|
||||
const payload = {
|
||||
type: 'calculateRequest',
|
||||
telemetryForComps,
|
||||
expression,
|
||||
parameters,
|
||||
callbackID
|
||||
};
|
||||
this.#sharedWorker.port.postMessage(payload);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#computeOnNewTelemetry(specificCompsManager, callbackID, newTelemetry) {
|
||||
if (!specificCompsManager.isReady()) {
|
||||
return;
|
||||
}
|
||||
const expression = specificCompsManager.getExpression();
|
||||
const telemetryForComps = specificCompsManager.getDataFrameForSubscription(newTelemetry);
|
||||
const parameters = JSON.parse(JSON.stringify(specificCompsManager.getParameters()));
|
||||
if (!expression || !parameters) {
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
type: 'calculateSubscription',
|
||||
telemetryForComps,
|
||||
newTelemetry,
|
||||
expression,
|
||||
parameters,
|
||||
callbackID
|
||||
};
|
||||
this.#sharedWorker.port.postMessage(payload);
|
||||
}
|
||||
|
||||
subscribe(domainObject, callback) {
|
||||
const specificCompsManager = CompsManager.getCompsManager(
|
||||
domainObject,
|
||||
this.#openmct,
|
||||
this.#compsManagerPool
|
||||
);
|
||||
const callbackID = this.#getCallbackID();
|
||||
this.#subscriptionCallbacks[callbackID] = callback;
|
||||
const boundComputeOnNewTelemetry = this.#computeOnNewTelemetry.bind(
|
||||
this,
|
||||
specificCompsManager,
|
||||
callbackID
|
||||
);
|
||||
specificCompsManager.on('underlyingTelemetryUpdated', boundComputeOnNewTelemetry);
|
||||
const telemetryOptions = {
|
||||
strategy: 'latest',
|
||||
size: 1
|
||||
};
|
||||
specificCompsManager.load(telemetryOptions);
|
||||
return () => {
|
||||
delete this.#subscriptionCallbacks[callbackID];
|
||||
specificCompsManager.stopListeningToUnderlyingTelemetry();
|
||||
specificCompsManager.off('underlyingTelemetryUpdated', boundComputeOnNewTelemetry);
|
||||
};
|
||||
}
|
||||
|
||||
#startSharedWorker() {
|
||||
if (this.#sharedWorker) {
|
||||
throw new Error('Shared worker already started');
|
||||
}
|
||||
const sharedWorkerURL = `${this.#openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}compsMathWorker.js`;
|
||||
|
||||
this.#sharedWorker = new SharedWorker(sharedWorkerURL, `Comps Math Worker`);
|
||||
this.#sharedWorker.port.onmessage = this.onSharedWorkerMessage.bind(this);
|
||||
this.#sharedWorker.port.onmessageerror = this.onSharedWorkerMessageError.bind(this);
|
||||
this.#sharedWorker.port.start();
|
||||
|
||||
this.#sharedWorker.port.postMessage({ type: 'init' });
|
||||
|
||||
this.#openmct.on('destroy', () => {
|
||||
this.#sharedWorker.port.close();
|
||||
});
|
||||
}
|
||||
|
||||
onSharedWorkerMessage(event) {
|
||||
const { type, result, callbackID, error } = event.data;
|
||||
if (
|
||||
type === 'calculationSubscriptionResult' &&
|
||||
this.#subscriptionCallbacks[callbackID] &&
|
||||
result.length
|
||||
) {
|
||||
this.#subscriptionCallbacks[callbackID](result);
|
||||
} else if (type === 'calculationRequestResult' && this.#requestPromises[callbackID]) {
|
||||
if (error) {
|
||||
console.error('📝 Error calculating request:', event.data);
|
||||
this.#requestPromises[callbackID].resolve([]);
|
||||
} else {
|
||||
this.#requestPromises[callbackID].resolve(result);
|
||||
}
|
||||
delete this.#requestPromises[callbackID];
|
||||
}
|
||||
}
|
||||
|
||||
onSharedWorkerMessageError(event) {
|
||||
console.error('❌ Shared worker message error:', event);
|
||||
}
|
||||
}
|
98
src/plugins/comps/CompsViewProvider.js
Normal file
98
src/plugins/comps/CompsViewProvider.js
Normal file
@ -0,0 +1,98 @@
|
||||
/*****************************************************************************
|
||||
* 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 CompsView from './components/CompsView.vue';
|
||||
|
||||
const DEFAULT_VIEW_PRIORITY = 100;
|
||||
|
||||
export default class ConditionSetViewProvider {
|
||||
constructor(openmct, compsManagerPool) {
|
||||
this.openmct = openmct;
|
||||
this.name = 'Comps View';
|
||||
this.key = 'comps.view';
|
||||
this.cssClass = 'icon-derived-telemetry';
|
||||
this.compsManagerPool = compsManagerPool;
|
||||
}
|
||||
|
||||
canView(domainObject, objectPath) {
|
||||
return domainObject.type === 'comps' && this.openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
canEdit(domainObject, objectPath) {
|
||||
return domainObject.type === 'comps' && this.openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
view(domainObject, objectPath) {
|
||||
let _destroy = null;
|
||||
let component = null;
|
||||
|
||||
return {
|
||||
show: (container, isEditing) => {
|
||||
const { vNode, destroy } = mount(
|
||||
{
|
||||
el: container,
|
||||
components: {
|
||||
CompsView
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
domainObject,
|
||||
objectPath,
|
||||
compsManagerPool: this.compsManagerPool
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing
|
||||
};
|
||||
},
|
||||
template: '<CompsView :isEditing="isEditing"></CompsView>'
|
||||
},
|
||||
{
|
||||
app: this.openmct.app,
|
||||
element: container
|
||||
}
|
||||
);
|
||||
_destroy = destroy;
|
||||
component = vNode.componentInstance;
|
||||
},
|
||||
onEditModeChange: (isEditing) => {
|
||||
component.isEditing = isEditing;
|
||||
},
|
||||
destroy: () => {
|
||||
if (_destroy) {
|
||||
_destroy();
|
||||
}
|
||||
component = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
priority(domainObject) {
|
||||
if (domainObject.type === 'comps') {
|
||||
return Number.MAX_VALUE;
|
||||
} else {
|
||||
return DEFAULT_VIEW_PRIORITY;
|
||||
}
|
||||
}
|
||||
}
|
77
src/plugins/comps/components/CompsInspectorView.vue
Normal file
77
src/plugins/comps/components/CompsInspectorView.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<template>
|
||||
<div class="c-inspect-properties">
|
||||
<template v-if="isEditing">
|
||||
<ul class="c-inspect-properties__section">
|
||||
<li class="c-inspect-properties__row">
|
||||
<div class="c-inspect-properties__label" title="Output Format">
|
||||
<label for="OutputFormatControl">Output Format</label>
|
||||
</div>
|
||||
<div class="c-inspect-properties__value">
|
||||
<input
|
||||
id="OutputFormatControl"
|
||||
v-model="inputFormatValue"
|
||||
type="text"
|
||||
class="c-input--flex"
|
||||
placeholder="e.g. %0.2f"
|
||||
@change="changeInputFormat()"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, onBeforeMount, onBeforeUnmount, ref } from 'vue';
|
||||
|
||||
import CompsManager from '../CompsManager';
|
||||
|
||||
const isEditing = ref(false);
|
||||
const inputFormatValue = ref('');
|
||||
|
||||
const openmct = inject('openmct');
|
||||
const domainObject = inject('domainObject');
|
||||
const compsManagerPool = inject('compsManagerPool');
|
||||
|
||||
onBeforeMount(() => {
|
||||
isEditing.value = openmct.editor.isEditing();
|
||||
openmct.editor.on('isEditing', toggleEdit);
|
||||
inputFormatValue.value = domainObject.configuration.comps.outputFormat;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
openmct.editor.off('isEditing', toggleEdit);
|
||||
});
|
||||
|
||||
function toggleEdit(passedIsEditing) {
|
||||
isEditing.value = passedIsEditing;
|
||||
}
|
||||
|
||||
function changeInputFormat() {
|
||||
openmct.objects.mutate(domainObject, `configuration.comps.outputFormat`, inputFormatValue.value);
|
||||
const compsManager = CompsManager.getCompsManager(domainObject, openmct, compsManagerPool);
|
||||
compsManager.setOutputFormat(inputFormatValue.value);
|
||||
}
|
||||
</script>
|
399
src/plugins/comps/components/CompsView.vue
Normal file
399
src/plugins/comps/components/CompsView.vue
Normal file
@ -0,0 +1,399 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="c-comps" aria-label="Derived Telemetry">
|
||||
<section class="c-section c-comps-output">
|
||||
<div class="c-output-featured">
|
||||
<span class="c-output-featured__label">Current Output</span>
|
||||
<span class="c-output-featured__value" aria-label="Current Output Value">
|
||||
<template
|
||||
v-if="testDataApplied && currentTestOutput !== undefined && currentTestOutput !== null"
|
||||
>
|
||||
{{ currentTestOutput }}
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
!testDataApplied && currentCompOutput !== undefined && currentCompOutput !== null
|
||||
"
|
||||
>
|
||||
{{ currentCompOutput }}
|
||||
</template>
|
||||
<template v-else> --- </template>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
id="telemetryReferenceSection"
|
||||
class="c-comps__section c-comps__refs-and-controls"
|
||||
aria-describedby="telemetryReferences"
|
||||
>
|
||||
<div class="c-cs__header c-section__header">
|
||||
<div id="telemetryReferences" class="c-cs__header-label c-section__label">
|
||||
Telemetry References
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="c-comps__apply-test-data-control"
|
||||
:class="['c-comps__refs-controls c-cdef__controls', { disabled: !parameters?.length }]"
|
||||
>
|
||||
<label class="c-toggle-switch">
|
||||
<input type="checkbox" :checked="testDataApplied" @change="toggleTestData" />
|
||||
<span class="c-toggle-switch__slider" aria-label="Apply Test Data"></span>
|
||||
<span class="c-toggle-switch__label">Apply Test Values</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="c-comps__refs">
|
||||
<div v-for="parameter in parameters" :key="parameter.keyString" class="c-comps__ref">
|
||||
<div class="c-comps__ref-section">
|
||||
<div class="c-comps__ref-sub-section ref-and-path">
|
||||
<span class="c-test-datum__string">Reference</span>
|
||||
<input
|
||||
v-if="isEditing"
|
||||
v-model="parameter.name"
|
||||
:aria-label="`Reference Name Input for ${parameter.name}`"
|
||||
type="text"
|
||||
class="c-input--md"
|
||||
@change="updateParameters"
|
||||
/>
|
||||
<div v-else class="--em">{{ parameter.name }}</div>
|
||||
<span class="c-test-datum__string">=</span>
|
||||
<span
|
||||
class="c-comps__path-and-field"
|
||||
:aria-label="`Reference ${parameter.name} Object Path`"
|
||||
>
|
||||
<ObjectPathString
|
||||
:domain-object="compsManager.getTelemetryObjectForParameter(parameter.keyString)"
|
||||
:show-object-itself="true"
|
||||
class="c-comp__ref-path --em"
|
||||
/>
|
||||
<!-- drop down to select value from telemetry -->
|
||||
<select
|
||||
v-if="isEditing"
|
||||
v-model="parameter.valueToUse"
|
||||
class="c-comp__ref-field"
|
||||
@change="updateParameters"
|
||||
>
|
||||
<option
|
||||
v-for="parameterValueOption in compsManager.getMetaDataValuesForParameter(
|
||||
parameter.keyString
|
||||
)"
|
||||
:key="parameterValueOption.key"
|
||||
:value="parameterValueOption.key"
|
||||
>
|
||||
{{ parameterValueOption.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-else class="c-comp__ref-field">{{ parameter.valueToUse }}</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="c-comps__ref-sub-section accum-vals"
|
||||
:class="['c-comps__refs-controls', { disabled: !parameters?.length }]"
|
||||
>
|
||||
<label class="c-toggle-switch">
|
||||
<span class="c-toggle-switch__label">Accumulate Values</span>
|
||||
<input
|
||||
v-model="parameter.accumulateValues"
|
||||
type="checkbox"
|
||||
@change="updateAccumulateValues(parameter)"
|
||||
/>
|
||||
<span
|
||||
class="c-toggle-switch__slider"
|
||||
aria-label="Toggle Parameter Accumulation"
|
||||
></span>
|
||||
</label>
|
||||
|
||||
<span v-if="isEditing && parameter.accumulateValues" class="c-comps__label"
|
||||
>Sample Size</span
|
||||
>
|
||||
<input
|
||||
v-if="isEditing && parameter.accumulateValues"
|
||||
v-model="parameter.sampleSize"
|
||||
:aria-label="`Sample Size for ${parameter.name}`"
|
||||
type="number"
|
||||
class="c-input--sm c-comps__value"
|
||||
@change="updateParameters"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isEditing && parameter.accumulateValues"
|
||||
class="c-comps__ref-sub-section accum-vals"
|
||||
>
|
||||
Accumulating values with sample size {{ parameter.sampleSize }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditing" class="c-comps__ref-section">
|
||||
<span class="c-comps__label">Test value</span>
|
||||
<input
|
||||
v-if="isEditing"
|
||||
v-model="parameter.testValue"
|
||||
:aria-label="`Reference Test Value for ${parameter.name}`"
|
||||
type="text"
|
||||
class="c-input--md c-comps__value"
|
||||
@change="updateTestValue(parameter)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="expressionSection" class="c-comps__section c-comps__expression">
|
||||
<div class="c-cs__header c-section__header">
|
||||
<div class="c-cs__header-label c-section__label">Expression</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!parameters?.length && isEditing" class="hint">
|
||||
Drag in telemetry to add references for an expression.
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
v-if="parameters?.length && isEditing"
|
||||
v-model="expression"
|
||||
class="c-comps__expression-value"
|
||||
placeholder="Enter an expression"
|
||||
@change="updateExpression"
|
||||
></textarea>
|
||||
<div v-else>
|
||||
<div class="c-comps__expression-value" aria-label="Expression">
|
||||
{{ expression }}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="expression && expressionOutput"
|
||||
class="icon-alert-triangle c-comps__expression-msg --bad"
|
||||
>
|
||||
Invalid: {{ expressionOutput }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="expression && !expressionOutput && isEditing"
|
||||
class="c-comps__expression-msg --good"
|
||||
>
|
||||
Expression valid
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { evaluate } from 'mathjs';
|
||||
import { inject, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
import ObjectPathString from '../../../ui/components/ObjectPathString.vue';
|
||||
import CompsManager from '../CompsManager';
|
||||
|
||||
const openmct = inject('openmct');
|
||||
const domainObject = inject('domainObject');
|
||||
const compsManagerPool = inject('compsManagerPool');
|
||||
const compsManager = CompsManager.getCompsManager(domainObject, openmct, compsManagerPool);
|
||||
const currentCompOutput = ref(null);
|
||||
const currentTestOutput = ref(null);
|
||||
const testDataApplied = ref(false);
|
||||
const parameters = ref(null);
|
||||
const expression = ref(null);
|
||||
const expressionOutput = ref(null);
|
||||
const outputFormat = ref(null);
|
||||
|
||||
let outputTelemetryCollection;
|
||||
|
||||
const props = defineProps({
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeMount(async () => {
|
||||
let maxSampleSize = 20;
|
||||
if (parameters.value) {
|
||||
maxSampleSize =
|
||||
parameters.value.reduce((max, param) => {
|
||||
if (param.accumulateValues) {
|
||||
return Math.max(max, param.sampleSize);
|
||||
}
|
||||
return max;
|
||||
}, 0) + 20;
|
||||
}
|
||||
|
||||
const telemetryOptions = {
|
||||
strategy: 'minmax',
|
||||
size: maxSampleSize
|
||||
};
|
||||
// TODO: we should dynamically set size to the largest comp input window
|
||||
outputTelemetryCollection = openmct.telemetry.requestCollection(domainObject, telemetryOptions);
|
||||
outputTelemetryCollection.on('add', telemetryProcessor);
|
||||
outputTelemetryCollection.on('clear', clearData);
|
||||
compsManager.on('parameterAdded', reloadParameters);
|
||||
compsManager.on('parameterRemoved', reloadParameters);
|
||||
compsManager.on('outputFormatChanged', updateOutputFormat);
|
||||
await outputTelemetryCollection.load(telemetryOptions); // will implicitly load compsManager
|
||||
parameters.value = compsManager.getParameters();
|
||||
expression.value = compsManager.getExpression();
|
||||
outputFormat.value = compsManager.getOutputFormat();
|
||||
applyTestData();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
outputTelemetryCollection.off('add', telemetryProcessor);
|
||||
outputTelemetryCollection.off('clear', clearData);
|
||||
compsManager.off('parameterAdded', reloadParameters);
|
||||
compsManager.off('parameterRemoved', reloadParameters);
|
||||
compsManager.off('outputFormatChanged', updateOutputFormat);
|
||||
outputTelemetryCollection.destroy();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.isEditing,
|
||||
(editMode) => {
|
||||
if (!editMode) {
|
||||
testDataApplied.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function updateOutputFormat() {
|
||||
outputFormat.value = compsManager.getOutputFormat();
|
||||
// delete the metadata cache so that the new output format is used
|
||||
openmct.telemetry.removeMetadataFromCache(domainObject);
|
||||
}
|
||||
|
||||
function reloadParameters(passedDomainObject) {
|
||||
// Because this is triggered by a composition change, we have
|
||||
// to defer mutation of our domain object, otherwise we might
|
||||
// mutate an outdated version of the domain object.
|
||||
setTimeout(function () {
|
||||
domainObject.configuration.comps.parameters = passedDomainObject.configuration.comps.parameters;
|
||||
parameters.value = domainObject.configuration.comps.parameters;
|
||||
openmct.objects.mutate(domainObject, `configuration.comps.parameters`, parameters.value);
|
||||
compsManager.setDomainObject(domainObject);
|
||||
applyTestData();
|
||||
});
|
||||
}
|
||||
|
||||
function updateParameters() {
|
||||
openmct.objects.mutate(domainObject, `configuration.comps.parameters`, parameters.value);
|
||||
compsManager.setDomainObject(domainObject);
|
||||
applyTestData();
|
||||
reload();
|
||||
}
|
||||
|
||||
function updateAccumulateValues(parameter) {
|
||||
if (parameter.accumulateValues) {
|
||||
parameter.testValue = [''];
|
||||
} else {
|
||||
parameter.testValue = '';
|
||||
}
|
||||
updateParameters();
|
||||
}
|
||||
|
||||
function updateTestValue(parameter) {
|
||||
if (parameter.accumulateValues && parameter.testValue === '') {
|
||||
parameter.testValue = [];
|
||||
}
|
||||
updateParameters();
|
||||
}
|
||||
|
||||
function toggleTestData() {
|
||||
testDataApplied.value = !testDataApplied.value;
|
||||
if (testDataApplied.value) {
|
||||
applyTestData();
|
||||
} else {
|
||||
clearData();
|
||||
}
|
||||
}
|
||||
|
||||
function updateExpression() {
|
||||
openmct.objects.mutate(domainObject, `configuration.comps.expression`, expression.value);
|
||||
compsManager.setDomainObject(domainObject);
|
||||
applyTestData();
|
||||
reload();
|
||||
}
|
||||
|
||||
function getValueFormatter() {
|
||||
const metaData = openmct.telemetry.getMetadata(domainObject);
|
||||
const outputMetaDatum = metaData.values().find((metaDatum) => metaDatum.key === 'value');
|
||||
return openmct.telemetry.getValueFormatter(outputMetaDatum);
|
||||
}
|
||||
|
||||
function applyTestData() {
|
||||
if (!expression.value || !parameters.value) {
|
||||
return;
|
||||
}
|
||||
const scope = parameters.value.reduce((acc, parameter) => {
|
||||
// try to parse the test value as JSON
|
||||
try {
|
||||
const parsedValue = JSON.parse(parameter.testValue);
|
||||
acc[parameter.name] = parsedValue;
|
||||
} catch (error) {
|
||||
acc[parameter.name] = parameter.testValue;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// see which parameters are misconfigured as non-arrays
|
||||
const misconfiguredParameterNames = parameters.value
|
||||
.filter((parameter) => {
|
||||
return parameter.accumulateValues && !Array.isArray(scope[parameter.name]);
|
||||
})
|
||||
.map((parameter) => parameter.name);
|
||||
if (misconfiguredParameterNames.length) {
|
||||
const misconfiguredParameterNamesString = misconfiguredParameterNames.join(', ');
|
||||
currentTestOutput.value = null;
|
||||
expressionOutput.value = `Reference "${misconfiguredParameterNamesString}" set to accumulating, but test values aren't arrays.`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const testOutput = evaluate(expression.value, scope);
|
||||
const formattedData = getValueFormatter().format(testOutput);
|
||||
currentTestOutput.value = formattedData;
|
||||
expressionOutput.value = null;
|
||||
} catch (error) {
|
||||
currentTestOutput.value = null;
|
||||
expressionOutput.value = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function telemetryProcessor(data) {
|
||||
if (testDataApplied.value) {
|
||||
return;
|
||||
}
|
||||
// new data will come in as array, so just take the last element
|
||||
const currentOutput = data[data.length - 1]?.value;
|
||||
const formattedOutput = getValueFormatter().format(currentOutput);
|
||||
currentCompOutput.value = formattedOutput;
|
||||
}
|
||||
|
||||
function reload() {
|
||||
clearData();
|
||||
outputTelemetryCollection._requestHistoricalTelemetry();
|
||||
}
|
||||
|
||||
function clearData() {
|
||||
currentCompOutput.value = null;
|
||||
}
|
||||
</script>
|
153
src/plugins/comps/components/comps.scss
Normal file
153
src/plugins/comps/components/comps.scss
Normal file
@ -0,0 +1,153 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
@mixin expressionMsg($fg, $bg) {
|
||||
$op: 0.4;
|
||||
color: rgba($fg, $op * 1.5);
|
||||
background: rgba($bg, $op);
|
||||
}
|
||||
|
||||
.c-comps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $interiorMarginLg;
|
||||
|
||||
.is-editing & {
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
|
||||
&__output {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: $interiorMargin;
|
||||
|
||||
&-label {
|
||||
flex: 0 0 auto;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&-value {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__section,
|
||||
&__refs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $interiorMarginSm;
|
||||
}
|
||||
|
||||
|
||||
&__apply-test-data-control {
|
||||
padding: $interiorMargin 0;
|
||||
}
|
||||
|
||||
&__refs {
|
||||
|
||||
}
|
||||
|
||||
&__ref {
|
||||
@include discreteItem();
|
||||
align-items: start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 $interiorMargin;
|
||||
line-height: 170%; // Aligns text with controls like selects
|
||||
|
||||
> * + * {
|
||||
border-top: 1px solid $colorInteriorBorder;
|
||||
}
|
||||
}
|
||||
|
||||
&__ref-section {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $interiorMargin;
|
||||
padding: $interiorMargin 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__ref-sub-section {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
gap: $interiorMargin;
|
||||
|
||||
&.ref-and-path {
|
||||
flex: 0 1 auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__path-and-field {
|
||||
align-items: start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $interiorMargin;
|
||||
|
||||
.c-comp__ref-path {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
&__label,
|
||||
&__value {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__expression {
|
||||
*[class*=value] {
|
||||
font-family: monospace;
|
||||
resize: vertical; // Only applies to textarea
|
||||
}
|
||||
div[class*=value] {
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
&__expression-msg {
|
||||
@include expressionMsg($colorOkFg, $colorOk);
|
||||
border-radius: $basicCr;
|
||||
display: flex; // Creates hanging indent from :before icon
|
||||
padding: $interiorMarginSm $interiorMarginLg $interiorMarginSm $interiorMargin;
|
||||
max-width: max-content;
|
||||
|
||||
&:before {
|
||||
content: $glyph-icon-check;
|
||||
font-family: symbolsfont;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&.--bad {
|
||||
@include expressionMsg($colorErrorFg, $colorError);
|
||||
|
||||
&:before {
|
||||
content: $glyph-icon-alert-triangle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.--em {
|
||||
color: $colorBodyFgEm;
|
||||
}
|
||||
}
|
60
src/plugins/comps/plugin.js
Normal file
60
src/plugins/comps/plugin.js
Normal file
@ -0,0 +1,60 @@
|
||||
/*****************************************************************************
|
||||
* 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 CompsInspectorViewProvider from './CompsInspectorViewProvider.js';
|
||||
import CompsMetadataProvider from './CompsMetadataProvider.js';
|
||||
import CompsTelemetryProvider from './CompsTelemetryProvider.js';
|
||||
import CompsViewProvider from './CompsViewProvider.js';
|
||||
|
||||
export default function CompsPlugin() {
|
||||
const compsManagerPool = {};
|
||||
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('comps', {
|
||||
name: 'Derived Telemetry',
|
||||
key: 'comps',
|
||||
description:
|
||||
'Add one or more telemetry end points, apply a mathematical operation to them, and output the result as new telemetry.',
|
||||
creatable: true,
|
||||
cssClass: 'icon-derived-telemetry',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.configuration = {
|
||||
comps: {
|
||||
expression: '',
|
||||
parameters: []
|
||||
}
|
||||
};
|
||||
domainObject.composition = [];
|
||||
domainObject.telemetry = {};
|
||||
}
|
||||
});
|
||||
openmct.composition.addPolicy((parent, child) => {
|
||||
if (parent.type === 'comps' && !openmct.telemetry.isTelemetryObject(child)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
openmct.telemetry.addProvider(new CompsMetadataProvider(openmct, compsManagerPool));
|
||||
openmct.telemetry.addProvider(new CompsTelemetryProvider(openmct, compsManagerPool));
|
||||
openmct.objectViews.addProvider(new CompsViewProvider(openmct, compsManagerPool));
|
||||
openmct.inspectorViews.addProvider(new CompsInspectorViewProvider(openmct, compsManagerPool));
|
||||
};
|
||||
}
|
53
src/plugins/condition/ConditionInspectorViewProvider.js
Normal file
53
src/plugins/condition/ConditionInspectorViewProvider.js
Normal file
@ -0,0 +1,53 @@
|
||||
// src/plugins/condition/ConditionInspectorView.js
|
||||
|
||||
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: '<condition-config-view></condition-config-view>'
|
||||
},
|
||||
{
|
||||
app: openmct.app,
|
||||
element
|
||||
}
|
||||
);
|
||||
_destroy = destroy;
|
||||
},
|
||||
showTab: function (isEditing) {
|
||||
return isEditing;
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
},
|
||||
destroy: function () {
|
||||
if (_destroy) {
|
||||
_destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
@ -24,6 +24,7 @@ import { EventEmitter } from 'eventemitter3';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import Condition from './Condition.js';
|
||||
import HistoricalTelemetryProvider from './HistoricalTelemetryProvider.js';
|
||||
import { getLatestTimestamp } from './utils/time.js';
|
||||
|
||||
export default class ConditionManager extends EventEmitter {
|
||||
@ -46,6 +47,8 @@ export default class ConditionManager extends EventEmitter {
|
||||
applied: false
|
||||
};
|
||||
this.initialize();
|
||||
this.telemetryBuffer = [];
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
subscribeToTelemetry(telemetryObject) {
|
||||
@ -320,69 +323,18 @@ export default class ConditionManager extends EventEmitter {
|
||||
return currentCondition;
|
||||
}
|
||||
|
||||
getCurrentConditionLAD(conditionResults) {
|
||||
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
||||
let currentCondition = conditionCollection[conditionCollection.length - 1];
|
||||
|
||||
for (let i = 0; i < conditionCollection.length - 1; i++) {
|
||||
if (conditionResults[conditionCollection[i].id]) {
|
||||
//first condition to be true wins
|
||||
currentCondition = conditionCollection[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return currentCondition;
|
||||
}
|
||||
|
||||
async requestLADConditionSetOutput(options) {
|
||||
if (!this.conditions.length) {
|
||||
async getHistoricalData(options) {
|
||||
if (!this.conditionSetDomainObject.configuration.shouldFetchHistorical) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await this.compositionLoad;
|
||||
|
||||
let latestTimestamp;
|
||||
let conditionResults = {};
|
||||
let nextLegOptions = { ...options };
|
||||
delete nextLegOptions.onPartialResponse;
|
||||
|
||||
const results = await Promise.all(
|
||||
this.conditions.map((condition) => condition.requestLADConditionResult(nextLegOptions))
|
||||
const historicalTelemetry = new HistoricalTelemetryProvider(
|
||||
this.openmct,
|
||||
this.conditions,
|
||||
this.conditionSetDomainObject,
|
||||
options
|
||||
);
|
||||
|
||||
results.forEach((resultObj) => {
|
||||
const {
|
||||
id,
|
||||
data,
|
||||
data: { result }
|
||||
} = resultObj;
|
||||
|
||||
if (this.findConditionById(id)) {
|
||||
conditionResults[id] = Boolean(result);
|
||||
}
|
||||
|
||||
latestTimestamp = getLatestTimestamp(
|
||||
latestTimestamp,
|
||||
data,
|
||||
this.timeSystems,
|
||||
this.openmct.time.getTimeSystem()
|
||||
);
|
||||
});
|
||||
|
||||
if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentCondition = this.getCurrentConditionLAD(conditionResults);
|
||||
const currentOutput = {
|
||||
output: currentCondition.configuration.output,
|
||||
id: this.conditionSetDomainObject.identifier,
|
||||
conditionId: currentCondition.id,
|
||||
...latestTimestamp
|
||||
};
|
||||
|
||||
return [currentOutput];
|
||||
const historicalData = await historicalTelemetry.getHistoricalData();
|
||||
return historicalData;
|
||||
}
|
||||
|
||||
isTelemetryUsed(endpoint) {
|
||||
@ -409,13 +361,16 @@ export default class ConditionManager extends EventEmitter {
|
||||
const datum = data[0];
|
||||
|
||||
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
|
||||
const timeSystemKey = this.openmct.time.getTimeSystem().key;
|
||||
let timestamp = {};
|
||||
const currentTimestamp = normalizedDatum[timeSystemKey];
|
||||
timestamp[timeSystemKey] = currentTimestamp;
|
||||
if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {
|
||||
const keyString = this.openmct.objects.makeKeyString(endpoint.identifier);
|
||||
const associatedTelemetryCollection = this.telemetryCollections[keyString];
|
||||
const timeKey = associatedTelemetryCollection.timeKey;
|
||||
const formattedTimeStamp = associatedTelemetryCollection.parseTime(datum);
|
||||
const rawTimestamp = {
|
||||
[timeKey]: datum[timeKey]
|
||||
};
|
||||
if (this.shouldEvaluateNewTelemetry(formattedTimeStamp)) {
|
||||
this.updateConditionResults(normalizedDatum);
|
||||
this.updateCurrentCondition(timestamp);
|
||||
this.updateCurrentCondition(rawTimestamp, endpoint, datum);
|
||||
}
|
||||
}
|
||||
|
||||
@ -428,20 +383,75 @@ export default class ConditionManager extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
updateCurrentCondition(timestamp) {
|
||||
const currentCondition = this.getCurrentCondition();
|
||||
emitConditionSetResult(currentCondition, timestamp, outputValue, result) {
|
||||
const conditionSetResult = {
|
||||
id: this.conditionSetDomainObject.identifier,
|
||||
conditionId: currentCondition.id,
|
||||
value: outputValue,
|
||||
result,
|
||||
...timestamp
|
||||
};
|
||||
this.emit('conditionSetResultUpdated', conditionSetResult);
|
||||
}
|
||||
|
||||
this.emit(
|
||||
'conditionSetResultUpdated',
|
||||
Object.assign(
|
||||
{
|
||||
output: currentCondition.configuration.output,
|
||||
id: this.conditionSetDomainObject.identifier,
|
||||
conditionId: currentCondition.id
|
||||
},
|
||||
updateCurrentCondition(timestamp, telemetryObject, telemetryData) {
|
||||
this.telemetryBuffer.push({ timestamp, telemetryObject, telemetryData });
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
fetchUnderlyingTelemetry(currentCondition, telemetryObject, telemetryData, timestamp) {
|
||||
let telemetryValue;
|
||||
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 {
|
||||
// grab it from the associated telemetry collection
|
||||
const outputTelemetryCollection = this.telemetryCollections[selectedOutputIdentifier];
|
||||
const latestTelemetryData = outputTelemetryCollection.getAll();
|
||||
const lastValue = latestTelemetryData[latestTelemetryData.length - 1];
|
||||
if (lastValue && lastValue?.[outputMetadata]) {
|
||||
telemetryValue = lastValue?.[outputMetadata];
|
||||
} else {
|
||||
telemetryValue = null;
|
||||
}
|
||||
}
|
||||
return telemetryValue;
|
||||
}
|
||||
|
||||
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 (currentCondition?.configuration?.outputTelemetry) {
|
||||
telemetryValue = this.fetchUnderlyingTelemetry(
|
||||
currentCondition,
|
||||
telemetryObject,
|
||||
telemetryData,
|
||||
timestamp
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
this.emitConditionSetResult(currentCondition, timestamp, telemetryValue, conditionResult);
|
||||
}
|
||||
|
||||
getTestData(metadatum) {
|
||||
@ -460,18 +470,7 @@ export default class ConditionManager extends EventEmitter {
|
||||
|
||||
createNormalizedDatum(telemetryDatum, endpoint) {
|
||||
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
|
||||
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
|
||||
|
||||
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
|
||||
const testValue = this.getTestData(metadatum);
|
||||
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
|
||||
datum[metadatum.key] =
|
||||
testValue !== undefined
|
||||
? formatter.parse(testValue)
|
||||
: formatter.parse(telemetryDatum[metadatum.source]);
|
||||
|
||||
return datum;
|
||||
}, {});
|
||||
const normalizedDatum = { ...telemetryDatum };
|
||||
|
||||
normalizedDatum.id = id;
|
||||
|
||||
|
@ -29,7 +29,7 @@ export default class ConditionSetMetadataProvider {
|
||||
return domainObject.type === 'conditionSet';
|
||||
}
|
||||
|
||||
getDomains(domainObject) {
|
||||
getDefaultDomains(domainObject) {
|
||||
return this.openmct.time.getAllTimeSystems().map(function (ts, i) {
|
||||
return {
|
||||
key: ts.key,
|
||||
@ -43,34 +43,59 @@ 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;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
values: this.getDomains().concat([
|
||||
const resultEnum = [
|
||||
{
|
||||
string: 'true',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
string: 'false',
|
||||
value: false
|
||||
}
|
||||
];
|
||||
|
||||
const metaDataToReturn = {
|
||||
values: [
|
||||
{
|
||||
key: 'state',
|
||||
source: 'output',
|
||||
name: 'State',
|
||||
format: 'enum',
|
||||
enumerations: enumerations,
|
||||
key: 'value',
|
||||
name: 'Value',
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'output',
|
||||
name: 'Value',
|
||||
format: 'string',
|
||||
key: 'result',
|
||||
source: 'result',
|
||||
name: 'Result',
|
||||
format: 'enum',
|
||||
enumerations: resultEnum,
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
}
|
||||
])
|
||||
]
|
||||
};
|
||||
|
||||
// if there are any parameters, grab the first one's timeMetaData
|
||||
const timeMetaData =
|
||||
domainObject?.configuration?.conditionCollection[0]?.configuration.timeMetadata;
|
||||
|
||||
if (timeMetaData) {
|
||||
metaDataToReturn.values.push(timeMetaData);
|
||||
} else {
|
||||
const defaultDomains = this.getDefaultDomains(domainObject);
|
||||
metaDataToReturn.values.push(...defaultDomains);
|
||||
}
|
||||
|
||||
return metaDataToReturn;
|
||||
}
|
||||
}
|
||||
|
@ -42,15 +42,17 @@ export default class ConditionSetTelemetryProvider {
|
||||
|
||||
async request(domainObject, options) {
|
||||
let conditionManager = this.getConditionManager(domainObject);
|
||||
let latestOutput = await conditionManager.requestLADConditionSetOutput(options);
|
||||
return latestOutput;
|
||||
const formattedHistoricalData = await conditionManager.getHistoricalData(options);
|
||||
return formattedHistoricalData;
|
||||
}
|
||||
|
||||
subscribe(domainObject, callback) {
|
||||
let conditionManager = this.getConditionManager(domainObject);
|
||||
|
||||
conditionManager.on('conditionSetResultUpdated', (data) => {
|
||||
callback(data);
|
||||
if (data?.result) {
|
||||
callback(data);
|
||||
}
|
||||
});
|
||||
|
||||
return this.destroyConditionManager.bind(
|
||||
|
285
src/plugins/condition/HistoricalTelemetryProvider.js
Normal file
285
src/plugins/condition/HistoricalTelemetryProvider.js
Normal file
@ -0,0 +1,285 @@
|
||||
import { evaluateResults } from './utils/evaluator.js';
|
||||
|
||||
export default class HistoricalTelemetryProvider {
|
||||
#telemetryOptions;
|
||||
#telemetryObjects = {};
|
||||
#conditions;
|
||||
#conditionSetDomainObject;
|
||||
#openmct;
|
||||
#telemetryCollections = {};
|
||||
#composition;
|
||||
#outputTelemetryDetails = {};
|
||||
|
||||
constructor(openmct, conditions, conditionSetDomainObject, telemetryOptions) {
|
||||
this.#openmct = openmct;
|
||||
this.#conditions = conditions;
|
||||
this.#conditionSetDomainObject = conditionSetDomainObject;
|
||||
this.#telemetryOptions = telemetryOptions;
|
||||
this.addTelemetryObject = this.addTelemetryObject.bind(this);
|
||||
this.removeTelemetryObject = this.removeTelemetryObject.bind(this);
|
||||
}
|
||||
|
||||
#evaluateTrueCondition() {
|
||||
return null;
|
||||
}
|
||||
|
||||
#getInputTelemetry(conditionCriteria, dataFrame, timestamp) {
|
||||
if (conditionCriteria?.telemetry === 'all') {
|
||||
// if the criteria is 'all', return all telemetry data
|
||||
const allTelemetry = [];
|
||||
Object.keys(dataFrame).forEach((key) => {
|
||||
const telemetryData = dataFrame[key][timestamp];
|
||||
telemetryData.id = key;
|
||||
if (telemetryData) {
|
||||
allTelemetry.push(telemetryData);
|
||||
}
|
||||
});
|
||||
return allTelemetry;
|
||||
}
|
||||
if (!conditionCriteria?.telemetry) {
|
||||
console.debug('🚨 Missing telemetry key in condition criteria - are we the default?');
|
||||
return null;
|
||||
}
|
||||
const conditionInputTelemetryKeyString = this.#openmct.objects.makeKeyString(
|
||||
conditionCriteria?.telemetry
|
||||
);
|
||||
|
||||
const inputTelemetryByDate = dataFrame[conditionInputTelemetryKeyString];
|
||||
if (!inputTelemetryByDate) {
|
||||
console.debug(`🚨 Missing ALL data for ${conditionInputTelemetryKeyString}`);
|
||||
return null;
|
||||
}
|
||||
const specificDatum = inputTelemetryByDate[timestamp];
|
||||
|
||||
if (!specificDatum) {
|
||||
console.debug(`🚨 Missing data for ${conditionInputTelemetryKeyString} at ${timestamp}`);
|
||||
return null;
|
||||
}
|
||||
specificDatum.id = conditionInputTelemetryKeyString;
|
||||
return specificDatum;
|
||||
}
|
||||
|
||||
#formatDatumForOutput(datum, metadata, result) {
|
||||
const formattedDatum = {
|
||||
...datum,
|
||||
value: datum[metadata],
|
||||
result
|
||||
};
|
||||
return formattedDatum;
|
||||
}
|
||||
|
||||
#computeHistoricalDatum(timestamp, dataFrame, timekey) {
|
||||
for (let conditionIndex = 0; conditionIndex < this.#conditions.length; conditionIndex++) {
|
||||
const condition = this.#conditions[conditionIndex];
|
||||
const { id } = condition;
|
||||
const conditionCriteria = condition?.criteria.length > 0;
|
||||
let result = false;
|
||||
let defaultHit = false;
|
||||
if (conditionCriteria) {
|
||||
result = evaluateResults(
|
||||
condition.criteria.map((criterion) => {
|
||||
const inputTelemetry = this.#getInputTelemetry(criterion, dataFrame, timestamp);
|
||||
return criterion.computeResult({ id, ...inputTelemetry });
|
||||
}),
|
||||
condition?.trigger
|
||||
);
|
||||
} else {
|
||||
// default criteria is 'all'
|
||||
defaultHit = true;
|
||||
}
|
||||
if (result || defaultHit) {
|
||||
// generate the output telemetry object if available
|
||||
const outputTelmetryDetail = this.#outputTelemetryDetails[id];
|
||||
if (
|
||||
outputTelmetryDetail?.outputTelemetryKeyString &&
|
||||
outputTelmetryDetail?.outputMetadata
|
||||
) {
|
||||
const outputTelmetryDatum =
|
||||
dataFrame[outputTelmetryDetail.outputTelemetryKeyString][timestamp];
|
||||
const formattedDatum = this.#formatDatumForOutput(
|
||||
outputTelmetryDatum,
|
||||
outputTelmetryDetail.outputMetadata,
|
||||
result
|
||||
);
|
||||
return formattedDatum;
|
||||
} else if (outputTelmetryDetail?.staticOutputValue) {
|
||||
const staticOutput = {
|
||||
output: outputTelmetryDetail?.staticOutputValue,
|
||||
[timekey]: timestamp,
|
||||
result
|
||||
};
|
||||
return staticOutput;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #loadTelemetryCollections() {
|
||||
await Promise.all(
|
||||
Object.entries(this.#telemetryObjects).map(async ([keystring, telemetryObject]) => {
|
||||
// clone telemetry options without size as we need to load all data
|
||||
const telemetryOptionsWithoutSize = { ...this.#telemetryOptions };
|
||||
delete telemetryOptionsWithoutSize.size;
|
||||
const telemetryCollection = this.#openmct.telemetry.requestCollection(
|
||||
telemetryObject,
|
||||
telemetryOptionsWithoutSize
|
||||
);
|
||||
await telemetryCollection.load();
|
||||
this.#telemetryCollections[keystring] = telemetryCollection;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#computeHistoricalData(dataFrame) {
|
||||
const historicalData = [];
|
||||
if (Object.keys(dataFrame).length === 0) {
|
||||
// if we have no telemetry data, return an empty object
|
||||
return historicalData;
|
||||
}
|
||||
|
||||
// use the first telemetry collection as the reference for the frame
|
||||
const referenceTelemetryKeyString = Object.keys(dataFrame)[0];
|
||||
const referenceTelemetryCollection = this.#telemetryCollections[referenceTelemetryKeyString];
|
||||
const referenceTelemetryData = referenceTelemetryCollection.getAll();
|
||||
referenceTelemetryData.forEach((datum) => {
|
||||
const timestamp = datum[referenceTelemetryCollection.timeKey];
|
||||
const historicalDatum = this.#computeHistoricalDatum(
|
||||
timestamp,
|
||||
dataFrame,
|
||||
referenceTelemetryCollection.timeKey
|
||||
);
|
||||
if (historicalDatum) {
|
||||
historicalData.push(historicalDatum);
|
||||
}
|
||||
});
|
||||
return historicalData;
|
||||
}
|
||||
|
||||
#getImputedDataUsingLOCF(datum, telemetryCollection) {
|
||||
const telemetryCollectionData = telemetryCollection.getAll();
|
||||
let insertionPointForNewData = telemetryCollection._sortedIndex(datum);
|
||||
if (insertionPointForNewData && insertionPointForNewData >= telemetryCollectionData.length) {
|
||||
insertionPointForNewData = telemetryCollectionData.length - 1;
|
||||
}
|
||||
// get the closest datum to the new datum
|
||||
const closestDatum = telemetryCollectionData[insertionPointForNewData];
|
||||
// clone the closest datum and replace the time key with the new time
|
||||
const imputedData = {
|
||||
...closestDatum,
|
||||
[telemetryCollection.timeKey]: datum[telemetryCollection.timeKey]
|
||||
};
|
||||
return imputedData;
|
||||
}
|
||||
|
||||
#createDataFrame() {
|
||||
// Step 1: Collect all unique timestamps from all telemetry collections
|
||||
const allTimestampsSet = new Set();
|
||||
|
||||
Object.values(this.#telemetryCollections).forEach((collection) => {
|
||||
collection.getAll().forEach((dataPoint) => {
|
||||
const timeKey = collection.timeKey;
|
||||
allTimestampsSet.add(dataPoint[timeKey]);
|
||||
});
|
||||
});
|
||||
|
||||
// Convert the set to a sorted array
|
||||
const allTimestamps = Array.from(allTimestampsSet).sort((a, b) => a - b);
|
||||
|
||||
// Step 2: Initialize the result object
|
||||
const dataFrame = {};
|
||||
|
||||
// Step 3: Iterate through each telemetry collection to align data
|
||||
Object.keys(this.#telemetryCollections)?.forEach((keyString) => {
|
||||
const telemetryCollection = this.#telemetryCollections[keyString];
|
||||
const alignedValues = {};
|
||||
|
||||
// Iterate through each common timestamp
|
||||
allTimestamps.forEach((timestamp) => {
|
||||
const timeKey = telemetryCollection.timeKey;
|
||||
const fakeData = { [timeKey]: timestamp };
|
||||
const imputedDatum = this.#getImputedDataUsingLOCF(fakeData, telemetryCollection);
|
||||
if (imputedDatum) {
|
||||
alignedValues[timestamp] = imputedDatum;
|
||||
} else {
|
||||
console.debug(`🚨 Missing data for ${keyString} at ${timestamp}`);
|
||||
}
|
||||
});
|
||||
|
||||
dataFrame[keyString] = alignedValues;
|
||||
});
|
||||
return dataFrame;
|
||||
}
|
||||
|
||||
addTelemetryObject(telemetryObjectToAdd) {
|
||||
const keyString = this.#openmct.objects.makeKeyString(telemetryObjectToAdd.identifier);
|
||||
this.#telemetryObjects[keyString] = telemetryObjectToAdd;
|
||||
}
|
||||
|
||||
removeTelemetryObject(telemetryIdentifierToRemove) {
|
||||
const keyStringToRemove = this.#openmct.objects.makeKeyString(telemetryIdentifierToRemove);
|
||||
this.#telemetryObjects = this.#telemetryObjects.filter((existingTelemetryObject) => {
|
||||
const existingKeyString = this.#openmct.objects.makeKeyString(
|
||||
existingTelemetryObject.identifier
|
||||
);
|
||||
return keyStringToRemove !== existingKeyString;
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#composition.off('add', this.addTelemetryObject);
|
||||
this.#composition.off('remove', this.removeTelemetryObject);
|
||||
Object.keys(this.#telemetryCollections).forEach((key) => {
|
||||
this.#telemetryCollections[key]?.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
async #loadComposition() {
|
||||
// load the composition of the condition set
|
||||
this.#composition = this.#openmct.composition.get(this.#conditionSetDomainObject);
|
||||
if (this.#composition) {
|
||||
this.#composition.on('add', this.addTelemetryObject);
|
||||
this.#composition.on('remove', this.removeTelemetryObject);
|
||||
await this.#composition.load();
|
||||
}
|
||||
}
|
||||
|
||||
#processConditionSet() {
|
||||
const conditionCollection = this.#conditionSetDomainObject.configuration.conditionCollection;
|
||||
conditionCollection.forEach((condition, index) => {
|
||||
const { outputTelemetry, outputMetadata, output } = condition.configuration;
|
||||
if (outputTelemetry && outputMetadata) {
|
||||
this.#outputTelemetryDetails[condition.id] = {
|
||||
outputTelemetryKeyString: outputTelemetry,
|
||||
outputMetadata
|
||||
};
|
||||
} else if (output) {
|
||||
this.#outputTelemetryDetails[condition.id] = {
|
||||
staticOutputValue: output
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getHistoricalData() {
|
||||
console.debug('🍯 Getting historical data');
|
||||
// get the telemetry objects from the condition set
|
||||
await this.#loadComposition();
|
||||
console.debug('🍯 Loaded telemetry objects', this.#telemetryObjects);
|
||||
if (!this.#telemetryObjects) {
|
||||
console.debug('🚨 No telemetry objects found in condition set');
|
||||
return [];
|
||||
}
|
||||
console.debug('🍯 Processed Condition Set', this.#outputTelemetryDetails);
|
||||
this.#processConditionSet();
|
||||
// load telemetry collections for each telemetry object
|
||||
await this.#loadTelemetryCollections();
|
||||
console.debug('🍯 Loaded Telemetry Collections', this.#telemetryCollections);
|
||||
// create data frame from telemetry collections
|
||||
const dataFrame = this.#createDataFrame();
|
||||
console.debug('🍯 Created data frame', dataFrame);
|
||||
// compute historical data from data frame
|
||||
const computedHistoricalData = this.#computeHistoricalData(dataFrame);
|
||||
console.debug('🍯 Computed historical data', computedHistoricalData);
|
||||
return computedHistoricalData;
|
||||
}
|
||||
}
|
@ -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) => {
|
||||
|
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="c-inspect-properties">
|
||||
<h2>Configuration</h2>
|
||||
<section>
|
||||
<div class="c-form-row">
|
||||
<label for="historical-toggle">Enable Historical: </label>
|
||||
<ToggleSwitch
|
||||
id="historical-toggle"
|
||||
class="c-toggle-switch"
|
||||
:checked="historicalEnabled"
|
||||
name="condition-historical-toggle"
|
||||
@change="onToggleChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ToggleSwitch
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
historicalEnabled: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.historicalEnabled = this.domainObject.configuration.shouldFetchHistorical;
|
||||
},
|
||||
methods: {
|
||||
onToggleChange() {
|
||||
this.historicalEnabled = !this.historicalEnabled;
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
'configuration.shouldFetchHistorical',
|
||||
this.historicalEnabled
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.c-inspect-properties {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.c-form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
@ -99,13 +99,13 @@
|
||||
@change="setOutputValue"
|
||||
>
|
||||
<option v-for="option in outputOptions" :key="option" :value="option">
|
||||
{{ initCap(option) }}
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="c-cdef__control">
|
||||
<input
|
||||
v-if="selectedOutputSelection === outputOptions[2]"
|
||||
v-if="selectedOutputSelection === outputOptions[3]"
|
||||
v-model="condition.configuration.output"
|
||||
aria-label="Condition Output String"
|
||||
class="t-condition-name-input"
|
||||
@ -113,8 +113,41 @@
|
||||
@change="persist"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="selectedOutputSelection === telemetryValueString" class="c-cdef__control">
|
||||
<select
|
||||
v-model="condition.configuration.outputTelemetry"
|
||||
aria-label="Output Telemetry Selection"
|
||||
@change="persist"
|
||||
>
|
||||
<option value="">- Select Telemetry -</option>
|
||||
<option
|
||||
v-for="telemetryOption in telemetry"
|
||||
:key="openmct.objects.makeKeyString(telemetryOption.identifier)"
|
||||
:value="openmct.objects.makeKeyString(telemetryOption.identifier)"
|
||||
>
|
||||
{{ telemetryOption.path }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<span v-if="condition.configuration.outputTelemetry" class="c-cdef__control">
|
||||
<select
|
||||
v-model="condition.configuration.outputMetadata"
|
||||
aria-label="Output Telemetry Metadata Selection"
|
||||
@change="persist"
|
||||
>
|
||||
<option value="">- Select Field -</option>
|
||||
<option
|
||||
v-for="(option, index) in telemetryMetadataOptions[
|
||||
condition.configuration.outputTelemetry
|
||||
]"
|
||||
:key="index"
|
||||
:value="option.key"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div v-if="!condition.isDefault" class="c-cdef__match-and-criteria">
|
||||
<span class="c-cdef__separator c-row-separator"></span>
|
||||
<span class="c-cdef__label">Match</span>
|
||||
@ -183,7 +216,12 @@
|
||||
<span class="c-condition__name">
|
||||
{{ condition.configuration.name }}
|
||||
</span>
|
||||
<span class="c-condition__output"> Output: {{ condition.configuration.output }} </span>
|
||||
<span class="c-condition__output">
|
||||
Output:
|
||||
{{
|
||||
condition.configuration.output === undefined ? 'none' : condition.configuration.output
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="c-condition__summary">
|
||||
<ConditionDescription :show-label="false" :condition="condition" />
|
||||
@ -197,6 +235,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';
|
||||
|
||||
@ -252,10 +291,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: {
|
||||
@ -289,32 +331,64 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
condition: {
|
||||
handler() {
|
||||
const config = this.condition?.configuration;
|
||||
if (config?.output !== TELEMETRY_VALUE) {
|
||||
config.outputTelemetry = null;
|
||||
config.outputMetadata = null;
|
||||
config.timeMetadata = 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(),
|
||||
@ -396,13 +470,56 @@ export default {
|
||||
this.condition.configuration.criteria.splice(index + 1, 0, clonedCriterion);
|
||||
this.persist();
|
||||
},
|
||||
persistTimeMetadata() {
|
||||
if (!this.condition.configuration.outputTelemetry) {
|
||||
return;
|
||||
}
|
||||
const outputTelemetryObject = this.telemetry.find(
|
||||
(telemetryItem) =>
|
||||
this.openmct.objects.makeKeyString(telemetryItem.identifier) ===
|
||||
this.condition.configuration.outputTelemetry
|
||||
);
|
||||
const timeSystem = this.openmct.time.getTimeSystem();
|
||||
const telemetryMetadata = this.openmct.telemetry.getMetadata(outputTelemetryObject);
|
||||
const domains = telemetryMetadata?.valuesForHints(['domain']);
|
||||
const timeMetaData = domains.find((d) => d.key === timeSystem.key);
|
||||
if (telemetryMetadata) {
|
||||
this.condition.configuration.timeMetadata = timeMetaData;
|
||||
}
|
||||
},
|
||||
persist() {
|
||||
const valueMetadata = this.getOutputMetadata();
|
||||
if (valueMetadata) {
|
||||
this.condition.configuration.valueMetadata = valueMetadata;
|
||||
this.persistTimeMetadata();
|
||||
}
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -23,9 +23,9 @@
|
||||
<template>
|
||||
<div class="c-cs" :class="{ 'is-stale': isStale }" aria-label="Condition Set">
|
||||
<section class="c-cs__current-output c-section">
|
||||
<div class="c-cs__content c-cs__current-output-value">
|
||||
<span class="c-cs__current-output-value__label">Current Output</span>
|
||||
<span class="c-cs__current-output-value__value" aria-label="Current Output Value">
|
||||
<div class="c-output-featured">
|
||||
<span class="c-output-featured__label">Current Output</span>
|
||||
<span class="c-output-featured__value" aria-label="Current Output Value">
|
||||
<template v-if="currentConditionOutput">
|
||||
{{ currentConditionOutput }}
|
||||
</template>
|
||||
@ -86,7 +86,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateCurrentOutput(currentConditionResult) {
|
||||
this.currentConditionOutput = currentConditionResult.output;
|
||||
this.currentConditionOutput = currentConditionResult.value;
|
||||
},
|
||||
updateDefaultOutput(output) {
|
||||
this.currentConditionOutput = output;
|
||||
|
@ -40,7 +40,7 @@
|
||||
:key="telemetryOption.identifier.key"
|
||||
:value="telemetryOption.identifier"
|
||||
>
|
||||
{{ telemetryOption.name }}
|
||||
{{ telemetryOption.path }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
|
@ -63,7 +63,7 @@
|
||||
:key="index"
|
||||
:value="telemetryOption.identifier"
|
||||
>
|
||||
{{ telemetryOption.name }}
|
||||
{{ telemetryOption.path || telemetryOption.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
@ -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('/');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -51,6 +51,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
gap: $interiorMargin;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@ -89,23 +90,24 @@
|
||||
&__conditions {
|
||||
flex: 1 1 auto;
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
//> * + * {
|
||||
// margin-top: $interiorMarginSm;
|
||||
//}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $interiorMarginSm;
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
+ * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
//+ * {
|
||||
// margin-top: $interiorMarginSm;
|
||||
//}
|
||||
}
|
||||
|
||||
.c-button {
|
||||
@ -121,6 +123,7 @@
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $interiorMargin;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
};
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -123,7 +123,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.conditionalLabel = latestDatum.output || '';
|
||||
this.conditionalLabel = latestDatum.value || '';
|
||||
},
|
||||
async showToolTip() {
|
||||
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
|
@ -225,7 +225,13 @@ export default class PlotSeries extends Model {
|
||||
|
||||
try {
|
||||
const points = await this.openmct.telemetry.request(this.domainObject, options);
|
||||
const data = this.getSeriesData();
|
||||
// if derived, we can't use the old data
|
||||
let data = this.getSeriesData();
|
||||
|
||||
if (this.metadata.value(this.get('yKey')).derived) {
|
||||
data = [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line you-dont-need-lodash-underscore/concat
|
||||
const newPoints = _(data)
|
||||
.concat(points)
|
||||
|
@ -243,13 +243,15 @@ export default {
|
||||
domainObject: {
|
||||
...this.childObject,
|
||||
configuration: {
|
||||
...this.childObject.configuration,
|
||||
series: [
|
||||
{
|
||||
identifier: this.childObject.identifier,
|
||||
...persistedSeriesConfig.series
|
||||
}
|
||||
],
|
||||
yAxis: persistedSeriesConfig.yAxis
|
||||
yAxis: persistedSeriesConfig.yAxis,
|
||||
...this.childObject.configuration
|
||||
}
|
||||
},
|
||||
openmct: this.openmct,
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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]) {
|
||||
|
@ -32,6 +32,7 @@ import BarChartPlugin from './charts/bar/plugin.js';
|
||||
import ScatterPlotPlugin from './charts/scatter/plugin.js';
|
||||
import ClearData from './clearData/plugin.js';
|
||||
import Clock from './clock/plugin.js';
|
||||
import CompsPlugin from './comps/plugin.js';
|
||||
import ConditionPlugin from './condition/plugin.js';
|
||||
import ConditionWidgetPlugin from './conditionWidget/plugin.js';
|
||||
import CouchDBSearchFolder from './CouchDBSearchFolder/plugin.js';
|
||||
@ -178,5 +179,6 @@ plugins.Timelist = TimeList;
|
||||
plugins.InspectorViews = InspectorViews;
|
||||
plugins.InspectorDataVisualization = InspectorDataVisualization;
|
||||
plugins.EventTimestripPlugin = EventTimestripPlugin;
|
||||
plugins.Comps = CompsPlugin;
|
||||
|
||||
export default plugins;
|
||||
|
@ -353,7 +353,7 @@ $colorInspectorBg: $colorBodyBg;
|
||||
$colorInspectorFg: $colorBodyFg;
|
||||
$colorInspectorPropName: $colorBodyFgSubtle;
|
||||
$colorInspectorPropVal: $colorBodyFgEm;
|
||||
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%);
|
||||
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 10%);
|
||||
$colorInspectorSectionHeaderFg: #bfbfbf;
|
||||
|
||||
// Tabs
|
||||
|
@ -277,8 +277,9 @@ $glyph-icon-bar-chart: '\eb2c';
|
||||
$glyph-icon-map: '\eb2d';
|
||||
$glyph-icon-plan: '\eb2e';
|
||||
$glyph-icon-timelist: '\eb2f';
|
||||
$glyph-icon-notebook-shift-log: '\eb31';
|
||||
$glyph-icon-plot-scatter: '\eb30';
|
||||
$glyph-icon-notebook-shift-log: '\eb31';
|
||||
$glyph-icon-derived-telemetry: '\eb32';
|
||||
|
||||
/************************** GLYPHS AS DATA URI */
|
||||
// Only objects have been converted, for use in Create menu and folder views
|
||||
@ -335,3 +336,4 @@ $bg-icon-telemetry-aggregate: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns
|
||||
$bg-icon-trash: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='512px' height='512px' viewBox='0 0 512 512' enable-background='new 0 0 512 512' xml:space='preserve'%3e%3cpath d='M416,64h-96.18V32c0-17.6-14.4-32-32-32h-64c-17.6,0-32,14.4-32,32v32H96c-52.8,0-96,36-96,80s0,80,0,80h32v192 c0,52.8,43.2,96,96,96h256c52.8,0,96-43.2,96-96V224h32c0,0,0-36,0-80S468.8,64,416,64z M160,416H96V224h64V416z M288,416h-64V224 h64V416z M416,416h-64V224h64V416z'/%3e%3c/svg%3e");
|
||||
$bg-icon-eye-open: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 512 512' style='enable-background:new 0 0 512 512;' xml:space='preserve'%3e%3cstyle type='text/css'%3e .st0%7bfill:%2300A14B;%7d %3c/style%3e%3ctitle%3eicon-eye-open-v2%3c/title%3e%3cg%3e%3cpath class='st0' d='M256,58.2c-122.9,0-226.1,84-255.4,197.8C29.9,369.7,133.1,453.8,256,453.8s226.1-84,255.4-197.8 C482.1,142.3,378.9,58.2,256,58.2z M414.6,294.2c-11.3,17.2-25.3,32.4-41.5,45.2c-16.4,12.9-34.5,22.8-54,29.7 c-20.2,7.1-41.4,10.7-63,10.7s-42.9-3.6-63-10.7c-19.5-6.9-37.7-16.9-54-29.7c-16.2-12.8-30.2-27.9-41.5-45.2 c-7.9-12-14.4-24.8-19.3-38.2c5-13.4,11.5-26.2,19.3-38.2c11.3-17.2,25.3-32.4,41.5-45.2c16.4-12.9,34.5-22.8,54-29.7 c20.2-7.1,41.4-10.7,63-10.7s42.9,3.6,63,10.7c19.5,6.9,37.7,16.9,54,29.7c16.2,12.8,30.2,27.9,41.5,45.2 c7.9,12,14.4,24.8,19.3,38.2C429,269.4,422.5,282.2,414.6,294.2z'/%3e%3ccircle class='st0' cx='256' cy='256' r='96'/%3e%3c/g%3e%3c/svg%3e");
|
||||
$bg-icon-camera: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3ctitle%3eicon-camera-v2%3c/title%3e%3cpath d='M448,128H384L320,0H192L128,128H64A64.2,64.2,0,0,0,0,192V448a64.2,64.2,0,0,0,64,64H448a64.2,64.2,0,0,0,64-64V192A64.2,64.2,0,0,0,448,128ZM256,432A128,128,0,1,1,384,304,128,128,0,0,1,256,432Z'/%3e%3c/svg%3e");
|
||||
$bg-icon-derived-telemetry: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M66.1 166c20.2 24.3 35.1 54.6 44 75.7 10 23.6 21.7 44 33.1 57.7 11.1 13.3 18.5 16.3 20.2 16.3s9.1-3 20.2-16.3c11.4-13.7 23.1-34.2 33.1-57.7 8.9-21.1 23.8-51.4 44-75.7 23.3-28.1 48.7-42.3 75.6-42.3s52.2 14.2 75.6 42.3c20.2 24.3 35.1 54.6 44 75.7 10 23.6 21.7 44 33.1 57.7 11.1 13.3 18.5 16.3 20.2 16.3s1.6-.3 3.2-1.1v-58.9c-.2-141.3-114.9-256-256.2-256H.2v124.6c23.3 3 45.4 17 66 41.7Z'/%3e%3cpath d='M509 387.7c-26.8 0-52.2-14.2-75.6-42.3-20.2-24.3-35.1-54.6-44-75.7-10-23.6-21.7-44-33.1-57.7-11.1-13.3-18.5-16.3-20.2-16.3s-9.1 3-20.2 16.3c-11.4 13.7-23.1 34.2-33.1 57.7-8.9 21.1-23.8 51.4-44 75.7-23.3 28.1-48.7 42.3-75.6 42.3s-52.2-14.2-75.6-42.3c-20.2-24.3-35.1-54.6-44-75.7-10-23.6-21.7-44-33.1-57.7-4.1-4.9-7.6-8.4-10.6-10.8v54.5c.3 141.4 114.9 256 256.3 256h256V387.6H509Z'/%3e%3c/svg%3e");
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@ -2,6 +2,7 @@
|
||||
@import '../api/overlays/components/overlay-component.scss';
|
||||
@import '../api/tooltips/components/tooltip-component.scss';
|
||||
@import '../plugins/condition/components/conditionals.scss';
|
||||
@import '../plugins/comps/components/comps.scss';
|
||||
@import '../plugins/conditionWidget/components/condition-widget.scss';
|
||||
@import '../plugins/condition/components/inspector/conditional-styles.scss';
|
||||
@import '../plugins/displayLayout/components/box-and-line-views';
|
||||
|
168
src/ui/components/ObjectPathString.vue
Normal file
168
src/ui/components/ObjectPathString.vue
Normal file
@ -0,0 +1,168 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="orderedPath.length"
|
||||
class="c-object-path-string"
|
||||
:aria-label="`${domainObject.name} Object Path`"
|
||||
role="navigation"
|
||||
>
|
||||
{{ orderedPathStr }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
showObjectItself: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
objectPath: {
|
||||
type: Array,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
orderedPath: [],
|
||||
orderedPathStr: ''
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.abortController = new AbortController();
|
||||
this.nameChangeListeners = {};
|
||||
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
|
||||
if (keyString && this.keyString !== keyString) {
|
||||
this.keyString = keyString;
|
||||
this.originalPath = [];
|
||||
|
||||
let rawPath = null;
|
||||
if (this.objectPath === null) {
|
||||
try {
|
||||
rawPath = await this.openmct.objects.getOriginalPath(
|
||||
keyString,
|
||||
[],
|
||||
this.abortController.signal
|
||||
);
|
||||
} catch (error) {
|
||||
// aborting the search is ok, everything else should be thrown
|
||||
if (error.name !== 'AbortError') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rawPath = this.objectPath;
|
||||
}
|
||||
|
||||
const pathWithDomainObject = rawPath.map((domainObject, index, pathArray) => {
|
||||
let key = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const objectPath = pathArray.slice(index);
|
||||
|
||||
return {
|
||||
domainObject,
|
||||
key,
|
||||
objectPath
|
||||
};
|
||||
});
|
||||
if (this.showObjectItself) {
|
||||
// remove ROOT only
|
||||
this.orderedPath = pathWithDomainObject.slice(0, pathWithDomainObject.length - 1).reverse();
|
||||
} else {
|
||||
// remove ROOT and object itself from path
|
||||
this.orderedPath = pathWithDomainObject.slice(1, pathWithDomainObject.length - 1).reverse();
|
||||
}
|
||||
|
||||
this.orderedPath.forEach((pathObject) => {
|
||||
this.orderedPathStr = this.orderedPathStr.concat('/').concat(pathObject.domainObject.name);
|
||||
});
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
Object.values(this.nameChangeListeners).forEach((unlisten) => {
|
||||
unlisten();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Generate the hash url for the given object path, removing the '/ROOT' prefix if present.
|
||||
* @param {import('../../api/objects/ObjectAPI').DomainObject[]} objectPath
|
||||
*/
|
||||
navigateToPath(objectPath) {
|
||||
/** @type {string} */
|
||||
const path = `/browse/${this.openmct.objects.getRelativePath(objectPath)}`;
|
||||
|
||||
return path.replace('ROOT/', '');
|
||||
},
|
||||
updateObjectPathName(keyString, newName) {
|
||||
this.orderedPath = this.orderedPath.map((pathObject) => {
|
||||
if (this.openmct.objects.makeKeyString(pathObject.domainObject.identifier) === keyString) {
|
||||
return {
|
||||
...pathObject,
|
||||
domainObject: { ...pathObject.domainObject, name: newName }
|
||||
};
|
||||
}
|
||||
return pathObject;
|
||||
});
|
||||
},
|
||||
removeNameListenerFor(domainObject) {
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
if (this.nameChangeListeners[keyString]) {
|
||||
this.nameChangeListeners[keyString]();
|
||||
delete this.nameChangeListeners[keyString];
|
||||
}
|
||||
},
|
||||
addNameListenerFor(domainObject) {
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
if (!this.nameChangeListeners[keyString]) {
|
||||
this.nameChangeListeners[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'name',
|
||||
this.updateObjectPathName.bind(this, keyString)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -25,6 +25,7 @@
|
||||
.c-toggle-switch {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
gap: $interiorMarginSm;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
|
||||
@ -55,7 +56,6 @@
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-left: $interiorMarginSm;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user