mirror of
https://github.com/nasa/openmct.git
synced 2025-06-03 08:00:52 +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,22 +383,77 @@ export default class ConditionManager extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
updateCurrentCondition(timestamp) {
|
||||
const currentCondition = this.getCurrentCondition();
|
||||
|
||||
this.emit(
|
||||
'conditionSetResultUpdated',
|
||||
Object.assign(
|
||||
{
|
||||
output: currentCondition.configuration.output,
|
||||
emitConditionSetResult(currentCondition, timestamp, outputValue, result) {
|
||||
const conditionSetResult = {
|
||||
id: this.conditionSetDomainObject.identifier,
|
||||
conditionId: currentCondition.id
|
||||
},
|
||||
conditionId: currentCondition.id,
|
||||
value: outputValue,
|
||||
result,
|
||||
...timestamp
|
||||
};
|
||||
this.emit('conditionSetResultUpdated', conditionSetResult);
|
||||
}
|
||||
|
||||
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) {
|
||||
let data = undefined;
|
||||
if (this.testData.applied) {
|
||||
@ -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 = [
|
||||
{
|
||||
key: 'state',
|
||||
source: 'output',
|
||||
name: 'State',
|
||||
format: 'enum',
|
||||
enumerations: enumerations,
|
||||
string: 'true',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
string: 'false',
|
||||
value: false
|
||||
}
|
||||
];
|
||||
|
||||
const metaDataToReturn = {
|
||||
values: [
|
||||
{
|
||||
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) => {
|
||||
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 {
|
||||
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,6 +139,7 @@ export default {
|
||||
styleObj.isStyleInvisible = null;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
Object.entries(styleObj).forEach(([key, value]) => {
|
||||
if (typeof value !== 'string' || !value.includes('__no_value')) {
|
||||
elemToStyle.style[key] = value;
|
||||
@ -146,6 +147,7 @@ export default {
|
||||
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");
|
||||
|
@ -200,6 +200,7 @@ button {
|
||||
color: inherit;
|
||||
display: block;
|
||||
transition: transform $transOutTime;
|
||||
|
||||
&:before {
|
||||
content: $glyph-icon-arrow-down;
|
||||
font-family: symbolsfont;
|
||||
@ -314,6 +315,7 @@ button {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
}
|
||||
|
||||
/********* Disclosure Triangle */
|
||||
// Provides an arrow icon that when clicked expands an element to reveal its contents.
|
||||
// Used in tree items, plot legends. Always placed BEFORE an element.
|
||||
@ -368,8 +370,9 @@ button {
|
||||
section {
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
+ section {
|
||||
margin-top: $interiorMargin;
|
||||
//margin-top: $interiorMargin;
|
||||
}
|
||||
|
||||
.c-section__header {
|
||||
@ -377,11 +380,12 @@ section {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
margin-bottom: $interiorMargin;
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
gap: $interiorMarginSm;
|
||||
//margin-bottom: $interiorMargin;
|
||||
//
|
||||
//> * + * {
|
||||
// margin-left: $interiorMarginSm;
|
||||
//}
|
||||
}
|
||||
|
||||
> [class*='__label'] {
|
||||
@ -405,6 +409,7 @@ input[type='password'],
|
||||
input[type='date'],
|
||||
textarea {
|
||||
@include reactive-input();
|
||||
|
||||
&.numeric {
|
||||
text-align: right;
|
||||
}
|
||||
@ -463,6 +468,11 @@ textarea {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
&--md {
|
||||
// Smallish inputs, like numerics or short text
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
&--autocomplete {
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
@ -516,6 +526,7 @@ input[type='number'].c-input-number--no-spinners {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
@ -546,8 +557,7 @@ select {
|
||||
color: $colorSelectFg;
|
||||
box-shadow: $shdwSelect;
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
background-position:
|
||||
right 0.4em top 80%,
|
||||
background-position: right 0.4em top 80%,
|
||||
0 0;
|
||||
border: none;
|
||||
border-radius: $controlCr;
|
||||
@ -978,6 +988,7 @@ select {
|
||||
|
||||
.c-labeled-input {
|
||||
font-size: 0.9em;
|
||||
|
||||
input[type='number'] {
|
||||
width: 40px; // Number input sucks and must have size set using this method
|
||||
}
|
||||
@ -1095,6 +1106,7 @@ select {
|
||||
/******************************************************** SLIDERS */
|
||||
.c-slider {
|
||||
@include cControl();
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
@ -1137,10 +1149,12 @@ input[type='range'] {
|
||||
-webkit-appearance: none;
|
||||
@include sliderKnobRound($knobH);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
border: none;
|
||||
@include sliderKnobRound($knobH);
|
||||
}
|
||||
|
||||
&::-ms-thumb {
|
||||
border: none;
|
||||
@include sliderKnobRound($knobH);
|
||||
@ -1250,6 +1264,34 @@ input[type='range'] {
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************** DISCRETE ITEMS */
|
||||
// OUTPUT
|
||||
// Element that showcases an output value. Used in Condition Sets and Derived Telemetry
|
||||
|
||||
.c-output-featured {
|
||||
display: flex;
|
||||
gap: $interiorMargin;
|
||||
padding: 0 $interiorMargin;
|
||||
|
||||
> * {
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
|
||||
&__label {
|
||||
flex: 0 0 auto;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__value {
|
||||
$p: $interiorMargin * 2;
|
||||
//font-size: 1.25em;
|
||||
padding-left: $p;
|
||||
padding-right: $p;
|
||||
background: rgba(black, 0.2);
|
||||
border-radius: $basicCr;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************** LEGACY */
|
||||
.l-btn-set {
|
||||
display: flex;
|
||||
|
@ -42,503 +42,673 @@
|
||||
.icon-alert-rect {
|
||||
@include glyphBefore($glyph-icon-alert-rect);
|
||||
}
|
||||
|
||||
.icon-alert-triangle {
|
||||
@include glyphBefore($glyph-icon-alert-triangle);
|
||||
}
|
||||
|
||||
.icon-arrow-up {
|
||||
@include glyphBefore($glyph-icon-arrow-up);
|
||||
}
|
||||
|
||||
.icon-arrow-double-up {
|
||||
@include glyphBefore($glyph-icon-arrow-double-up);
|
||||
}
|
||||
|
||||
.icon-arrow-tall-up {
|
||||
@include glyphBefore($glyph-icon-arrow-tall-up);
|
||||
}
|
||||
|
||||
.icon-arrow-right {
|
||||
@include glyphBefore($glyph-icon-arrow-right);
|
||||
}
|
||||
|
||||
.icon-arrow-right-equilateral {
|
||||
@include glyphBefore($glyph-icon-arrow-right-equilateral);
|
||||
}
|
||||
|
||||
.icon-arrow-down {
|
||||
@include glyphBefore($glyph-icon-arrow-down);
|
||||
}
|
||||
|
||||
.icon-arrow-double-down {
|
||||
@include glyphBefore($glyph-icon-arrow-double-down);
|
||||
}
|
||||
|
||||
.icon-arrow-tall-down {
|
||||
@include glyphBefore($glyph-icon-arrow-tall-down);
|
||||
}
|
||||
|
||||
.icon-arrow-left {
|
||||
@include glyphBefore($glyph-icon-arrow-left);
|
||||
}
|
||||
|
||||
.icon-asterisk {
|
||||
@include glyphBefore($glyph-icon-asterisk);
|
||||
}
|
||||
|
||||
.icon-bell {
|
||||
@include glyphBefore($glyph-icon-bell);
|
||||
}
|
||||
|
||||
.icon-box-round-corners {
|
||||
@include glyphBefore($glyph-icon-box-round-corners);
|
||||
}
|
||||
|
||||
.icon-box-with-arrow {
|
||||
@include glyphBefore($glyph-icon-box-with-arrow);
|
||||
}
|
||||
|
||||
.icon-check {
|
||||
@include glyphBefore($glyph-icon-check);
|
||||
}
|
||||
|
||||
.icon-connectivity {
|
||||
@include glyphBefore($glyph-icon-connectivity);
|
||||
}
|
||||
|
||||
.icon-database-in-brackets {
|
||||
@include glyphBefore($glyph-icon-database-in-brackets);
|
||||
}
|
||||
|
||||
.icon-eye-open {
|
||||
@include glyphBefore($glyph-icon-eye-open);
|
||||
}
|
||||
|
||||
.icon-gear {
|
||||
@include glyphBefore($glyph-icon-gear);
|
||||
}
|
||||
|
||||
.icon-gear-after {
|
||||
@include glyphAfter($glyph-icon-gear);
|
||||
}
|
||||
|
||||
.icon-hourglass {
|
||||
@include glyphBefore($glyph-icon-hourglass);
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
@include glyphBefore($glyph-icon-info);
|
||||
}
|
||||
|
||||
.icon-link {
|
||||
@include glyphBefore($glyph-icon-link);
|
||||
}
|
||||
|
||||
.icon-lock {
|
||||
@include glyphBefore($glyph-icon-lock);
|
||||
}
|
||||
|
||||
.icon-minus {
|
||||
@include glyphBefore($glyph-icon-minus);
|
||||
}
|
||||
|
||||
.icon-people {
|
||||
@include glyphBefore($glyph-icon-people);
|
||||
}
|
||||
|
||||
.icon-person {
|
||||
@include glyphBefore($glyph-icon-person);
|
||||
}
|
||||
|
||||
.icon-plus {
|
||||
@include glyphBefore($glyph-icon-plus);
|
||||
}
|
||||
|
||||
.icon-plus-in-rect {
|
||||
@include glyphBefore($glyph-icon-plus-in-rect);
|
||||
}
|
||||
|
||||
.icon-trash {
|
||||
@include glyphBefore($glyph-icon-trash);
|
||||
}
|
||||
|
||||
.icon-x {
|
||||
@include glyphBefore($glyph-icon-x);
|
||||
}
|
||||
|
||||
.icon-brackets {
|
||||
@include glyphBefore($glyph-icon-brackets);
|
||||
}
|
||||
|
||||
.icon-crosshair {
|
||||
@include glyphBefore($glyph-icon-crosshair);
|
||||
}
|
||||
|
||||
.icon-grippy {
|
||||
@include glyphBefore($glyph-icon-grippy);
|
||||
}
|
||||
|
||||
.icon-grid {
|
||||
@include glyphBefore($glyph-icon-grid);
|
||||
}
|
||||
|
||||
.icon-grippy-ew {
|
||||
@include glyphBefore($glyph-icon-grippy-ew);
|
||||
}
|
||||
|
||||
.icon-columns {
|
||||
@include glyphBefore($glyph-icon-columns);
|
||||
}
|
||||
|
||||
.icon-rows {
|
||||
@include glyphBefore($glyph-icon-rows);
|
||||
}
|
||||
|
||||
.icon-filter {
|
||||
@include glyphBefore($glyph-icon-filter);
|
||||
}
|
||||
|
||||
.icon-filter-outline {
|
||||
@include glyphBefore($glyph-icon-filter-outline);
|
||||
}
|
||||
|
||||
.icon-suitcase {
|
||||
@include glyphBefore($glyph-icon-suitcase);
|
||||
}
|
||||
|
||||
.icon-cursor-lock {
|
||||
@include glyphBefore($glyph-icon-cursor-lock);
|
||||
}
|
||||
|
||||
.icon-flag {
|
||||
@include glyphBefore($glyph-icon-flag);
|
||||
}
|
||||
|
||||
.icon-eye-disabled {
|
||||
@include glyphBefore($glyph-icon-eye-disabled);
|
||||
}
|
||||
|
||||
.icon-notebook-page {
|
||||
@include glyphBefore($glyph-icon-notebook-page);
|
||||
}
|
||||
|
||||
.icon-unlocked {
|
||||
@include glyphBefore($glyph-icon-unlocked);
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
@include glyphBefore($glyph-icon-circle);
|
||||
}
|
||||
|
||||
.icon-draft {
|
||||
@include glyphBefore($glyph-icon-draft);
|
||||
}
|
||||
|
||||
.icon-question-mark {
|
||||
@include glyphBefore($glyph-icon-question-mark);
|
||||
}
|
||||
|
||||
.icon-circle-slash {
|
||||
@include glyphBefore($glyph-icon-circle-slash);
|
||||
}
|
||||
|
||||
.icon-status-poll-check {
|
||||
@include glyphBefore($glyph-icon-status-poll-check);
|
||||
}
|
||||
|
||||
.icon-status-poll-caution {
|
||||
@include glyphBefore($glyph-icon-status-poll-caution);
|
||||
}
|
||||
|
||||
.icon-status-poll-circle-slash {
|
||||
@include glyphBefore($glyph-icon-status-poll-circle-slash);
|
||||
}
|
||||
|
||||
.icon-status-poll-question-mark {
|
||||
@include glyphBefore($glyph-icon-status-poll-question-mark);
|
||||
}
|
||||
|
||||
.icon-status-poll-edit {
|
||||
@include glyphBefore($glyph-icon-status-poll-edit);
|
||||
}
|
||||
|
||||
.icon-stale {
|
||||
@include glyphBefore($glyph-icon-stale);
|
||||
}
|
||||
|
||||
.icon-arrows-right-left {
|
||||
@include glyphBefore($glyph-icon-arrows-right-left);
|
||||
}
|
||||
|
||||
.icon-arrows-up-down {
|
||||
@include glyphBefore($glyph-icon-arrows-up-down);
|
||||
}
|
||||
|
||||
.icon-bullet {
|
||||
@include glyphBefore($glyph-icon-bullet);
|
||||
}
|
||||
|
||||
.icon-calendar {
|
||||
@include glyphBefore($glyph-icon-calendar);
|
||||
}
|
||||
|
||||
.icon-chain-links {
|
||||
@include glyphBefore($glyph-icon-chain-links);
|
||||
}
|
||||
|
||||
.icon-download {
|
||||
@include glyphBefore($glyph-icon-download);
|
||||
}
|
||||
|
||||
.icon-duplicate {
|
||||
@include glyphBefore($glyph-icon-duplicate);
|
||||
}
|
||||
|
||||
.icon-folder-new {
|
||||
@include glyphBefore($glyph-icon-folder-new);
|
||||
}
|
||||
|
||||
.icon-fullscreen-collapse {
|
||||
@include glyphBefore($glyph-icon-fullscreen-collapse);
|
||||
}
|
||||
|
||||
.icon-fullscreen-expand {
|
||||
@include glyphBefore($glyph-icon-fullscreen-expand);
|
||||
}
|
||||
|
||||
.icon-layers {
|
||||
@include glyphBefore($glyph-icon-layers);
|
||||
}
|
||||
|
||||
.icon-line-horz {
|
||||
@include glyphBefore($glyph-icon-line-horz);
|
||||
}
|
||||
|
||||
.icon-magnify {
|
||||
@include glyphBefore($glyph-icon-magnify);
|
||||
}
|
||||
|
||||
.icon-magnify-in {
|
||||
@include glyphBefore($glyph-icon-magnify-in);
|
||||
}
|
||||
|
||||
.icon-magnify-out {
|
||||
@include glyphBefore($glyph-icon-magnify-out);
|
||||
}
|
||||
|
||||
.icon-menu-hamburger {
|
||||
@include glyphBefore($glyph-icon-menu-hamburger);
|
||||
}
|
||||
|
||||
.icon-move {
|
||||
@include glyphBefore($glyph-icon-move);
|
||||
}
|
||||
|
||||
.icon-new-window {
|
||||
@include glyphBefore($glyph-icon-new-window);
|
||||
}
|
||||
|
||||
.icon-paint-bucket {
|
||||
@include glyphBefore($glyph-icon-paint-bucket);
|
||||
}
|
||||
|
||||
.icon-pencil {
|
||||
@include glyphBefore($glyph-icon-pencil);
|
||||
}
|
||||
|
||||
.icon-pencil-in-brackets {
|
||||
@include glyphBefore($glyph-icon-pencil-in-brackets);
|
||||
}
|
||||
|
||||
.icon-play {
|
||||
@include glyphBefore($glyph-icon-play);
|
||||
}
|
||||
|
||||
.icon-pause {
|
||||
@include glyphBefore($glyph-icon-pause);
|
||||
}
|
||||
|
||||
.icon-plot-resource {
|
||||
@include glyphBefore($glyph-icon-plot-resource);
|
||||
}
|
||||
|
||||
.icon-pointer-left {
|
||||
@include glyphBefore($glyph-icon-pointer-left);
|
||||
}
|
||||
|
||||
.icon-pointer-right {
|
||||
@include glyphBefore($glyph-icon-pointer-right);
|
||||
}
|
||||
|
||||
.icon-refresh {
|
||||
@include glyphBefore($glyph-icon-refresh);
|
||||
}
|
||||
|
||||
.icon-save {
|
||||
@include glyphBefore($glyph-icon-save);
|
||||
}
|
||||
|
||||
.icon-save-as {
|
||||
@include glyphBefore($glyph-icon-save-as);
|
||||
}
|
||||
|
||||
.icon-sine {
|
||||
@include glyphBefore($glyph-icon-sine);
|
||||
}
|
||||
|
||||
.icon-font {
|
||||
@include glyphBefore($glyph-icon-font);
|
||||
}
|
||||
|
||||
.icon-thumbs-strip {
|
||||
@include glyphBefore($glyph-icon-thumbs-strip);
|
||||
}
|
||||
|
||||
.icon-two-parts-both {
|
||||
@include glyphBefore($glyph-icon-two-parts-both);
|
||||
}
|
||||
|
||||
.icon-two-parts-one-only {
|
||||
@include glyphBefore($glyph-icon-two-parts-one-only);
|
||||
}
|
||||
|
||||
.icon-resync {
|
||||
@include glyphBefore($glyph-icon-resync);
|
||||
}
|
||||
|
||||
.icon-reset {
|
||||
@include glyphBefore($glyph-icon-reset);
|
||||
}
|
||||
|
||||
.icon-x-in-circle {
|
||||
@include glyphBefore($glyph-icon-x-in-circle);
|
||||
}
|
||||
|
||||
.icon-brightness {
|
||||
@include glyphBefore($glyph-icon-brightness);
|
||||
}
|
||||
|
||||
.icon-contrast {
|
||||
@include glyphBefore($glyph-icon-contrast);
|
||||
}
|
||||
|
||||
.icon-expand {
|
||||
@include glyphBefore($glyph-icon-expand);
|
||||
}
|
||||
|
||||
.icon-list-view {
|
||||
@include glyphBefore($glyph-icon-list-view);
|
||||
}
|
||||
|
||||
.icon-grid-snap-to {
|
||||
@include glyphBefore($glyph-icon-grid-snap-to);
|
||||
}
|
||||
|
||||
.icon-grid-snap-no {
|
||||
@include glyphBefore($glyph-icon-grid-snap-no);
|
||||
}
|
||||
|
||||
.icon-frame-show {
|
||||
@include glyphBefore($glyph-icon-frame-show);
|
||||
}
|
||||
|
||||
.icon-frame-hide {
|
||||
@include glyphBefore($glyph-icon-frame-hide);
|
||||
}
|
||||
|
||||
.icon-import {
|
||||
@include glyphBefore($glyph-icon-import);
|
||||
}
|
||||
|
||||
.icon-export {
|
||||
@include glyphBefore($glyph-icon-export);
|
||||
}
|
||||
|
||||
.icon-font-size {
|
||||
@include glyphBefore($glyph-icon-font-size);
|
||||
}
|
||||
|
||||
.icon-clear-data {
|
||||
@include glyphBefore($glyph-icon-clear-data);
|
||||
}
|
||||
|
||||
.icon-history {
|
||||
@include glyphBefore($glyph-icon-history);
|
||||
}
|
||||
|
||||
.icon-arrow-nav-to-parent {
|
||||
@include glyphBefore($glyph-icon-arrow-nav-to-parent);
|
||||
}
|
||||
|
||||
.icon-crosshair-in-circle {
|
||||
@include glyphBefore($glyph-icon-crosshair-in-circle);
|
||||
}
|
||||
|
||||
.icon-target {
|
||||
@include glyphBefore($glyph-icon-target);
|
||||
}
|
||||
|
||||
.icon-items-collapse {
|
||||
@include glyphBefore($glyph-icon-items-collapse);
|
||||
}
|
||||
|
||||
.icon-items-expand {
|
||||
@include glyphBefore($glyph-icon-items-expand);
|
||||
}
|
||||
|
||||
.icon-3-dots {
|
||||
@include glyphBefore($glyph-icon-3-dots);
|
||||
}
|
||||
|
||||
.icon-grid-on {
|
||||
@include glyphBefore($glyph-icon-grid-on);
|
||||
}
|
||||
|
||||
.icon-grid-off {
|
||||
@include glyphBefore($glyph-icon-grid-off);
|
||||
}
|
||||
|
||||
.icon-camera {
|
||||
@include glyphBefore($glyph-icon-camera);
|
||||
}
|
||||
|
||||
.icon-folders-collapse {
|
||||
@include glyphBefore($glyph-icon-folders-collapse);
|
||||
}
|
||||
|
||||
.icon-activity {
|
||||
@include glyphBefore($glyph-icon-activity);
|
||||
}
|
||||
|
||||
.icon-activity-mode {
|
||||
@include glyphBefore($glyph-icon-activity-mode);
|
||||
}
|
||||
|
||||
.icon-autoflow-tabular {
|
||||
@include glyphBefore($glyph-icon-autoflow-tabular);
|
||||
}
|
||||
|
||||
.icon-clock {
|
||||
@include glyphBefore($glyph-icon-clock);
|
||||
}
|
||||
|
||||
.icon-database {
|
||||
@include glyphBefore($glyph-icon-database);
|
||||
}
|
||||
|
||||
.icon-database-query {
|
||||
@include glyphBefore($glyph-icon-database-query);
|
||||
}
|
||||
|
||||
.icon-dataset {
|
||||
@include glyphBefore($glyph-icon-dataset);
|
||||
}
|
||||
|
||||
.icon-datatable {
|
||||
@include glyphBefore($glyph-icon-datatable);
|
||||
}
|
||||
|
||||
.icon-dictionary {
|
||||
@include glyphBefore($glyph-icon-dictionary);
|
||||
}
|
||||
|
||||
.icon-folder {
|
||||
@include glyphBefore($glyph-icon-folder);
|
||||
}
|
||||
|
||||
.icon-image {
|
||||
@include glyphBefore($glyph-icon-image);
|
||||
}
|
||||
|
||||
.icon-layout {
|
||||
@include glyphBefore($glyph-icon-layout);
|
||||
}
|
||||
|
||||
.icon-object {
|
||||
@include glyphBefore($glyph-icon-object);
|
||||
}
|
||||
|
||||
.icon-object-unknown {
|
||||
@include glyphBefore($glyph-icon-object-unknown);
|
||||
}
|
||||
|
||||
.icon-packet {
|
||||
@include glyphBefore($glyph-icon-packet);
|
||||
}
|
||||
|
||||
.icon-page {
|
||||
@include glyphBefore($glyph-icon-page);
|
||||
}
|
||||
|
||||
.icon-plot-overlay {
|
||||
@include glyphBefore($glyph-icon-plot-overlay);
|
||||
}
|
||||
|
||||
.icon-plot-stacked {
|
||||
@include glyphBefore($glyph-icon-plot-stacked);
|
||||
}
|
||||
|
||||
.icon-session {
|
||||
@include glyphBefore($glyph-icon-session);
|
||||
}
|
||||
|
||||
.icon-tabular {
|
||||
@include glyphBefore($glyph-icon-tabular);
|
||||
}
|
||||
|
||||
.icon-tabular-lad {
|
||||
@include glyphBefore($glyph-icon-tabular-lad);
|
||||
}
|
||||
|
||||
.icon-tabular-lad-set {
|
||||
@include glyphBefore($glyph-icon-tabular-lad-set);
|
||||
}
|
||||
|
||||
.icon-tabular-realtime {
|
||||
@include glyphBefore($glyph-icon-tabular-realtime);
|
||||
}
|
||||
|
||||
.icon-tabular-scrolling {
|
||||
@include glyphBefore($glyph-icon-tabular-scrolling);
|
||||
}
|
||||
|
||||
.icon-telemetry {
|
||||
@include glyphBefore($glyph-icon-telemetry);
|
||||
}
|
||||
|
||||
.icon-timeline {
|
||||
@include glyphBefore($glyph-icon-timeline);
|
||||
}
|
||||
|
||||
.icon-timer {
|
||||
@include glyphBefore($glyph-icon-timer);
|
||||
}
|
||||
|
||||
.icon-topic {
|
||||
@include glyphBefore($glyph-icon-topic);
|
||||
}
|
||||
|
||||
.icon-box-with-dashed-lines {
|
||||
@include glyphBefore($glyph-icon-box-with-dashed-lines);
|
||||
}
|
||||
|
||||
.icon-summary-widget {
|
||||
@include glyphBefore($glyph-icon-summary-widget);
|
||||
}
|
||||
|
||||
.icon-notebook {
|
||||
@include glyphBefore($glyph-icon-notebook);
|
||||
}
|
||||
|
||||
.icon-tabs-view {
|
||||
@include glyphBefore($glyph-icon-tabs-view);
|
||||
}
|
||||
|
||||
.icon-flexible-layout {
|
||||
@include glyphBefore($glyph-icon-flexible-layout);
|
||||
}
|
||||
|
||||
.icon-generator-telemetry {
|
||||
@include glyphBefore($glyph-icon-generator-telemetry);
|
||||
}
|
||||
|
||||
.icon-generator-events {
|
||||
@include glyphBefore($glyph-icon-generator-events);
|
||||
}
|
||||
|
||||
.icon-gauge {
|
||||
@include glyphBefore($glyph-icon-gauge);
|
||||
}
|
||||
|
||||
.icon-spectra {
|
||||
@include glyphBefore($glyph-icon-spectra);
|
||||
}
|
||||
|
||||
.icon-spectra-telemetry {
|
||||
@include glyphBefore($glyph-icon-spectra-telemetry);
|
||||
}
|
||||
|
||||
.icon-command {
|
||||
@include glyphBefore($glyph-icon-command);
|
||||
}
|
||||
|
||||
.icon-conditional {
|
||||
@include glyphBefore($glyph-icon-conditional);
|
||||
}
|
||||
|
||||
.icon-condition-widget {
|
||||
@include glyphBefore($glyph-icon-condition-widget);
|
||||
}
|
||||
|
||||
.icon-alphanumeric {
|
||||
@include glyphBefore($glyph-icon-alphanumeric);
|
||||
}
|
||||
|
||||
.icon-image-telemetry {
|
||||
@include glyphBefore($glyph-icon-image-telemetry);
|
||||
}
|
||||
|
||||
.icon-telemetry-aggregate {
|
||||
@include glyphBefore($glyph-icon-telemetry-aggregate);
|
||||
}
|
||||
|
||||
.icon-bar-chart {
|
||||
@include glyphBefore($glyph-icon-bar-chart);
|
||||
}
|
||||
|
||||
.icon-map {
|
||||
@include glyphBefore($glyph-icon-map);
|
||||
}
|
||||
|
||||
.icon-plan {
|
||||
@include glyphBefore($glyph-icon-plan);
|
||||
}
|
||||
|
||||
.icon-timelist {
|
||||
@include glyphBefore($glyph-icon-timelist);
|
||||
}
|
||||
|
||||
.icon-plot-scatter {
|
||||
@include glyphBefore($glyph-icon-plot-scatter);
|
||||
}
|
||||
|
||||
.icon-notebook-shift-log {
|
||||
@include glyphBefore($glyph-icon-notebook-shift-log);
|
||||
}
|
||||
.icon-plot-scatter {
|
||||
@include glyphBefore($glyph-icon-plot-scatter);
|
||||
|
||||
.icon-derived-telemetry {
|
||||
@include glyphBefore($glyph-icon-derived-telemetry);
|
||||
}
|
||||
|
||||
/************************** 12 PX CLASSES */
|
||||
@ -546,18 +716,23 @@
|
||||
.icon-filter-12px {
|
||||
@include glyphBefore($glyph-icon-filter, 'symbolsfont-12px');
|
||||
}
|
||||
|
||||
.icon-filter-outline-12px {
|
||||
@include glyphBefore($glyph-icon-filter-outline, 'symbolsfont-12px');
|
||||
}
|
||||
|
||||
.icon-crosshair-12px {
|
||||
@include glyphBefore($glyph-icon-crosshair, 'symbolsfont-12px');
|
||||
}
|
||||
|
||||
.icon-folder-12px {
|
||||
@include glyphBefore($glyph-icon-folder, 'symbolsfont-12px');
|
||||
}
|
||||
|
||||
.icon-list-view-12px {
|
||||
@include glyphBefore($glyph-icon-list-view, 'symbolsfont-12px');
|
||||
}
|
||||
|
||||
.icon-grippy-12px {
|
||||
@include glyphBefore($glyph-icon-grippy, 'symbolsfont-12px');
|
||||
}
|
||||
@ -566,159 +741,215 @@
|
||||
.bg-icon-alert-rect {
|
||||
@include glyphBg($bg-icon-alert-rect);
|
||||
}
|
||||
|
||||
.bg-icon-alert-triangle {
|
||||
@include glyphBg($bg-icon-alert-triangle);
|
||||
}
|
||||
|
||||
.bg-icon-bell {
|
||||
@include glyphBg($bg-icon-bell);
|
||||
}
|
||||
|
||||
.bg-icon-info {
|
||||
@include glyphBg($bg-icon-info);
|
||||
}
|
||||
|
||||
.bg-icon-plus {
|
||||
@include glyphBg($bg-icon-plus);
|
||||
}
|
||||
|
||||
.bg-icon-grippy-ew {
|
||||
@include glyphBg($bg-icon-grippy-ew);
|
||||
}
|
||||
|
||||
.bg-icon-chain-links {
|
||||
@include glyphBg($bg-icon-chain-links);
|
||||
}
|
||||
|
||||
.bg-icon-clock {
|
||||
@include glyphBg($bg-icon-clock);
|
||||
}
|
||||
|
||||
.bg-icon-database {
|
||||
@include glyphBg($bg-icon-database);
|
||||
}
|
||||
|
||||
.bg-icon-database-query {
|
||||
@include glyphBg($bg-icon-database-query);
|
||||
}
|
||||
|
||||
.bg-icon-dataset {
|
||||
@include glyphBg($bg-icon-dataset);
|
||||
}
|
||||
|
||||
.bg-icon-datatable {
|
||||
@include glyphBg($bg-icon-datatable);
|
||||
}
|
||||
|
||||
.bg-icon-dictionary {
|
||||
@include glyphBg($bg-icon-dictionary);
|
||||
}
|
||||
|
||||
.bg-icon-folder {
|
||||
@include glyphBg($bg-icon-folder);
|
||||
}
|
||||
|
||||
.bg-icon-image {
|
||||
@include glyphBg($bg-icon-image);
|
||||
}
|
||||
|
||||
.bg-icon-layout {
|
||||
@include glyphBg($bg-icon-layout);
|
||||
}
|
||||
|
||||
.bg-icon-object {
|
||||
@include glyphBg($bg-icon-object);
|
||||
}
|
||||
|
||||
.bg-icon-object-unknown {
|
||||
@include glyphBg($bg-icon-object-unknown);
|
||||
}
|
||||
|
||||
.bg-icon-packet {
|
||||
@include glyphBg($bg-icon-packet);
|
||||
}
|
||||
|
||||
.bg-icon-page {
|
||||
@include glyphBg($bg-icon-page);
|
||||
}
|
||||
|
||||
.bg-icon-plot-overlay {
|
||||
@include glyphBg($bg-icon-plot-overlay);
|
||||
}
|
||||
|
||||
.bg-icon-plot-stacked {
|
||||
@include glyphBg($bg-icon-plot-stacked);
|
||||
}
|
||||
|
||||
.bg-icon-session {
|
||||
@include glyphBg($bg-icon-session);
|
||||
}
|
||||
|
||||
.bg-icon-tabular {
|
||||
@include glyphBg($bg-icon-tabular);
|
||||
}
|
||||
|
||||
.bg-icon-tabular-lad {
|
||||
@include glyphBg($bg-icon-tabular-lad);
|
||||
}
|
||||
|
||||
.bg-icon-tabular-lad-set {
|
||||
@include glyphBg($bg-icon-tabular-lad-set);
|
||||
}
|
||||
|
||||
.bg-icon-tabular-scrolling {
|
||||
@include glyphBg($bg-icon-tabular-scrolling);
|
||||
}
|
||||
|
||||
.bg-icon-telemetry {
|
||||
@include glyphBg($bg-icon-telemetry);
|
||||
}
|
||||
|
||||
.bg-icon-timeline {
|
||||
@include glyphBg($bg-icon-timeline);
|
||||
}
|
||||
|
||||
.bg-icon-timer {
|
||||
@include glyphBg($bg-icon-timer);
|
||||
}
|
||||
|
||||
.bg-icon-box-with-dashed-lines {
|
||||
@include glyphBg($bg-icon-box-with-dashed-lines);
|
||||
}
|
||||
|
||||
.bg-icon-summary-widget {
|
||||
@include glyphBg($bg-icon-summary-widget);
|
||||
}
|
||||
|
||||
.bg-icon-notebook {
|
||||
@include glyphBg($bg-icon-notebook);
|
||||
}
|
||||
|
||||
.bg-icon-tabs-view {
|
||||
@include glyphBg($bg-icon-tabs-view);
|
||||
}
|
||||
|
||||
.bg-icon-flexible-layout {
|
||||
@include glyphBg($bg-icon-flexible-layout);
|
||||
}
|
||||
|
||||
.bg-icon-generator-telemetry {
|
||||
@include glyphBg($bg-icon-generator-telemetry);
|
||||
}
|
||||
|
||||
.bg-icon-generator-events {
|
||||
@include glyphBg($bg-icon-generator-events);
|
||||
}
|
||||
|
||||
.bg-icon-gauge {
|
||||
@include glyphBg($bg-icon-gauge);
|
||||
}
|
||||
|
||||
.bg-icon-spectra {
|
||||
@include glyphBg($bg-icon-spectra);
|
||||
}
|
||||
|
||||
.bg-icon-spectra-telemetry {
|
||||
@include glyphBg($bg-icon-spectra-telemetry);
|
||||
}
|
||||
|
||||
.bg-icon-command {
|
||||
@include glyphBg($bg-icon-command);
|
||||
}
|
||||
|
||||
.bg-icon-conditional {
|
||||
@include glyphBg($bg-icon-conditional);
|
||||
}
|
||||
|
||||
.bg-icon-condition-widget {
|
||||
@include glyphBg($bg-icon-condition-widget);
|
||||
}
|
||||
|
||||
.bg-icon-bar-chart {
|
||||
@include glyphBg($bg-icon-bar-chart);
|
||||
}
|
||||
|
||||
.bg-icon-map {
|
||||
@include glyphBg($bg-icon-map);
|
||||
}
|
||||
|
||||
.bg-icon-plan {
|
||||
@include glyphBg($bg-icon-plan);
|
||||
}
|
||||
|
||||
.bg-icon-timelist {
|
||||
@include glyphBg($bg-icon-timelist);
|
||||
}
|
||||
|
||||
.bg-icon-plot-scatter {
|
||||
@include glyphBg($bg-icon-plot-scatter);
|
||||
}
|
||||
|
||||
.bg-icon-notebook-shift-log {
|
||||
@include glyphBg($bg-icon-notebook-shift-log);
|
||||
}
|
||||
|
||||
.bg-icon-telemetry-aggregate {
|
||||
@include glyphBg($bg-icon-telemetry-aggregate);
|
||||
}
|
||||
|
||||
.bg-icon-trash {
|
||||
@include glyphBg($bg-icon-trash);
|
||||
}
|
||||
|
||||
.bg-icon-eye-open {
|
||||
@include glyphBg($bg-icon-eye-open);
|
||||
}
|
||||
|
||||
.bg-icon-camera {
|
||||
@include glyphBg($bg-icon-camera);
|
||||
}
|
||||
|
||||
.bg-icon-derived-telemetry {
|
||||
@include glyphBg($bg-icon-derived-telemetry);
|
||||
}
|
||||
|
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