Compare commits

...

108 Commits

Author SHA1 Message Date
67703902ec Merge branch 'master' into telemetry-comps 2025-03-07 08:49:50 -08:00
5e15af120b Merge branch 'master' into telemetry-comps 2024-12-11 16:58:25 +01:00
5683320bea Merge remote-tracking branch 'origin/master' into telemetry-comps 2024-11-15 12:30:15 +01:00
64ae96e602 Closes #7823
- Markup/CSS sanding and shimming for derived telemetry front-end.
- Toggle switch CSS improved to use `gap` instead of left margin.
2024-11-15 12:29:32 +01:00
55063a0045 removing debug statements for PR review 2024-10-28 09:24:23 +01:00
9ca489cf12 widen size to accomodate sample size 2024-10-28 09:01:36 +01:00
4a4dabf7f0 Merge remote-tracking branch 'origin/master' into telemetry-comps 2024-10-28 08:43:49 +01:00
96b1ef0db5 reduce size 2024-10-22 21:02:10 +02:00
1516524f0b support lambda values 2024-10-14 15:28:19 +02:00
51f73bbfbe Merge remote-tracking branch 'origin/master' into telemetry-comps 2024-10-14 09:33:29 +02:00
00c8c0e095 Merge remote-tracking branch 'origin/master' into telemetry-comps 2024-10-11 11:37:34 +02:00
7d46afbced add placeholder 2024-10-10 11:15:04 +02:00
6710ad0d4c remove unused function 2024-10-10 10:34:17 +02:00
d5ee430f8b update output when comp changes 2024-10-10 10:31:16 +02:00
3b78d4d2bf fix output and add accumulation label 2024-10-10 09:38:34 +02:00
4de03542f8 impute requested data properly 2024-10-09 22:15:53 +02:00
3b06c7749e change to just value 2024-10-09 17:56:02 +02:00
3be414e2e7 Merge remote-tracking branch 'origin/master' into telemetry-comps-with-acc 2024-10-09 13:53:23 +02:00
85a77699e6 check for blank test values 2024-10-09 13:35:46 +02:00
d6f50567bc allow blank test values for arrays 2024-10-09 13:32:56 +02:00
bf9d5ef4a7 add a few more awaits 2024-10-09 13:20:11 +02:00
7c01a5e0d7 ensure we are checking for not equal 2024-10-09 12:47:03 +02:00
ba7f2919c0 can do derived derived accumulated comps now 2024-10-09 12:36:20 +02:00
2de628adac ensure we have a reference value before we slice 2024-10-09 12:16:02 +02:00
8e7bfd080d ensure number for sample size 2024-10-09 12:04:28 +02:00
c6806944eb check if sample size greater than zero 2024-10-09 10:24:57 +02:00
3f92deb896 add sample size 2024-10-09 10:24:08 +02:00
395436a361 works 2024-10-09 09:32:44 +02:00
d859322a47 add new flag to deal with plots asking for zoomed out data that should be ignoring clock 2024-10-04 14:29:52 +02:00
58d6cdb574 some changes 2024-10-04 11:29:00 +02:00
f718ccdf4e stacked plots are overriding telemetry object configurations 2024-10-04 11:14:45 +02:00
38316bd2f5 more debug just in case 2024-10-04 09:46:51 +02:00
4bbbd17b61 more telemetry collection changes to acommodate bound changes 2024-10-03 15:24:48 +02:00
154e8c695d fix slow loading errors 2024-10-03 14:47:12 +02:00
8e5ac68360 improvement in loading 2024-10-03 14:10:45 +02:00
1a9401039e Merge remote-tracking branch 'origin/master' into telemetry-comps 2024-10-02 16:33:02 +02:00
5e013b6aa8 ensure we only ask for latest for comp editor 2024-10-01 21:28:50 +02:00
eb5d32c2a0 fix domains 2024-10-01 18:02:07 +02:00
4a301a15d2 ensure derived DERIVED telemetry loads in the proper order. also have telemetry collections prioritize request options over clock 2024-09-25 13:19:00 +02:00
dfcfa47237 pass options on request to underlying collections 2024-09-24 21:32:56 +02:00
ee612a6b5a resolve conflicts 2024-09-17 16:45:43 +02:00
c1a361db5f Merge branch 'master' into telemetry-comps 2024-09-11 15:59:27 +02:00
e02217ad63 Merge branch 'master' into telemetry-comps 2024-09-10 10:28:36 +02:00
60e07e642b refactor aria labeling 2024-09-10 10:27:45 +02:00
8b4eed938a good job code scanner. changing to or condition 2024-09-09 09:45:22 +02:00
dd57d78c4f add some basic object path label tests 2024-09-07 11:30:14 +02:00
89e18d482d add output format test 2024-09-06 11:50:47 +02:00
b251fde1fc ensure output format propagates 2024-09-06 11:27:20 +02:00
1b186d7596 add tests 2024-09-06 10:47:20 +02:00
59461d6b06 Enhancements for Derived Telemetry object
- Closes #7823
- Final sanding and polishing.
- New discrete item style `c-output-featured` added to controls.scss
  - Applied to 'Current Output' section of both Derived Telemetry and Condition Sets.
- TODOs:
  - [ ] Check for low-risk regressions in Condition Sets browse and edit modes.
  - [ ] New tests for ObjectPathString.vue component.
2024-09-05 17:12:07 -07:00
1615c36b7e Merge branch 'telemetry-comps' of github.com:nasa/openmct into telemetry-comps 2024-09-05 15:03:21 -07:00
80d8babb61 handle arrays and add skeleton tests 2024-09-05 21:43:35 +02:00
fdcece8e0e Merge branch 'telemetry-comps' of github.com:nasa/openmct into telemetry-comps 2024-09-05 12:13:51 -07:00
cde34e3c6d Enhancements for Derived Telemetry object
- Closes #7823
- Updated Derived Telemetry glyph design.
- Set `CompsViewProvider.js` to use the correct icon.
2024-09-05 12:11:09 -07:00
dde0d1a64d can delete properly 2024-09-05 15:55:15 +02:00
1996e66891 can chain and plot derived data 2024-09-05 15:38:41 +02:00
06e916ea20 allow deleting of parameters 2024-09-05 15:27:09 +02:00
fb8730cf22 clean up composition effects 2024-09-05 13:44:08 +02:00
9aeb454fb6 trying this again without blowing away the very pretty styling 2024-09-05 09:34:34 +02:00
02cf70135e Revert "do not apply test data when edit mode changes to false"
This reverts commit eac4676cad.
2024-09-05 09:31:08 +02:00
eac4676cad do not apply test data when edit mode changes to false 2024-09-05 09:28:35 +02:00
e1f50feda0 Enhancements for Derived Telemetry object
- Closes #7823
- Code cleanup and tweaks.
2024-09-04 18:22:50 -07:00
f8ceaa5a83 Merge branch 'telemetry-comps' of github.com:nasa/openmct into telemetry-comps
# Conflicts:
#	src/plugins/comps/components/CompsView.vue
2024-09-04 17:41:34 -07:00
1d686c2926 Style enhancements for Derived Telemetry object
- Closes #7823
- Significant mods for style and CSS classing. Still WIP, more coming.
2024-09-04 17:33:36 -07:00
2793da40ec Enhancements for Derived Telemetry object
- Closes #7823
- Added new `ObjectPathString.vue` UI component, based on `ObjectPath.vue`.
2024-09-04 17:32:43 -07:00
cce30084ee Style enhancements for Derived Telemetry object
- Closes #7823
- Tweak to object description.
2024-09-04 11:58:57 -07:00
4260cc3603 Style enhancements for Derived Telemetry object
- Closes #7823
- Font files updated - do not merge!
- New glyph and data URI background for object.
2024-09-04 11:58:33 -07:00
3ad21ee01e use key instead of name 2024-09-04 18:49:54 +02:00
ccc12deacd lint 2024-09-04 15:47:18 +02:00
40a95cfa08 lint 2024-09-04 15:36:54 +02:00
5894363ba5 add icons and fix errant telemetry 2024-09-04 15:28:00 +02:00
1c68c7e044 be consistent with output key 2024-09-03 16:48:17 +02:00
5af6413e01 implement output format 2024-09-03 15:30:44 +02:00
386c3b4131 implement output format 2024-09-03 15:30:26 +02:00
28ec13a532 mutate when parameters or expression changes 2024-08-22 07:12:50 -05:00
d96c3fc537 fix editing issues 2024-08-21 08:28:16 -05:00
58568b849e persist after adding 2024-08-20 13:24:26 -05:00
e4dcda80f1 do not persist 2024-08-20 13:01:10 -05:00
a9d63b9272 fix test data 2024-08-20 10:56:34 -05:00
d4b2986651 expression validator 2024-08-19 15:06:55 -05:00
d7d79130ac fix initial load 2024-08-19 14:56:20 -05:00
d6c8beeeac Merge remote-tracking branch 'origin/master' into telemetry-comps 2024-08-19 11:37:32 -05:00
4d546fb63f styles 2024-08-16 16:01:42 -05:00
aa8750eb97 memory conserved 2024-08-15 16:39:29 -05:00
446c8119c3 gui still leaking data 2024-08-15 16:07:13 -05:00
484a81b370 get rid of batch for now 2024-08-15 15:45:09 -05:00
df3ca84fda timesystem works 2024-08-15 14:04:50 -05:00
87dc272a5a works with time 2024-08-15 13:21:16 -05:00
dcc893880b need to figure out ranger 2024-08-14 17:32:26 -05:00
9e3e7394d2 ui works kinda 2024-08-14 16:34:37 -05:00
f174515c9e styles and timing for loading worker 2024-08-14 15:05:15 -05:00
413338d3e2 can take arbitrary expressions 2024-08-13 16:21:08 -05:00
0326e38f5d parameters 2024-08-13 11:15:47 -05:00
e5719fc71b correctly persist 2024-08-13 09:54:45 -05:00
a0a2ead5e7 rudimentary expression editor 2024-08-12 17:18:25 -05:00
0113ec08db fix plots 2024-08-12 16:34:09 -05:00
a94c752616 slowing adding gui 2024-08-08 15:35:30 +02:00
55c8609953 Merge remote-tracking branch 'origin/master' into telemetry-comps 2024-08-08 14:02:04 +02:00
37d222fd87 subscriptions work 2024-08-06 15:50:24 +02:00
90a24b380f exact values enabled 2024-08-05 16:26:54 +02:00
69db534042 adding works 2024-08-02 17:58:14 +02:00
ce5e435ae0 can add two sin waves 2024-08-02 17:25:08 +02:00
74c3a95ca3 telemetry is null for some reason 2024-08-02 16:26:15 +02:00
ff814544c6 worker ready 2024-08-02 15:57:15 +02:00
0dd04426f5 add comps manager 2024-08-01 17:27:12 +02:00
f5d7a33915 more drafts 2024-07-30 16:44:14 +02:00
6c74e84e11 more scaffolding 2024-07-29 18:37:21 +02:00
431600342f skeleton for comps 2024-07-25 11:55:59 +02:00
34 changed files with 4541 additions and 1451 deletions

View File

@ -484,6 +484,7 @@
"darkmatter",
"Undeletes",
"SSSZ",
"LOCF",
"pageerror"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],

View File

@ -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: [

View File

@ -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({

View 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$/);
});
});

101
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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.

View File

@ -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.
*

View File

@ -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);
}
/**

View 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();
}
}
};
}
}

View 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];
}
}

View 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;
}

View 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;
}
}

View 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);
}
}

View 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;
}
}
}

View 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>

View 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>

View 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;
}
}

View 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));
};
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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)

View File

@ -249,7 +249,8 @@ export default {
...persistedSeriesConfig.series
}
],
yAxis: persistedSeriesConfig.yAxis
yAxis: persistedSeriesConfig.yAxis,
...this.childObject.configuration
}
},
openmct: this.openmct,

View File

@ -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';
@ -176,5 +177,6 @@ plugins.Gauge = GaugePlugin;
plugins.Timelist = TimeList;
plugins.InspectorViews = InspectorViews;
plugins.InspectorDataVisualization = InspectorDataVisualization;
plugins.Comps = CompsPlugin;
export default plugins;

View File

@ -356,7 +356,7 @@ $colorInspectorBg: $colorBodyBg;
$colorInspectorFg: $colorBodyFg;
$colorInspectorPropName: $colorBodyFgSubtle;
$colorInspectorPropVal: $colorBodyFgEm;
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%);
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 10%);
$colorInspectorSectionHeaderFg: #bfbfbf;
// Tabs

View File

@ -277,8 +277,9 @@ $glyph-icon-bar-chart: '\eb2c';
$glyph-icon-map: '\eb2d';
$glyph-icon-plan: '\eb2e';
$glyph-icon-timelist: '\eb2f';
$glyph-icon-notebook-shift-log: '\eb31';
$glyph-icon-plot-scatter: '\eb30';
$glyph-icon-notebook-shift-log: '\eb31';
$glyph-icon-derived-telemetry: '\eb32';
/************************** GLYPHS AS DATA URI */
// Only objects have been converted, for use in Create menu and folder views
@ -335,3 +336,4 @@ $bg-icon-telemetry-aggregate: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns
$bg-icon-trash: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='512px' height='512px' viewBox='0 0 512 512' enable-background='new 0 0 512 512' xml:space='preserve'%3e%3cpath d='M416,64h-96.18V32c0-17.6-14.4-32-32-32h-64c-17.6,0-32,14.4-32,32v32H96c-52.8,0-96,36-96,80s0,80,0,80h32v192 c0,52.8,43.2,96,96,96h256c52.8,0,96-43.2,96-96V224h32c0,0,0-36,0-80S468.8,64,416,64z M160,416H96V224h64V416z M288,416h-64V224 h64V416z M416,416h-64V224h64V416z'/%3e%3c/svg%3e");
$bg-icon-eye-open: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 512 512' style='enable-background:new 0 0 512 512;' xml:space='preserve'%3e%3cstyle type='text/css'%3e .st0%7bfill:%2300A14B;%7d %3c/style%3e%3ctitle%3eicon-eye-open-v2%3c/title%3e%3cg%3e%3cpath class='st0' d='M256,58.2c-122.9,0-226.1,84-255.4,197.8C29.9,369.7,133.1,453.8,256,453.8s226.1-84,255.4-197.8 C482.1,142.3,378.9,58.2,256,58.2z M414.6,294.2c-11.3,17.2-25.3,32.4-41.5,45.2c-16.4,12.9-34.5,22.8-54,29.7 c-20.2,7.1-41.4,10.7-63,10.7s-42.9-3.6-63-10.7c-19.5-6.9-37.7-16.9-54-29.7c-16.2-12.8-30.2-27.9-41.5-45.2 c-7.9-12-14.4-24.8-19.3-38.2c5-13.4,11.5-26.2,19.3-38.2c11.3-17.2,25.3-32.4,41.5-45.2c16.4-12.9,34.5-22.8,54-29.7 c20.2-7.1,41.4-10.7,63-10.7s42.9,3.6,63,10.7c19.5,6.9,37.7,16.9,54,29.7c16.2,12.8,30.2,27.9,41.5,45.2 c7.9,12,14.4,24.8,19.3,38.2C429,269.4,422.5,282.2,414.6,294.2z'/%3e%3ccircle class='st0' cx='256' cy='256' r='96'/%3e%3c/g%3e%3c/svg%3e");
$bg-icon-camera: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3ctitle%3eicon-camera-v2%3c/title%3e%3cpath d='M448,128H384L320,0H192L128,128H64A64.2,64.2,0,0,0,0,192V448a64.2,64.2,0,0,0,64,64H448a64.2,64.2,0,0,0,64-64V192A64.2,64.2,0,0,0,448,128ZM256,432A128,128,0,1,1,384,304,128,128,0,0,1,256,432Z'/%3e%3c/svg%3e");
$bg-icon-derived-telemetry: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M66.1 166c20.2 24.3 35.1 54.6 44 75.7 10 23.6 21.7 44 33.1 57.7 11.1 13.3 18.5 16.3 20.2 16.3s9.1-3 20.2-16.3c11.4-13.7 23.1-34.2 33.1-57.7 8.9-21.1 23.8-51.4 44-75.7 23.3-28.1 48.7-42.3 75.6-42.3s52.2 14.2 75.6 42.3c20.2 24.3 35.1 54.6 44 75.7 10 23.6 21.7 44 33.1 57.7 11.1 13.3 18.5 16.3 20.2 16.3s1.6-.3 3.2-1.1v-58.9c-.2-141.3-114.9-256-256.2-256H.2v124.6c23.3 3 45.4 17 66 41.7Z'/%3e%3cpath d='M509 387.7c-26.8 0-52.2-14.2-75.6-42.3-20.2-24.3-35.1-54.6-44-75.7-10-23.6-21.7-44-33.1-57.7-11.1-13.3-18.5-16.3-20.2-16.3s-9.1 3-20.2 16.3c-11.4 13.7-23.1 34.2-33.1 57.7-8.9 21.1-23.8 51.4-44 75.7-23.3 28.1-48.7 42.3-75.6 42.3s-52.2-14.2-75.6-42.3c-20.2-24.3-35.1-54.6-44-75.7-10-23.6-21.7-44-33.1-57.7-4.1-4.9-7.6-8.4-10.6-10.8v54.5c.3 141.4 114.9 256 256.3 256h256V387.6H509Z'/%3e%3c/svg%3e");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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';

View 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>

View File

@ -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;
}