mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 18:50:11 +00:00
Compare commits
6 Commits
telemetry-
...
eval-sourc
Author | SHA1 | Date | |
---|---|---|---|
204ad0b0e9 | |||
adc6f8edd5 | |||
11fe7537df | |||
cc58dbd5e7 | |||
c19c4e7065 | |||
3bb4df8d39 |
@ -483,9 +483,7 @@
|
||||
"countup",
|
||||
"darkmatter",
|
||||
"Undeletes",
|
||||
"SSSZ",
|
||||
"LOCF",
|
||||
"pageerror"
|
||||
"SSSZ"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
|
||||
"ignorePaths": [
|
||||
|
@ -14,8 +14,7 @@ const config = {
|
||||
__OPENMCT_VERSION__: 'readonly',
|
||||
__OPENMCT_BUILD_DATE__: 'readonly',
|
||||
__OPENMCT_REVISION__: 'readonly',
|
||||
__OPENMCT_BUILD_BRANCH__: 'readonly',
|
||||
__OPENMCT_ROOT_RELATIVE__: 'readonly'
|
||||
__OPENMCT_BUILD_BRANCH__: 'readonly'
|
||||
},
|
||||
plugins: ['prettier', 'unicorn', 'simple-import-sort'],
|
||||
extends: [
|
||||
|
10
.github/workflows/e2e-couchdb.yml
vendored
10
.github/workflows/e2e-couchdb.yml
vendored
@ -51,7 +51,7 @@ jobs:
|
||||
env:
|
||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
|
||||
run: npm run test:e2e:couchdb
|
||||
|
||||
|
||||
- name: Generate Code Coverage Report
|
||||
run: npm run cov:e2e:report
|
||||
|
||||
@ -66,19 +66,15 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: e2e-couchdb-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Archive html test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: e2e-couchdb-html-test-results
|
||||
path: html-test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:couchdb label (if present)
|
||||
if: always()
|
||||
|
4
.github/workflows/e2e-flakefinder.yml
vendored
4
.github/workflows/e2e-flakefinder.yml
vendored
@ -38,11 +38,9 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: e2e-flakefinder-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:flakefinder label (if present)
|
||||
if: always()
|
||||
|
4
.github/workflows/e2e-perf.yml
vendored
4
.github/workflows/e2e-perf.yml
vendored
@ -35,11 +35,9 @@ jobs:
|
||||
- run: npm run test:perf:memory
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: e2e-perf-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:perf label (if present)
|
||||
if: always()
|
||||
|
4
.github/workflows/e2e-pr.yml
vendored
4
.github/workflows/e2e-pr.yml
vendored
@ -45,11 +45,9 @@ jobs:
|
||||
npm run cov:e2e:full:publish
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: e2e-pr-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e label (if present)
|
||||
if: always()
|
||||
|
@ -48,7 +48,6 @@ 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'
|
||||
@ -90,8 +89,7 @@ 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_HYDRATION_MISMATCH_DETAILS__: false, // enable/disable hydration mismatch details in production, default: false
|
||||
__VUE_PROD_DEVTOOLS__: false // enable/disable devtools support in production, default: false
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
|
@ -15,5 +15,5 @@ export default merge(common, {
|
||||
__OPENMCT_ROOT_RELATIVE__: '""'
|
||||
})
|
||||
],
|
||||
devtool: 'source-map'
|
||||
devtool: 'eval-source-map'
|
||||
});
|
||||
|
@ -682,21 +682,6 @@ async function linkParameterToObject(page, parameterName, objectName) {
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the currently viewed `domainObject` from the browse bar.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function renameCurrentObjectFromBrowseBar(page, newName) {
|
||||
const nameInput = page.getByLabel('Browse bar object name');
|
||||
await nameInput.click();
|
||||
await nameInput.fill('');
|
||||
await nameInput.fill(newName);
|
||||
// Click the browse bar container to save changes
|
||||
await page.getByLabel('Browse bar', { exact: true }).click();
|
||||
}
|
||||
|
||||
export {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
@ -708,7 +693,6 @@ export {
|
||||
linkParameterToObject,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
navigateToObjectWithRealTime,
|
||||
renameCurrentObjectFromBrowseBar,
|
||||
setEndOffset,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
|
@ -1,67 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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, setRealTimeMode } from '../../../appActions.js';
|
||||
import { MISSION_TIME } from '../../../constants.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const TELEMETRY_RATE = 2500;
|
||||
|
||||
test.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Event Message Generator with Acknowledge'
|
||||
});
|
||||
});
|
||||
|
||||
test('Rows are updatable in place', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7938'
|
||||
});
|
||||
|
||||
await test.step('First telemetry datum gets added as new row', async () => {
|
||||
await page.clock.fastForward(TELEMETRY_RATE);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK');
|
||||
});
|
||||
|
||||
await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => {
|
||||
await page.clock.fastForward(TELEMETRY_RATE * 2);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(acknowledgeCell).toHaveAttribute('title', 'OK');
|
||||
});
|
||||
});
|
||||
});
|
@ -24,9 +24,7 @@ import {
|
||||
createDomainObjectWithDefaults,
|
||||
createPlanFromJSON,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
setTimeConductorBounds
|
||||
setFixedIndependentTimeConductorBounds
|
||||
} from '../../../appActions.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
@ -76,14 +74,21 @@ const testPlan = {
|
||||
};
|
||||
|
||||
test.describe('Time Strip', () => {
|
||||
let timestrip;
|
||||
let plan;
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5627'
|
||||
});
|
||||
|
||||
// Constant locators
|
||||
const activityBounds = page.locator('.activity-bounds');
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Goto baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
timestrip = await test.step('Create a Time Strip', async () => {
|
||||
const timestrip = await test.step('Create a Time Strip', async () => {
|
||||
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
||||
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||
expect(objectName).toBe(createdTimeStrip.name);
|
||||
@ -91,7 +96,7 @@ test.describe('Time Strip', () => {
|
||||
return createdTimeStrip;
|
||||
});
|
||||
|
||||
plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||
const plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||
const createdPlan = await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan
|
||||
@ -105,22 +110,6 @@ test.describe('Time Strip', () => {
|
||||
.dragTo(page.getByLabel('Object View'));
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
return createdPlan;
|
||||
});
|
||||
});
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5627'
|
||||
});
|
||||
|
||||
// Constant locators
|
||||
const activityBounds = page.locator('.activity-bounds');
|
||||
|
||||
await test.step('Set time strip to fixed timespan mode and verify activities', async () => {
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
|
||||
@ -130,6 +119,8 @@ test.describe('Time Strip', () => {
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.locator('.activity-bounds').count();
|
||||
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
||||
|
||||
return createdPlan;
|
||||
});
|
||||
|
||||
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
|
||||
@ -186,48 +177,4 @@ test.describe('Time Strip', () => {
|
||||
expect(await activityBounds.count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Time strip now line', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7817'
|
||||
});
|
||||
|
||||
await test.step('Is displayed in realtime mode', async () => {
|
||||
await expect(page.getByLabel('Now Marker')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Is hidden when out of bounds of the time axis', async () => {
|
||||
// Switch to fixed timespan mode
|
||||
await setFixedTimeMode(page);
|
||||
// Get the end bounds
|
||||
const endBounds = await page.getByLabel('End bounds').textContent();
|
||||
|
||||
// Add 2 minutes to end bound datetime and use it as the new end time
|
||||
let endTimeStamp = new Date(endBounds);
|
||||
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() + 2);
|
||||
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||
const milliseconds = endTimeStamp.getMilliseconds();
|
||||
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||
|
||||
// Subtract 1 minute from the end bound and use it as the new start time
|
||||
let startTimeStamp = new Date(endBounds);
|
||||
startTimeStamp.setUTCMinutes(startTimeStamp.getUTCMinutes() + 1);
|
||||
const startDate = startTimeStamp.toISOString().split('T')[0];
|
||||
const startMilliseconds = startTimeStamp.getMilliseconds();
|
||||
const startTime = startTimeStamp
|
||||
.toISOString()
|
||||
.split('T')[1]
|
||||
.replace(`.${startMilliseconds}Z`, '');
|
||||
// Set fixed timespan mode to the future so that "now" is out of bounds.
|
||||
await setTimeConductorBounds(page, {
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime
|
||||
});
|
||||
|
||||
await expect(page.getByLabel('Now Marker')).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,111 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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$/);
|
||||
});
|
||||
});
|
@ -287,41 +287,6 @@ test.describe('Basic Condition Set Use', () => {
|
||||
description: 'https://github.com/nasa/openmct/issues/7484'
|
||||
});
|
||||
});
|
||||
|
||||
test('ConditionSet has add criteria button enabled/disabled when composition is and is not available', async ({
|
||||
page
|
||||
}) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.goto(conditionSet.url);
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Create a condition
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
|
||||
|
||||
// Validate that the add criteria button is disabled
|
||||
await expect(page.getByLabel('Add Criteria - Disabled')).toHaveAttribute('disabled');
|
||||
|
||||
// Add Telemetry to ConditionSet
|
||||
const sineWaveGeneratorTreeItem = page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: exampleTelemetry.name
|
||||
});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
// Validate that the add criteria button is enabled and adds a new criterion
|
||||
await expect(page.getByLabel('Add Criteria - Enabled')).not.toHaveAttribute('disabled');
|
||||
await page.getByLabel('Add Criteria - Enabled').click();
|
||||
const numOfUnnamedCriteria = await page.getByLabel('Criterion Telemetry Selection').count();
|
||||
expect(numOfUnnamedCriteria).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Condition Set Composition', () => {
|
||||
|
@ -507,140 +507,8 @@ test.describe('Display Layout', () => {
|
||||
// In real time mode, we don't fetch annotations at all
|
||||
await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Same objects with different request options have unique subscriptions', async ({
|
||||
page
|
||||
}) => {
|
||||
// Expand My Items
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display'
|
||||
});
|
||||
|
||||
// Create a State Generator, set to higher frequency updates
|
||||
const stateGenerator = await createDomainObjectWithDefaults(page, {
|
||||
type: 'State Generator',
|
||||
name: 'State Generator'
|
||||
});
|
||||
const stateGeneratorTreeItem = page.getByRole('treeitem', {
|
||||
name: stateGenerator.name
|
||||
});
|
||||
await stateGeneratorTreeItem.click({ button: 'right' });
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Create a Table for filtering ON values
|
||||
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Table Filter On Value'
|
||||
});
|
||||
const tableFilterOnTreeItem = page.getByRole('treeitem', {
|
||||
name: tableFilterOnValue.name
|
||||
});
|
||||
|
||||
// Create a Table for filtering OFF values
|
||||
const tableFilterOffValue = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Table Filter Off Value'
|
||||
});
|
||||
const tableFilterOffTreeItem = page.getByRole('treeitem', {
|
||||
name: tableFilterOffValue.name
|
||||
});
|
||||
|
||||
// Navigate to ON filtering table and add state generator and setup filters
|
||||
await page.goto(tableFilterOnValue.url);
|
||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||
await selectFilterOption(page, '1');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Navigate to OFF filtering table and add state generator and setup filters
|
||||
await page.goto(tableFilterOffValue.url);
|
||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||
await selectFilterOption(page, '0');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Navigate to the display layout and edit it
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
// Add the tables to the display layout
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await tableFilterOffTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 10, y: 300 }
|
||||
});
|
||||
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 400, y: 500 },
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await tableFilterOnTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 10, y: 100 }
|
||||
});
|
||||
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 400, y: 300 },
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Get the tables so we can verify filtering is working as expected
|
||||
const tableFilterOn = page.getByLabel(`${tableFilterOnValue.name} Frame`, {
|
||||
exact: true
|
||||
});
|
||||
const tableFilterOff = page.getByLabel(`${tableFilterOffValue.name} Frame`, {
|
||||
exact: true
|
||||
});
|
||||
|
||||
// Verify filtering is working correctly
|
||||
|
||||
// Check that no filtered values appear for at least 2 seconds
|
||||
const VERIFICATION_TIME = 2000; // 2 seconds
|
||||
const CHECK_INTERVAL = 100; // Check every 100ms
|
||||
|
||||
// Create a promise that will check for filtered values periodically
|
||||
const checkForCorrectValues = new Promise((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
const offCount = await tableFilterOn.locator('td[title="OFF"]').count();
|
||||
const onCount = await tableFilterOff.locator('td[title="ON"]').count();
|
||||
if (offCount > 0 || onCount > 0) {
|
||||
clearInterval(interval);
|
||||
reject(
|
||||
new Error(
|
||||
`Found ${offCount} OFF and ${onCount} ON values when expecting 0 OFF and 0 ON`
|
||||
)
|
||||
);
|
||||
}
|
||||
}, CHECK_INTERVAL);
|
||||
|
||||
// After VERIFICATION_TIME, if no filtered values were found, resolve successfully
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}, VERIFICATION_TIME);
|
||||
});
|
||||
|
||||
await expect(checkForCorrectValues).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
async function selectFilterOption(page, filterOption) {
|
||||
await page.getByRole('tab', { name: 'Filters' }).click();
|
||||
await page
|
||||
.getByLabel('Inspector Views')
|
||||
.locator('li')
|
||||
.filter({ hasText: 'State Generator' })
|
||||
.locator('span')
|
||||
.click();
|
||||
await page.getByRole('switch').click();
|
||||
await page.selectOption('select[name="setSelectionThreshold"]', filterOption);
|
||||
}
|
||||
|
||||
async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
|
||||
await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);
|
||||
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);
|
||||
|
@ -28,9 +28,7 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
setRealTimeMode,
|
||||
setStartOffset
|
||||
createExampleTelemetryObject
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
@ -168,57 +166,6 @@ test.describe('Gauge', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Gauge does not break when an object is missing', async ({ page }) => {
|
||||
// Set up error listeners
|
||||
const pageErrors = [];
|
||||
|
||||
// Listen for uncaught exceptions
|
||||
page.on('pageerror', (err) => {
|
||||
pageErrors.push(err.message);
|
||||
});
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Create a Gauge
|
||||
const gauge = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gauge',
|
||||
name: 'Gauge with missing object'
|
||||
});
|
||||
|
||||
// Create a Sine Wave Generator in the Gauge with a loading delay
|
||||
const missingSWG = await createExampleTelemetryObject(page, gauge.uuid);
|
||||
|
||||
// Remove the object from local storage
|
||||
await page.evaluate(
|
||||
([missingObject]) => {
|
||||
const mct = localStorage.getItem('mct');
|
||||
const mctObjects = JSON.parse(mct);
|
||||
delete mctObjects[missingObject.uuid];
|
||||
localStorage.setItem('mct', JSON.stringify(mctObjects));
|
||||
},
|
||||
[missingSWG]
|
||||
);
|
||||
|
||||
// Verify start bounds
|
||||
await expect(page.getByLabel('Start offset: 00:30:00')).toBeVisible();
|
||||
|
||||
// Nav to the Gauge
|
||||
await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// adjust time bounds and ensure they are updated
|
||||
await setStartOffset(page, {
|
||||
startHours: '00',
|
||||
startMins: '45',
|
||||
startSecs: '00'
|
||||
});
|
||||
|
||||
// Verify start bounds changed
|
||||
await expect(page.getByLabel('Start offset: 00:45:00')).toBeVisible();
|
||||
|
||||
// // Verify no errors were thrown
|
||||
expect(pageErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Gauge enforces composition policy', async ({ page }) => {
|
||||
// Create a Gauge
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
|
@ -26,10 +26,7 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
renameCurrentObjectFromBrowseBar
|
||||
} from '../../../../appActions.js';
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
|
||||
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
||||
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
|
||||
@ -599,61 +596,4 @@ test.describe('Notebook entry tests', () => {
|
||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
|
||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('When changing the name of a notebook in the browse bar, new notebook changes are not lost', async ({
|
||||
page
|
||||
}) => {
|
||||
const TEST_TEXT = 'Do not lose me!';
|
||||
const FIRST_NEW_NAME = 'New Name';
|
||||
const SECOND_NEW_NAME = 'Second New Name';
|
||||
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
|
||||
await renameCurrentObjectFromBrowseBar(page, FIRST_NEW_NAME);
|
||||
|
||||
// verify the name change in tree and browse bar
|
||||
await verifyNameChange(page, FIRST_NEW_NAME);
|
||||
|
||||
// enter one entry
|
||||
await enterAndCommitTextEntry(page, TEST_TEXT);
|
||||
|
||||
// verify the entry is present
|
||||
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||
|
||||
// change the name
|
||||
await renameCurrentObjectFromBrowseBar(page, SECOND_NEW_NAME);
|
||||
|
||||
// verify the name change in tree and browse bar
|
||||
await verifyNameChange(page, SECOND_NEW_NAME);
|
||||
|
||||
// verify the entry is still present
|
||||
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Enter text into the last notebook entry and commit it.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} text
|
||||
*/
|
||||
async function enterAndCommitTextEntry(page, text) {
|
||||
await nbUtils.addNotebookEntry(page);
|
||||
await nbUtils.enterTextInLastEntry(page, text);
|
||||
await nbUtils.commitEntry(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the name change in the tree and browse bar.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function verifyNameChange(page, newName) {
|
||||
await expect(
|
||||
page.getByRole('treeitem').locator('.is-navigated-object .c-tree__item__name')
|
||||
).toHaveText(newName);
|
||||
await expect(page.getByLabel('Browse bar object name')).toHaveText(newName);
|
||||
}
|
||||
|
@ -108,42 +108,4 @@ test.describe('Plot Controls', () => {
|
||||
// Expect before and after plot points to match
|
||||
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
|
||||
});
|
||||
|
||||
/*
|
||||
Test to verify that switching a plot's time context from global to
|
||||
its own independent time context and then back to global context works correctly.
|
||||
|
||||
After switching from fixed time mode (ITC) to real time mode (global context),
|
||||
the pause control for the plot should be available, indicating that it is following the right context.
|
||||
*/
|
||||
test('Plots follow the right time context', async ({ page }) => {
|
||||
// Set global time conductor to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is visible since global time conductor is in Real time mode.
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||
|
||||
// Toggle independent time conductor ON
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Bring up the independent time conductor popup and switch to fixed time mode
|
||||
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||
await page.getByLabel('Independent Time Conductor Mode Menu').click();
|
||||
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is no longer visible since the plot is following the independent time context
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeHidden();
|
||||
|
||||
// Toggle independent time conductor OFF - Note that the global time conductor is still in Real time mode
|
||||
await page.getByLabel('Disable Independent Time Conductor').click();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is visible since the global time conductor is in real time mode
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -1,58 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2025, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
* This test suite is dedicated to testing the rendering and interaction of plots.
|
||||
*
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Plot Controls in compact mode', () => {
|
||||
let timeStrip;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timeStrip = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time Strip'
|
||||
});
|
||||
|
||||
// Create an overlay plot with a sine wave generator
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: timeStrip.uuid
|
||||
});
|
||||
await page.goto(`${timeStrip.url}`);
|
||||
});
|
||||
|
||||
test('Plots show cursor guides', async ({ page }) => {
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// click on cursor guides control
|
||||
await page.getByTitle('Toggle cursor guides').click();
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
await expect(page.getByLabel('Vertical cursor guide')).toBeVisible();
|
||||
await expect(page.getByLabel('Horizontal cursor guide')).toBeVisible();
|
||||
});
|
||||
});
|
@ -46,24 +46,6 @@ class EventMetadataProvider {
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const inPlaceUpdateMetadataValue = {
|
||||
key: 'messageId',
|
||||
name: 'row identifier',
|
||||
format: 'string',
|
||||
useToUpdateInPlace: true
|
||||
};
|
||||
const eventAcknowledgeMetadataValue = {
|
||||
key: 'acknowledge',
|
||||
name: 'Acknowledge',
|
||||
format: 'string'
|
||||
};
|
||||
|
||||
const eventGeneratorWithAcknowledge = structuredClone(this.METADATA_BY_TYPE.eventGenerator);
|
||||
eventGeneratorWithAcknowledge.values.push(inPlaceUpdateMetadataValue);
|
||||
eventGeneratorWithAcknowledge.values.push(eventAcknowledgeMetadataValue);
|
||||
|
||||
this.METADATA_BY_TYPE.eventGeneratorWithAcknowledge = eventGeneratorWithAcknowledge;
|
||||
}
|
||||
|
||||
supportsMetadata(domainObject) {
|
||||
|
@ -1,70 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
|
||||
*/
|
||||
|
||||
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
||||
|
||||
class EventWithAcknowledgeTelemetryProvider extends EventTelemetryProvider {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.unAcknowledgedData = undefined;
|
||||
}
|
||||
|
||||
generateData(firstObservedTime, count, startTime, duration, name) {
|
||||
if (this.unAcknowledgedData === undefined) {
|
||||
const unAcknowledgedData = super.generateData(
|
||||
firstObservedTime,
|
||||
count,
|
||||
startTime,
|
||||
duration,
|
||||
name
|
||||
);
|
||||
unAcknowledgedData.messageId = unAcknowledgedData.message;
|
||||
this.unAcknowledgedData = unAcknowledgedData;
|
||||
|
||||
return this.unAcknowledgedData;
|
||||
} else {
|
||||
const acknowledgedData = {
|
||||
...this.unAcknowledgedData,
|
||||
acknowledge: 'OK'
|
||||
};
|
||||
|
||||
this.unAcknowledgedData = undefined;
|
||||
|
||||
return acknowledgedData;
|
||||
}
|
||||
}
|
||||
|
||||
supportsRequest(domainObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
supportsSubscribe(domainObject) {
|
||||
return domainObject.type === 'eventGeneratorWithAcknowledge';
|
||||
}
|
||||
}
|
||||
|
||||
export default EventWithAcknowledgeTelemetryProvider;
|
@ -21,7 +21,6 @@
|
||||
*****************************************************************************/
|
||||
import EventMetadataProvider from './EventMetadataProvider.js';
|
||||
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
||||
import EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js';
|
||||
|
||||
export default function EventGeneratorPlugin(options) {
|
||||
return function install(openmct) {
|
||||
@ -39,20 +38,5 @@ export default function EventGeneratorPlugin(options) {
|
||||
});
|
||||
openmct.telemetry.addProvider(new EventTelemetryProvider());
|
||||
openmct.telemetry.addProvider(new EventMetadataProvider());
|
||||
|
||||
openmct.types.addType('eventGeneratorWithAcknowledge', {
|
||||
name: 'Event Message Generator with Acknowledge',
|
||||
description:
|
||||
'For development use. Creates sample event message data stream and updates the event row with an acknowledgement.',
|
||||
cssClass: 'icon-generator-events',
|
||||
creatable: true,
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
duration: 2.5
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider());
|
||||
};
|
||||
}
|
||||
|
@ -108,16 +108,6 @@ const METADATA_BY_TYPE = {
|
||||
string: 'ON'
|
||||
}
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
singleSelectionThreshold: true,
|
||||
comparator: 'equals',
|
||||
possibleValues: [
|
||||
{ label: 'OFF', value: 0 },
|
||||
{ label: 'ON', value: 1 }
|
||||
]
|
||||
}
|
||||
],
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
|
@ -34,16 +34,14 @@ StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
|
||||
return domainObject.type === 'example.state-generator';
|
||||
};
|
||||
|
||||
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback, options) {
|
||||
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
|
||||
var duration = domainObject.telemetry.duration * 1000;
|
||||
|
||||
var interval = setInterval(() => {
|
||||
var interval = setInterval(function () {
|
||||
var now = Date.now();
|
||||
var datum = pointForTimestamp(now, duration, domainObject.name);
|
||||
if (!this.shouldBeFiltered(datum, options)) {
|
||||
datum.value = String(datum.value);
|
||||
callback(datum);
|
||||
}
|
||||
datum.value = String(datum.value);
|
||||
callback(datum);
|
||||
}, duration);
|
||||
|
||||
return function () {
|
||||
@ -65,25 +63,9 @@ StateGeneratorProvider.prototype.request = function (domainObject, options) {
|
||||
|
||||
var data = [];
|
||||
while (start <= end && data.length < 5000) {
|
||||
const point = pointForTimestamp(start, duration, domainObject.name);
|
||||
|
||||
if (!this.shouldBeFiltered(point, options)) {
|
||||
data.push(point);
|
||||
}
|
||||
data.push(pointForTimestamp(start, duration, domainObject.name));
|
||||
start += duration;
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
};
|
||||
|
||||
StateGeneratorProvider.prototype.shouldBeFiltered = function (point, options) {
|
||||
const valueToFilter = options?.filters?.state?.equals?.[0];
|
||||
|
||||
if (!valueToFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { value } = point;
|
||||
|
||||
return value !== Number(valueToFilter);
|
||||
};
|
||||
|
101
package-lock.json
generated
101
package-lock.json
generated
@ -62,7 +62,6 @@
|
||||
"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",
|
||||
@ -644,18 +643,6 @@
|
||||
"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",
|
||||
@ -3101,19 +3088,6 @@
|
||||
"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",
|
||||
@ -4059,12 +4033,6 @@
|
||||
"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",
|
||||
@ -4515,12 +4483,6 @@
|
||||
"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",
|
||||
@ -5855,19 +5817,6 @@
|
||||
"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",
|
||||
@ -7114,12 +7063,6 @@
|
||||
"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",
|
||||
@ -7765,29 +7708,6 @@
|
||||
"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",
|
||||
@ -9571,12 +9491,6 @@
|
||||
"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",
|
||||
@ -9933,12 +9847,6 @@
|
||||
"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",
|
||||
@ -10925,15 +10833,6 @@
|
||||
"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,7 +65,6 @@
|
||||
"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,7 +306,6 @@ 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.
|
||||
|
@ -250,90 +250,6 @@ export default class TelemetryAPI {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes objects for consistent serialization by:
|
||||
* 1. Removing non-plain objects (class instances) and functions
|
||||
* 2. Sorting object keys alphabetically to ensure consistent ordering
|
||||
*/
|
||||
sanitizeForSerialization(key, value) {
|
||||
// Handle null and primitives directly
|
||||
if (value === null || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Remove functions and non-plain objects (except arrays)
|
||||
if (
|
||||
typeof value === 'function' ||
|
||||
(Object.getPrototypeOf(value) !== Object.prototype && !Array.isArray(value))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// For plain objects, just sort the keys
|
||||
if (!Array.isArray(value)) {
|
||||
const sortedObject = {};
|
||||
const sortedKeys = Object.keys(value).sort();
|
||||
|
||||
sortedKeys.forEach((objectKey) => {
|
||||
sortedObject[objectKey] = value[objectKey];
|
||||
});
|
||||
|
||||
return sortedObject;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a numeric hash value for an options object. The hash is consistent
|
||||
* for equivalent option objects regardless of property order.
|
||||
*
|
||||
* This is used to create compact, unique cache keys for telemetry subscriptions with
|
||||
* different options configurations. The hash function ensures that identical options
|
||||
* objects will always generate the same hash value, while different options objects
|
||||
* (even with small differences) will generate different hash values.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} options The options object to hash
|
||||
* @returns {number} A positive integer hash of the options object
|
||||
*/
|
||||
#hashOptions(options) {
|
||||
const sanitizedOptionsString = JSON.stringify(
|
||||
options,
|
||||
this.sanitizeForSerialization.bind(this)
|
||||
);
|
||||
|
||||
let hash = 0;
|
||||
const prime = 31;
|
||||
const modulus = 1e9 + 9; // Large prime number
|
||||
|
||||
for (let i = 0; i < sanitizedOptionsString.length; i++) {
|
||||
const char = sanitizedOptionsString.charCodeAt(i);
|
||||
// Calculate new hash value while keeping numbers manageable
|
||||
hash = Math.floor((hash * prime + char) % modulus);
|
||||
}
|
||||
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique cache key for a telemetry subscription based on the
|
||||
* domain object identifier and options (which includes strategy).
|
||||
*
|
||||
* Uses a hash of the options object to create compact cache keys while still
|
||||
* ensuring unique keys for different subscription configurations.
|
||||
*
|
||||
* @private
|
||||
* @param {import('openmct').DomainObject} domainObject The domain object being subscribed to
|
||||
* @param {Object} options The subscription options object (including strategy)
|
||||
* @returns {string} A unique key string for caching the subscription
|
||||
*/
|
||||
#getSubscriptionCacheKey(domainObject, options) {
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
|
||||
return `${keyString}:${this.#hashOptions(options)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
|
||||
* The request will be modified when it is received and will be returned in it's modified state
|
||||
@ -502,14 +418,16 @@ export default class TelemetryAPI {
|
||||
this.#subscribeCache = {};
|
||||
}
|
||||
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
|
||||
// Override the requested strategy with the strategy supported by the provider
|
||||
const optionsWithSupportedStrategy = {
|
||||
...options,
|
||||
strategy: supportedStrategy
|
||||
};
|
||||
|
||||
const cacheKey = this.#getSubscriptionCacheKey(domainObject, optionsWithSupportedStrategy);
|
||||
// If batching is supported, we need to cache a subscription for each strategy -
|
||||
// latest and batched.
|
||||
const cacheKey = `${keyString}:${supportedStrategy}`;
|
||||
let subscriber = this.#subscribeCache[cacheKey];
|
||||
|
||||
if (!subscriber) {
|
||||
@ -760,15 +678,6 @@ 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,23 +86,14 @@ 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();
|
||||
|
||||
const historicalTelemetryLoadedPromise = this._requestHistoricalTelemetry();
|
||||
this._requestHistoricalTelemetry();
|
||||
this._initiateSubscriptionTelemetry();
|
||||
|
||||
this.loaded = true;
|
||||
|
||||
return historicalTelemetryLoadedPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,7 +113,6 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
}
|
||||
|
||||
this.removeAllListeners();
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -178,7 +168,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this._processNewTelemetry(historicalData, false);
|
||||
this._processNewTelemetry(historicalData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -192,9 +182,10 @@ 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, true),
|
||||
(datum) => this._processNewTelemetry(datum),
|
||||
options
|
||||
);
|
||||
}
|
||||
@ -205,10 +196,9 @@ 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, isSubscriptionData = false) {
|
||||
_processNewTelemetry(telemetryData) {
|
||||
if (telemetryData === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -223,19 +213,12 @@ 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 < boundsToUse.start;
|
||||
afterEndOfBounds = parsedValue > boundsToUse.end;
|
||||
beforeStartOfBounds = parsedValue < this.lastBounds.start;
|
||||
afterEndOfBounds = parsedValue > this.lastBounds.end;
|
||||
|
||||
if (
|
||||
!afterEndOfBounds &&
|
||||
@ -414,10 +397,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this.emit('add', added, [this.boundedTelemetry.length]);
|
||||
}
|
||||
} else {
|
||||
// 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;
|
||||
// user bounds change, reset
|
||||
this._reset();
|
||||
}
|
||||
}
|
||||
@ -497,9 +477,9 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this.boundedTelemetry = [];
|
||||
this.futureBuffer = [];
|
||||
|
||||
const telemetryLoadPromise = this._requestHistoricalTelemetry();
|
||||
this.emit('clear');
|
||||
|
||||
this.emit('clear', telemetryLoadPromise);
|
||||
this._requestHistoricalTelemetry();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -359,18 +359,6 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
* @override
|
||||
*/
|
||||
isFixed() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.isFixed(...arguments);
|
||||
} else {
|
||||
return super.isFixed(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
* @override
|
||||
@ -412,7 +400,7 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the time context from the global time context
|
||||
* Reset the time context to the global time context
|
||||
*/
|
||||
resetContext() {
|
||||
if (this.upstreamTimeContext) {
|
||||
@ -440,10 +428,6 @@ class IndependentTimeContext extends TimeContext {
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.getBounds());
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||
// Also emit the mode in case it's different from previous time context
|
||||
if (this.getMode()) {
|
||||
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -518,10 +502,6 @@ class IndependentTimeContext extends TimeContext {
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.getBounds());
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||
// Also emit the mode in case it's different from the global time context
|
||||
if (this.getMode()) {
|
||||
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
|
||||
}
|
||||
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
|
||||
this.globalTimeContext.emit('refreshContext', viewKey);
|
||||
}
|
||||
|
@ -23,7 +23,6 @@
|
||||
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
|
||||
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
|
||||
|
||||
import { TIME_CONTEXT_EVENTS } from './constants';
|
||||
import GlobalTimeContext from './GlobalTimeContext.js';
|
||||
|
||||
/**
|
||||
@ -143,7 +142,7 @@ class TimeAPI extends GlobalTimeContext {
|
||||
addIndependentContext(key, value, clockKey) {
|
||||
let timeContext = this.getIndependentContext(key);
|
||||
|
||||
//stop following upstream time context since the view has its own
|
||||
//stop following upstream time context since the view has it's own
|
||||
timeContext.resetContext();
|
||||
|
||||
if (clockKey) {
|
||||
@ -153,9 +152,6 @@ class TimeAPI extends GlobalTimeContext {
|
||||
timeContext.setMode(FIXED_MODE_KEY, value);
|
||||
}
|
||||
|
||||
// Also emit the mode in case it's different from the previous time context
|
||||
timeContext.emit(TIME_CONTEXT_EVENTS.modeChanged, structuredClone(timeContext.getMode()));
|
||||
|
||||
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
|
||||
this.emit('refreshContext', key);
|
||||
|
||||
|
@ -1,85 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,379 +0,0 @@
|
||||
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];
|
||||
}
|
||||
}
|
@ -1,139 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
<!--
|
||||
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>
|
@ -1,399 +0,0 @@
|
||||
<!--
|
||||
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>
|
@ -1,153 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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));
|
||||
};
|
||||
}
|
@ -160,10 +160,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="c-cdef__separator c-row-separator"></div>
|
||||
<div class="c-cdef__controls">
|
||||
<div class="c-cdef__controls" :disabled="!telemetry.length">
|
||||
<button
|
||||
:disabled="!telemetry.length"
|
||||
:aria-label="`Add Criteria - ${!telemetry.length ? 'Disabled' : 'Enabled'}`"
|
||||
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
|
||||
@click="addCriteria"
|
||||
>
|
||||
|
@ -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-output-featured">
|
||||
<span class="c-output-featured__label">Current Output</span>
|
||||
<span class="c-output-featured__value" aria-label="Current Output Value">
|
||||
<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">
|
||||
<template v-if="currentConditionOutput">
|
||||
{{ currentConditionOutput }}
|
||||
</template>
|
||||
|
@ -51,7 +51,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
gap: $interiorMargin;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@ -90,24 +89,23 @@
|
||||
&__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 {
|
||||
@ -123,7 +121,6 @@
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $interiorMargin;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -27,9 +27,9 @@ To define a filter, you'll need to add a new `filter` property to the domain obj
|
||||
singleSelectionThreshold: true,
|
||||
comparator: 'equals',
|
||||
possibleValues: [
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
{ label: 'Banana', value: 'banana' },
|
||||
{ label: 'Orange', value: 'orange' }
|
||||
{ name: 'Apple', value: 'apple' },
|
||||
{ name: 'Banana', value: 'banana' },
|
||||
{ name: 'Orange', value: 'orange' }
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
@ -649,11 +649,6 @@ export default {
|
||||
},
|
||||
request(domainObject = this.telemetryObject) {
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
|
||||
if (!this.metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject);
|
||||
LimitEvaluator.limits().then(this.updateLimits);
|
||||
|
@ -164,13 +164,11 @@
|
||||
<div
|
||||
v-show="cursorGuide"
|
||||
ref="cursorGuideVertical"
|
||||
aria-label="Vertical cursor guide"
|
||||
class="c-cursor-guide--v js-cursor-guide--v"
|
||||
></div>
|
||||
<div
|
||||
v-show="cursorGuide"
|
||||
ref="cursorGuideHorizontal"
|
||||
aria-label="Horizontal cursor guide"
|
||||
class="c-cursor-guide--h js-cursor-guide--h"
|
||||
></div>
|
||||
</div>
|
||||
@ -539,7 +537,6 @@ export default {
|
||||
this.followTimeContext();
|
||||
},
|
||||
followTimeContext() {
|
||||
this.updateMode();
|
||||
this.updateDisplayBounds(this.timeContext.getBounds());
|
||||
this.timeContext.on('modeChanged', this.updateMode);
|
||||
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
|
||||
@ -857,11 +854,13 @@ export default {
|
||||
|
||||
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
|
||||
|
||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
|
||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||
if (!this.options.compact) {
|
||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
|
||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||
}
|
||||
},
|
||||
|
||||
marqueeAnnotations(annotationsToSelect) {
|
||||
@ -1116,21 +1115,19 @@ export default {
|
||||
this.listenTo(window, 'mouseup', this.onMouseUp, this);
|
||||
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
|
||||
|
||||
if (!this.options.compact) {
|
||||
// track frozen state on mouseDown to be read on mouseUp
|
||||
const isFrozen =
|
||||
this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
this.isFrozenOnMouseDown = isFrozen;
|
||||
// track frozen state on mouseDown to be read on mouseUp
|
||||
const isFrozen =
|
||||
this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
this.isFrozenOnMouseDown = isFrozen;
|
||||
|
||||
if (event.altKey && !event.shiftKey) {
|
||||
return this.startPan(event);
|
||||
} else if (event.altKey && event.shiftKey) {
|
||||
this.freeze();
|
||||
if (event.altKey && !event.shiftKey) {
|
||||
return this.startPan(event);
|
||||
} else if (event.altKey && event.shiftKey) {
|
||||
this.freeze();
|
||||
|
||||
return this.startMarquee(event, true);
|
||||
} else {
|
||||
return this.startMarquee(event, false);
|
||||
}
|
||||
return this.startMarquee(event, true);
|
||||
} else {
|
||||
return this.startMarquee(event, false);
|
||||
}
|
||||
},
|
||||
|
||||
@ -1161,15 +1158,11 @@ export default {
|
||||
},
|
||||
|
||||
isMouseClick() {
|
||||
// We may not have a marquee if we've disabled pan/zoom, but we still need to know if it's a mouse click for highlights and lock points.
|
||||
if (!this.marquee && !this.positionOverPlot) {
|
||||
if (!this.marquee) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { start, end } = this.marquee ?? {
|
||||
start: this.positionOverPlot,
|
||||
end: this.positionOverPlot
|
||||
};
|
||||
const { start, end } = this.marquee;
|
||||
const someYPositionOverPlot = start.y.some((y) => y);
|
||||
|
||||
return start.x === end.x && someYPositionOverPlot;
|
||||
|
@ -162,6 +162,14 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
gridLines(newGridLines) {
|
||||
this.gridLines = newGridLines;
|
||||
},
|
||||
cursorGuide(newCursorGuide) {
|
||||
this.cursorGuide = newCursorGuide;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
eventHelpers.extend(this);
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
|
@ -225,13 +225,7 @@ export default class PlotSeries extends Model {
|
||||
|
||||
try {
|
||||
const points = await this.openmct.telemetry.request(this.domainObject, options);
|
||||
// if derived, we can't use the old data
|
||||
let data = this.getSeriesData();
|
||||
|
||||
if (this.metadata.value(this.get('yKey')).derived) {
|
||||
data = [];
|
||||
}
|
||||
|
||||
const data = this.getSeriesData();
|
||||
// eslint-disable-next-line you-dont-need-lodash-underscore/concat
|
||||
const newPoints = _(data)
|
||||
.concat(points)
|
||||
|
@ -249,8 +249,7 @@ export default {
|
||||
...persistedSeriesConfig.series
|
||||
}
|
||||
],
|
||||
yAxis: persistedSeriesConfig.yAxis,
|
||||
...this.childObject.configuration
|
||||
yAxis: persistedSeriesConfig.yAxis
|
||||
}
|
||||
},
|
||||
openmct: this.openmct,
|
||||
|
@ -32,7 +32,6 @@ 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';
|
||||
@ -177,6 +176,5 @@ plugins.Gauge = GaugePlugin;
|
||||
plugins.Timelist = TimeList;
|
||||
plugins.InspectorViews = InspectorViews;
|
||||
plugins.InspectorDataVisualization = InspectorDataVisualization;
|
||||
plugins.Comps = CompsPlugin;
|
||||
|
||||
export default plugins;
|
||||
|
@ -91,19 +91,15 @@ export default class TelemetryTableRow {
|
||||
return [VIEW_DATUM_ACTION_KEY, VIEW_HISTORICAL_DATA_ACTION_KEY];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the row parameter's datum with the current row datum
|
||||
* @param {TelemetryTableRow} row
|
||||
*/
|
||||
updateWithDatum(row) {
|
||||
updateWithDatum(updatesToDatum) {
|
||||
const normalizedUpdatesToDatum = createNormalizedDatum(updatesToDatum, this.columns);
|
||||
this.datum = {
|
||||
...this.datum,
|
||||
...row.datum
|
||||
...normalizedUpdatesToDatum
|
||||
};
|
||||
|
||||
this.fullDatum = {
|
||||
...this.fullDatum,
|
||||
...row.fullDatum
|
||||
...updatesToDatum
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -23,11 +23,6 @@ import { EventEmitter } from 'eventemitter3';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { ORDER } from '../constants.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('.TelemetryTableRow.js').default} TelemetryTableRow
|
||||
*/
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
@ -129,22 +124,10 @@ export default class TableRowCollection extends EventEmitter {
|
||||
return foundIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* `incomingRow` exists in the collection,
|
||||
* so merge existing and incoming row properties
|
||||
*
|
||||
* Do to reactivity of Vue, we want to replace the existing row with the updated row
|
||||
* @param {TelemetryTableRow} incomingRow to update
|
||||
* @param {number} index of the existing row in the collection to update
|
||||
*/
|
||||
updateRowInPlace(incomingRow, index) {
|
||||
// Update the incoming row, not the existing row
|
||||
const existingRow = this.rows[index];
|
||||
incomingRow.updateWithDatum(existingRow);
|
||||
|
||||
// Replacing the existing row with the updated, incoming row will trigger Vue reactivity
|
||||
// because the reference to the row has changed
|
||||
this.rows.splice(index, 1, incomingRow);
|
||||
updateRowInPlace(row, index) {
|
||||
const foundRow = this.rows[index];
|
||||
foundRow.updateWithDatum(row.datum);
|
||||
this.rows[index] = foundRow;
|
||||
}
|
||||
|
||||
setLimit(rowLimit) {
|
||||
|
@ -373,6 +373,7 @@ export default {
|
||||
configuredColumnWidths: configuration.columnWidths,
|
||||
sizingRows: {},
|
||||
rowHeight: ROW_HEIGHT,
|
||||
scrollOffset: 0,
|
||||
totalHeight: 0,
|
||||
totalWidth: 0,
|
||||
rowOffset: 0,
|
||||
@ -551,7 +552,6 @@ export default {
|
||||
//Default sort
|
||||
this.sortOptions = this.table.tableRows.sortBy();
|
||||
this.scrollable = this.$refs.scrollable;
|
||||
this.lastScrollLeft = this.scrollable.scrollLeft;
|
||||
this.contentTable = this.$refs.contentTable;
|
||||
this.sizingTable = this.$refs.sizingTable;
|
||||
this.headersHolderEl = this.$refs.headersHolderEl;
|
||||
@ -740,9 +740,7 @@ export default {
|
||||
this.table.sortBy(this.sortOptions);
|
||||
},
|
||||
scroll() {
|
||||
if (this.lastScrollLeft === this.scrollable.scrollLeft) {
|
||||
this.throttledUpdateVisibleRows();
|
||||
}
|
||||
this.throttledUpdateVisibleRows();
|
||||
this.synchronizeScrollX();
|
||||
|
||||
if (this.shouldAutoScroll()) {
|
||||
@ -767,8 +765,6 @@ export default {
|
||||
this.scrollable.scrollTop = Number.MAX_SAFE_INTEGER;
|
||||
},
|
||||
synchronizeScrollX() {
|
||||
this.lastScrollLeft = this.scrollable.scrollLeft;
|
||||
|
||||
if (this.$refs.headersHolderEl && this.scrollable) {
|
||||
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
|
||||
}
|
||||
|
@ -243,20 +243,12 @@ export default {
|
||||
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
|
||||
},
|
||||
setTimeOptionsClock(clock) {
|
||||
// If the user has persisted any time options, then don't override them with global settings.
|
||||
if (this.independentTCEnabled) {
|
||||
return;
|
||||
}
|
||||
this.setTimeOptionsOffsets();
|
||||
this.timeOptions.clock = clock.key;
|
||||
},
|
||||
setTimeOptionsMode(mode) {
|
||||
// If the user has persisted any time options, then don't override them with global settings.
|
||||
if (this.independentTCEnabled) {
|
||||
this.setTimeOptionsOffsets();
|
||||
this.timeOptions.mode = mode;
|
||||
this.isFixed = this.timeOptions.mode === FIXED_MODE_KEY;
|
||||
}
|
||||
this.setTimeOptionsOffsets();
|
||||
this.timeOptions.mode = mode;
|
||||
},
|
||||
setTimeOptionsOffsets() {
|
||||
this.timeOptions.clockOffsets =
|
||||
|
@ -436,9 +436,6 @@ export default {
|
||||
|
||||
return startInBounds || endInBounds || middleInBounds;
|
||||
},
|
||||
isActivityInProgress(activity) {
|
||||
return this.persistedActivityStates[activity.id] === 'in-progress';
|
||||
},
|
||||
filterActivities(activity) {
|
||||
if (this.isEditing) {
|
||||
return true;
|
||||
@ -463,8 +460,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
// An activity may be out of bounds, but if it is in-progress, we show it.
|
||||
if (!this.isActivityInBounds(activity) && !this.isActivityInProgress(activity)) {
|
||||
if (!this.isActivityInBounds(activity)) {
|
||||
return false;
|
||||
}
|
||||
//current event or future start event or past end event
|
||||
|
@ -356,7 +356,7 @@ $colorInspectorBg: $colorBodyBg;
|
||||
$colorInspectorFg: $colorBodyFg;
|
||||
$colorInspectorPropName: $colorBodyFgSubtle;
|
||||
$colorInspectorPropVal: $colorBodyFgEm;
|
||||
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 10%);
|
||||
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%);
|
||||
$colorInspectorSectionHeaderFg: #bfbfbf;
|
||||
|
||||
// Tabs
|
||||
|
@ -277,9 +277,8 @@ $glyph-icon-bar-chart: '\eb2c';
|
||||
$glyph-icon-map: '\eb2d';
|
||||
$glyph-icon-plan: '\eb2e';
|
||||
$glyph-icon-timelist: '\eb2f';
|
||||
$glyph-icon-plot-scatter: '\eb30';
|
||||
$glyph-icon-notebook-shift-log: '\eb31';
|
||||
$glyph-icon-derived-telemetry: '\eb32';
|
||||
$glyph-icon-plot-scatter: '\eb30';
|
||||
|
||||
/************************** GLYPHS AS DATA URI */
|
||||
// Only objects have been converted, for use in Create menu and folder views
|
||||
@ -336,4 +335,3 @@ $bg-icon-telemetry-aggregate: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns
|
||||
$bg-icon-trash: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='512px' height='512px' viewBox='0 0 512 512' enable-background='new 0 0 512 512' xml:space='preserve'%3e%3cpath d='M416,64h-96.18V32c0-17.6-14.4-32-32-32h-64c-17.6,0-32,14.4-32,32v32H96c-52.8,0-96,36-96,80s0,80,0,80h32v192 c0,52.8,43.2,96,96,96h256c52.8,0,96-43.2,96-96V224h32c0,0,0-36,0-80S468.8,64,416,64z M160,416H96V224h64V416z M288,416h-64V224 h64V416z M416,416h-64V224h64V416z'/%3e%3c/svg%3e");
|
||||
$bg-icon-eye-open: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 512 512' style='enable-background:new 0 0 512 512;' xml:space='preserve'%3e%3cstyle type='text/css'%3e .st0%7bfill:%2300A14B;%7d %3c/style%3e%3ctitle%3eicon-eye-open-v2%3c/title%3e%3cg%3e%3cpath class='st0' d='M256,58.2c-122.9,0-226.1,84-255.4,197.8C29.9,369.7,133.1,453.8,256,453.8s226.1-84,255.4-197.8 C482.1,142.3,378.9,58.2,256,58.2z M414.6,294.2c-11.3,17.2-25.3,32.4-41.5,45.2c-16.4,12.9-34.5,22.8-54,29.7 c-20.2,7.1-41.4,10.7-63,10.7s-42.9-3.6-63-10.7c-19.5-6.9-37.7-16.9-54-29.7c-16.2-12.8-30.2-27.9-41.5-45.2 c-7.9-12-14.4-24.8-19.3-38.2c5-13.4,11.5-26.2,19.3-38.2c11.3-17.2,25.3-32.4,41.5-45.2c16.4-12.9,34.5-22.8,54-29.7 c20.2-7.1,41.4-10.7,63-10.7s42.9,3.6,63,10.7c19.5,6.9,37.7,16.9,54,29.7c16.2,12.8,30.2,27.9,41.5,45.2 c7.9,12,14.4,24.8,19.3,38.2C429,269.4,422.5,282.2,414.6,294.2z'/%3e%3ccircle class='st0' cx='256' cy='256' r='96'/%3e%3c/g%3e%3c/svg%3e");
|
||||
$bg-icon-camera: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3ctitle%3eicon-camera-v2%3c/title%3e%3cpath d='M448,128H384L320,0H192L128,128H64A64.2,64.2,0,0,0,0,192V448a64.2,64.2,0,0,0,64,64H448a64.2,64.2,0,0,0,64-64V192A64.2,64.2,0,0,0,448,128ZM256,432A128,128,0,1,1,384,304,128,128,0,0,1,256,432Z'/%3e%3c/svg%3e");
|
||||
$bg-icon-derived-telemetry: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M66.1 166c20.2 24.3 35.1 54.6 44 75.7 10 23.6 21.7 44 33.1 57.7 11.1 13.3 18.5 16.3 20.2 16.3s9.1-3 20.2-16.3c11.4-13.7 23.1-34.2 33.1-57.7 8.9-21.1 23.8-51.4 44-75.7 23.3-28.1 48.7-42.3 75.6-42.3s52.2 14.2 75.6 42.3c20.2 24.3 35.1 54.6 44 75.7 10 23.6 21.7 44 33.1 57.7 11.1 13.3 18.5 16.3 20.2 16.3s1.6-.3 3.2-1.1v-58.9c-.2-141.3-114.9-256-256.2-256H.2v124.6c23.3 3 45.4 17 66 41.7Z'/%3e%3cpath d='M509 387.7c-26.8 0-52.2-14.2-75.6-42.3-20.2-24.3-35.1-54.6-44-75.7-10-23.6-21.7-44-33.1-57.7-11.1-13.3-18.5-16.3-20.2-16.3s-9.1 3-20.2 16.3c-11.4 13.7-23.1 34.2-33.1 57.7-8.9 21.1-23.8 51.4-44 75.7-23.3 28.1-48.7 42.3-75.6 42.3s-52.2-14.2-75.6-42.3c-20.2-24.3-35.1-54.6-44-75.7-10-23.6-21.7-44-33.1-57.7-4.1-4.9-7.6-8.4-10.6-10.8v54.5c.3 141.4 114.9 256 256.3 256h256V387.6H509Z'/%3e%3c/svg%3e");
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@ -2,7 +2,6 @@
|
||||
@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';
|
||||
|
@ -1,168 +0,0 @@
|
||||
<!--
|
||||
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>
|
@ -21,9 +21,7 @@
|
||||
-->
|
||||
<template>
|
||||
<div ref="axisHolder" class="c-timesystem-axis">
|
||||
<div class="nowMarker" :style="nowMarkerStyle" aria-label="Now Marker">
|
||||
<span class="icon-arrow-down"></span>
|
||||
</div>
|
||||
<div class="nowMarker" :style="nowMarkerStyle"><span class="icon-arrow-down"></span></div>
|
||||
<svg :width="svgWidth" :height="svgHeight">
|
||||
<g class="axis" font-size="1.3em" :transform="axisTransform"></g>
|
||||
</svg>
|
||||
@ -118,10 +116,8 @@ export default {
|
||||
this.axisTransform = `translate(${this.alignmentData.leftWidth + leftOffset}, 20)`;
|
||||
|
||||
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
||||
|
||||
this.leftAlignmentOffset = this.alignmentData.leftWidth + leftOffset;
|
||||
this.alignmentOffset =
|
||||
this.leftAlignmentOffset + this.alignmentData.rightWidth + rightOffset;
|
||||
this.alignmentData.leftWidth + leftOffset + this.alignmentData.rightWidth + rightOffset;
|
||||
this.refresh();
|
||||
},
|
||||
deep: true
|
||||
@ -179,8 +175,8 @@ export default {
|
||||
this.nowMarkerStyle.height = this.contentHeight + 'px';
|
||||
const nowTimeStamp = this.openmct.time.now();
|
||||
const now = this.xScale(nowTimeStamp);
|
||||
this.nowMarkerStyle.left = `${now + this.leftAlignmentOffset}px`;
|
||||
if (now < 0 || now > this.width) {
|
||||
this.nowMarkerStyle.left = `${now + this.alignmentOffset}px`;
|
||||
if (now > this.width) {
|
||||
nowMarker.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,6 @@
|
||||
.c-toggle-switch {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
gap: $interiorMarginSm;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
|
||||
@ -56,6 +55,7 @@
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-left: $interiorMarginSm;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div class="l-browse-bar" aria-label="Browse bar">
|
||||
<div class="l-browse-bar">
|
||||
<div class="l-browse-bar__start">
|
||||
<button
|
||||
v-if="hasParent"
|
||||
@ -35,7 +35,6 @@
|
||||
</div>
|
||||
<span
|
||||
ref="objectName"
|
||||
aria-label="Browse bar object name"
|
||||
class="l-browse-bar__object-name c-object-label__name"
|
||||
:class="{ 'c-input-inline': isPersistable }"
|
||||
:contenteditable="isNameEditable"
|
||||
|
@ -80,11 +80,13 @@ class Browse {
|
||||
this.#openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
|
||||
}
|
||||
|
||||
#handleBrowseObjectUpdate(newObject) {
|
||||
this.#openmct.layout.$refs.browseBar.domainObject = newObject;
|
||||
|
||||
if (typeof newObject.name === 'string' && newObject.name !== document.title) {
|
||||
document.title = newObject.name;
|
||||
#updateDocumentTitleOnNameMutation(newName) {
|
||||
if (typeof newName === 'string' && newName !== document.title) {
|
||||
document.title = newName;
|
||||
this.#openmct.layout.$refs.browseBar.domainObject = {
|
||||
...this.#openmct.layout.$refs.browseBar.domainObject,
|
||||
name: newName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,8 +120,8 @@ class Browse {
|
||||
document.title = this.#browseObject.name; //change document title to current object in main view
|
||||
this.#unobserve = this.#openmct.objects.observe(
|
||||
this.#browseObject,
|
||||
'*',
|
||||
this.#handleBrowseObjectUpdate.bind(this)
|
||||
'name',
|
||||
this.#updateDocumentTitleOnNameMutation.bind(this)
|
||||
);
|
||||
const currentProvider = this.#openmct.objectViews.getByProviderKey(currentViewKey);
|
||||
if (currentProvider && currentProvider.canView(this.#browseObject, this.#openmct.router.path)) {
|
||||
|
Reference in New Issue
Block a user