resolve conflicts

This commit is contained in:
Scott Bell 2024-10-09 17:11:35 +02:00
commit ae03fa4121
32 changed files with 554 additions and 149 deletions

View File

@ -8,8 +8,8 @@ executors:
- image: mcr.microsoft.com/playwright:v1.47.2-focal - image: mcr.microsoft.com/playwright:v1.47.2-focal
environment: environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: "true" # Needed to store the percy browser in cache deps PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742) PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_PARALLEL_TOTAL: 2 PERCY_PARALLEL_TOTAL: 2
ubuntu: ubuntu:
machine: machine:
@ -17,7 +17,7 @@ executors:
docker_layer_caching: true docker_layer_caching: true
commands: commands:
build_and_install: build_and_install:
description: "All steps used to build and install." description: 'All steps used to build and install.'
parameters: parameters:
node-version: node-version:
type: string type: string
@ -27,7 +27,7 @@ commands:
node-version: << parameters.node-version >> node-version: << parameters.node-version >>
- node/install-packages - node/install-packages
generate_and_store_version_and_filesystem_artifacts: generate_and_store_version_and_filesystem_artifacts:
description: "Track important packages and files" description: 'Track important packages and files'
steps: steps:
- run: | - run: |
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts) [[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
@ -61,7 +61,7 @@ commands:
[[ $EUID -ne 0 ]] && sudo chmod +x codecov || chmod +x codecov [[ $EUID -ne 0 ]] && sudo chmod +x codecov || chmod +x codecov
./codecov --help ./codecov --help
generate_e2e_code_cov_report: generate_e2e_code_cov_report:
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test" description: 'Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test'
parameters: parameters:
suite: suite:
type: string type: string
@ -135,13 +135,13 @@ jobs:
suite: #ci or full suite: #ci or full
type: string type: string
executor: pw-focal-development executor: pw-focal-development
parallelism: 7 parallelism: 8
steps: steps:
- build_and_install: - build_and_install:
node-version: lts/hydrogen node-version: lts/hydrogen
- when: #Only install chrome-beta when running the 'full' suite to save $$$ - when: #Only install chrome-beta when running the 'full' suite to save $$$
condition: condition:
equal: ["full", <<parameters.suite>>] equal: ['full', <<parameters.suite>>]
steps: steps:
- run: npx playwright install chrome-beta - run: npx playwright install chrome-beta
- run: - run:
@ -323,7 +323,7 @@ workflows:
- e2e-couchdb - e2e-couchdb
triggers: triggers:
- schedule: - schedule:
cron: "0 0 * * *" cron: '0 0 * * *'
filters: filters:
branches: branches:
only: only:

View File

@ -243,6 +243,37 @@ async function createExampleTelemetryObject(page, parent = 'mine') {
}; };
} }
/**
* Create a Stable State Telemetry Object (State Generator) for use in visual tests
* and tests against plotting telemetry (e.g. logPlot tests). This will change state every 2 seconds.
* @param {import('@playwright/test').Page} page
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'
* @returns {Promise<CreatedObjectInfo>} An object containing information about the telemetry object.
*/
async function createStableStateTelemetry(page, parent = 'mine') {
const parentUrl = await getHashUrlToDomainObject(page, parent);
await page.goto(`${parentUrl}`);
const createdObject = await createDomainObjectWithDefaults(page, {
type: 'State Generator',
name: 'Stable State Generator'
});
// edit the state generator to have a 1 second update rate
await page.getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('2');
await page.getByLabel('Save').click();
// Wait until the URL is updated
const uuid = await getFocusedObjectUuid(page);
const url = await getHashUrlToDomainObject(page, uuid);
return {
name: createdObject.name,
uuid,
url
};
}
/** /**
* Navigates directly to a given object url, in fixed time mode, with the given start and end bounds. Note: does not set * Navigates directly to a given object url, in fixed time mode, with the given start and end bounds. Note: does not set
* default view type. * default view type.
@ -645,14 +676,34 @@ async function getCanvasPixels(page, canvasSelector) {
); );
} }
/**
* Search for telemetry and link it to an object. objectName should come from the domainObject.name function.
* @param {import('@playwright/test').Page} page
* @param {string} parameterName
* @param {string} objectName
*/
async function linkParameterToObject(page, parameterName, objectName) {
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill(parameterName);
await page.getByLabel('Object Results').getByText(parameterName).click();
await page.getByLabel('More actions').click();
await page.getByLabel('Create Link').click();
await page.getByLabel('Modal Overlay').getByLabel('Search Input').click();
await page.getByLabel('Modal Overlay').getByLabel('Search Input').fill(objectName);
await page.getByLabel('Modal Overlay').getByLabel(`Navigate to ${objectName}`).click();
await page.getByLabel('Save').click();
}
export { export {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
createExampleTelemetryObject, createExampleTelemetryObject,
createNotification, createNotification,
createPlanFromJSON, createPlanFromJSON,
createStableStateTelemetry,
expandEntireTree, expandEntireTree,
getCanvasPixels, getCanvasPixels,
getDomainObject, getDomainObject,
linkParameterToObject,
navigateToObjectWithFixedTimeBounds, navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime, navigateToObjectWithRealTime,
setEndOffset, setEndOffset,

View File

@ -26,8 +26,10 @@ import {
createExampleTelemetryObject, createExampleTelemetryObject,
createNotification, createNotification,
createPlanFromJSON, createPlanFromJSON,
createStableStateTelemetry,
expandEntireTree, expandEntireTree,
getCanvasPixels, getCanvasPixels,
linkParameterToObject,
navigateToObjectWithFixedTimeBounds, navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime, navigateToObjectWithRealTime,
setEndOffset, setEndOffset,
@ -339,4 +341,23 @@ test.describe('AppActions @framework', () => {
// Expect this step to fail // Expect this step to fail
await waitForPlotsToRender(page, { timeout: 1000 }); await waitForPlotsToRender(page, { timeout: 1000 });
}); });
test('createStableStateTelemetry', async ({ page }) => {
const stableStateTelemetry = await createStableStateTelemetry(page);
expect(stableStateTelemetry.name).toBe('Stable State Generator');
expect(stableStateTelemetry.url).toBe(`./#/browse/mine/${stableStateTelemetry.uuid}`);
expect(stableStateTelemetry.uuid).toBeDefined();
});
test('linkParameterToObject', async ({ page }) => {
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Test Display Layout'
});
const exampleTelemetry = await createExampleTelemetryObject(page);
await linkParameterToObject(page, exampleTelemetry.name, displayLayout.name);
await page.goto(displayLayout.url);
await expect(page.getByRole('main').getByText('Test Display Layout')).toBeVisible();
await expandEntireTree(page);
await expect(page.getByLabel('Navigate to VIPER Rover').first()).toBeVisible();
});
}); });

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government * Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government * Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -0,0 +1,163 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets and styling
*/
import {
createDomainObjectWithDefaults,
linkParameterToObject,
setRealTimeMode
} from '../../../../appActions.js';
import { MISSION_TIME } from '../../../../constants.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Conditionally Styling, using a Condition Set', () => {
let stateGenerator;
let conditionSet;
let displayLayout;
const STATE_CHANGE_INTERVAL = '1';
test.beforeEach(async ({ page }) => {
// Install the clock and set the time to the mission time such that the state generator will be controllable
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Condition Set, State Generator, and Display Layout
conditionSet = await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Condition Set'
});
stateGenerator = await createDomainObjectWithDefaults(page, {
type: 'State Generator',
name: 'One Second State Generator'
});
// edit the state generator to have a 1 second update rate
await page.getByTitle('More actions').click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page.getByLabel('State Duration (seconds)', { exact: true }).fill(STATE_CHANGE_INTERVAL);
await page.getByLabel('Save').click();
displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Test Display Layout'
});
});
test('Conditional styling, using a Condition Set, will style correctly based on the output @clock', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7840'
});
// set up the condition set to use the state generator
await page.goto(conditionSet.url, { waitUntil: 'domcontentloaded' });
// Add the State Generator to the Condition Set by dragging from the main tree
await page.getByLabel('Show selected item in tree').click();
await page
.getByRole('tree', {
name: 'Main Tree'
})
.getByRole('treeitem', {
name: stateGenerator.name
})
.dragTo(page.locator('#conditionCollection'));
// Add the state generator to the first criterion such that there is a condition named 'OFF' when the state generator is off
await page.getByLabel('Add Condition').click();
await page
.getByLabel('Criterion Telemetry Selection')
.selectOption({ label: stateGenerator.name });
await page.getByLabel('Criterion Metadata Selection').selectOption({ label: 'State' });
await page.getByLabel('Criterion Comparison Selection').selectOption({ label: 'is' });
await page.getByLabel('Condition Name Input').first().fill('OFF');
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await linkParameterToObject(page, stateGenerator.name, displayLayout.name);
//Add a box to the display layout
await page.goto(displayLayout.url, { waitUntil: 'domcontentloaded' });
await page.getByLabel('Edit Object').click();
//Add a box to the display layout and move it to the right
//TEMP: Click the layout such that the state generator is deselected
await page.getByLabel('Test Display Layout Layout Grid').locator('div').nth(1).click();
await page.getByLabel('Add Drawing Object').click();
await page.getByText('Box').click();
await page.getByLabel('X:').click();
await page.getByLabel('X:').fill('10');
await page.getByLabel('X:').press('Enter');
// set up conditional styling such that the box is red when the state generator condition is 'OFF'
await page.getByRole('tab', { name: 'Styles' }).click();
await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();
await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();
await page.getByLabel('Modal Overlay').getByLabel(`Preview ${conditionSet.name}`).click();
await page.getByText('Ok').click();
await page.getByLabel('Set background color').first().click();
await page.getByLabel('#ff0000').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await setRealTimeMode(page);
//Pause at a time when the state generator is 'OFF' which is 20 minutes in the future
await page.clock.pauseAt(new Date(MISSION_TIME + 1200000));
const redBG = 'background-color: rgb(255, 0, 0);';
const defaultBG = 'background-color: rgb(102, 102, 102);';
const textElement = page.getByLabel('Alpha-numeric telemetry value').locator('div:first-child');
const styledElement = page.getByLabel('Box', { exact: true });
await page.clock.resume();
// Check if the style is red when text is 'OFF'
await expect(textElement).toHaveText('OFF');
await waitForStyleChange(styledElement, redBG);
// Fast forward to the next state change
await page.clock.fastForward(STATE_CHANGE_INTERVAL * 1000);
// Check if the style is not red when text is 'ON'
await expect(textElement).toHaveText('ON');
await waitForStyleChange(styledElement, defaultBG);
});
});
/**
* Wait for the style of an element to change to the expected style.
* @param {import('@playwright/test').Locator} element - The element to check.
* @param {string} expectedStyle - The expected style to wait for.
* @param {number} timeout - The timeout in milliseconds.
*/
async function waitForStyleChange(element, expectedStyle, timeout = 0) {
await expect(async () => {
const style = await element.getAttribute('style');
// eslint-disable-next-line playwright/prefer-web-first-assertions
expect(style).toBe(expectedStyle);
}).toPass({ timeout: 1000 }); // timeout allows for the style to be applied
}

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government * Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government * Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -22,7 +22,11 @@
import percySnapshot from '@percy/playwright'; import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults } from '../../appActions.js'; import {
createDomainObjectWithDefaults,
createStableStateTelemetry,
linkParameterToObject
} from '../../appActions.js';
import { MISSION_TIME, VISUAL_FIXED_URL } from '../../constants.js'; import { MISSION_TIME, VISUAL_FIXED_URL } from '../../constants.js';
import { test } from '../../pluginFixtures.js'; import { test } from '../../pluginFixtures.js';
@ -47,16 +51,13 @@ test.describe('Visual - Display Layout @clock', () => {
name: 'Child Right Layout', name: 'Child Right Layout',
parent: parentLayout.uuid parent: parentLayout.uuid
}); });
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', const stableStateTelemetry = await createStableStateTelemetry(page);
name: 'SWG 1', await linkParameterToObject(page, stableStateTelemetry.name, child1Layout.name);
parent: child1Layout.uuid await linkParameterToObject(page, stableStateTelemetry.name, child2Layout.name);
});
await createDomainObjectWithDefaults(page, { // Pause the clock at a time where the telemetry is stable 20 minutes in the future
type: 'Sine Wave Generator', await page.clock.pauseAt(new Date(MISSION_TIME + 1200000));
name: 'SWG 2',
parent: child2Layout.uuid
});
await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' }); await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: 'Edit Object' }).click(); await page.getByRole('button', { name: 'Edit Object' }).click();

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government * Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -128,7 +128,7 @@
"test:perf:contract": "npm test --workspace e2e -- --config=playwright-performance-dev.config.js", "test:perf:contract": "npm test --workspace e2e -- --config=playwright-performance-dev.config.js",
"test:perf:localhost": "npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome", "test:perf:localhost": "npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome",
"test:perf:memory": "npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome-memory", "test:perf:memory": "npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome-memory",
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue", "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2024/gm' ./src/ui/layout/AboutDialog.vue",
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2024/gm'", "update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2024/gm'",
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e", "cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
"prepare": "npm run build:prod && npx tsc" "prepare": "npm run build:prod && npx tsc"

View File

@ -231,26 +231,20 @@ export default class TelemetryAPI {
* @returns {TelemetryRequestOptions} the options, with defaults filled in * @returns {TelemetryRequestOptions} the options, with defaults filled in
*/ */
standardizeRequestOptions(options = {}) { standardizeRequestOptions(options = {}) {
if (!Object.hasOwn(options, 'start')) { if (!Object.hasOwn(options, 'timeContext')) {
const bounds = options.timeContext?.getBounds(); options.timeContext = this.openmct.time;
if (bounds?.start) {
options.start = options.timeContext.getBounds().start;
} else {
options.start = this.openmct.time.getBounds().start;
}
}
if (!Object.hasOwn(options, 'end')) {
const bounds = options.timeContext?.getBounds();
if (bounds?.end) {
options.end = options.timeContext.getBounds().end;
} else {
options.end = this.openmct.time.getBounds().end;
}
} }
if (!Object.hasOwn(options, 'domain')) { if (!Object.hasOwn(options, 'domain')) {
options.domain = this.openmct.time.getTimeSystem().key; options.domain = options.timeContext.getTimeSystem().key;
}
if (!Object.hasOwn(options, 'start')) {
options.start = options.timeContext.getBounds().start;
}
if (!Object.hasOwn(options, 'end')) {
options.end = options.timeContext.getBounds().end;
} }
return options; return options;

View File

@ -269,36 +269,40 @@ describe('Telemetry API', () => {
await telemetryAPI.request(domainObject); await telemetryAPI.request(domainObject);
const { signal } = new AbortController(); const { signal } = new AbortController();
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system' domain: 'system',
timeContext: openmct.time
}); });
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system' domain: 'system',
timeContext: openmct.time
}); });
telemetryProvider.supportsRequest.calls.reset(); telemetryProvider.supportsRequest.calls.reset();
telemetryProvider.request.calls.reset(); telemetryProvider.request.calls.reset();
await telemetryAPI.request(domainObject, {}); await telemetryAPI.request(domainObject, {});
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system' domain: 'system',
timeContext: openmct.time
}); });
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system' domain: 'system',
timeContext: openmct.time
}); });
}); });
@ -313,18 +317,20 @@ describe('Telemetry API', () => {
domain: 'someDomain' domain: 'someDomain'
}); });
const { signal } = new AbortController(); const { signal } = new AbortController();
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {
start: 20, start: 20,
end: 30, end: 30,
domain: 'someDomain', domain: 'someDomain',
signal signal,
timeContext: openmct.time
}); });
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {
start: 20, start: 20,
end: 30, end: 30,
domain: 'someDomain', domain: 'someDomain',
signal signal,
timeContext: openmct.time
}); });
}); });
describe('telemetry batching support', () => { describe('telemetry batching support', () => {

View File

@ -62,9 +62,6 @@ export default class TelemetryCollection extends EventEmitter {
this.futureBuffer = []; this.futureBuffer = [];
this.parseTime = undefined; this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject); this.metadata = this.openmct.telemetry.getMetadata(domainObject);
if (!Object.hasOwn(options, 'timeContext')) {
options.timeContext = this.openmct.time;
}
this.options = options; this.options = options;
this.unsubscribe = undefined; this.unsubscribe = undefined;
this.pageState = undefined; this.pageState = undefined;
@ -84,6 +81,9 @@ export default class TelemetryCollection extends EventEmitter {
this._error(LOADED_ERROR); this._error(LOADED_ERROR);
} }
if (!Object.hasOwn(this.options, 'timeContext')) {
this.options.timeContext = this.openmct.time;
}
this._setTimeSystem(this.options.timeContext.getTimeSystem()); this._setTimeSystem(this.options.timeContext.getTimeSystem());
this.lastBounds = this.options.timeContext.getBounds(); this.lastBounds = this.options.timeContext.getBounds();
// prioritize passed options over time bounds // prioritize passed options over time bounds
@ -93,9 +93,6 @@ export default class TelemetryCollection extends EventEmitter {
if (this.options.end) { if (this.options.end) {
this.lastBounds.end = this.options.end; this.lastBounds.end = this.options.end;
} }
console.debug(
`🫙 Bounds for collection are start ${new Date(this.lastBounds.start).toISOString()} and end ${new Date(this.lastBounds.end).toISOString()}`
);
this._watchBounds(); this._watchBounds();
this._watchTimeSystem(); this._watchTimeSystem();
this._watchTimeModeChange(); this._watchTimeModeChange();
@ -140,10 +137,7 @@ export default class TelemetryCollection extends EventEmitter {
* @private * @private
*/ */
async _requestHistoricalTelemetry() { async _requestHistoricalTelemetry() {
console.debug( const options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });
`🫙 Requesting historical telemetry with start ${new Date(this.lastBounds.start).toISOString()} and end ${new Date(this.lastBounds.end).toISOString()}}`
);
let options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });
const historicalProvider = this.openmct.telemetry.findRequestProvider( const historicalProvider = this.openmct.telemetry.findRequestProvider(
this.domainObject, this.domainObject,
options options
@ -243,19 +237,6 @@ export default class TelemetryCollection extends EventEmitter {
beforeStartOfBounds = parsedValue < boundsToUse.start; beforeStartOfBounds = parsedValue < boundsToUse.start;
afterEndOfBounds = parsedValue > boundsToUse.end; afterEndOfBounds = parsedValue > boundsToUse.end;
if (beforeStartOfBounds) {
console.debug(
`🫙 Datum is BEFORE start of bounds: ${new Date(parsedValue).toISOString()} < ${new Date(this.lastBounds.start).toISOString()}`,
this.options
);
}
if (afterEndOfBounds) {
console.debug(
`🫙 Datum is AFTER start of bounds: ${new Date(parsedValue).toISOString()} < ${new Date(this.lastBounds.start).toISOString()}`,
this.options
);
}
if ( if (
!afterEndOfBounds && !afterEndOfBounds &&
(!beforeStartOfBounds || (this.isStrategyLatest && this.openmct.telemetry.greedyLAD())) (!beforeStartOfBounds || (this.isStrategyLatest && this.openmct.telemetry.greedyLAD()))

View File

@ -1,5 +1,4 @@
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import _ from 'lodash';
export default class CompsManager extends EventEmitter { export default class CompsManager extends EventEmitter {
#openmct; #openmct;
@ -12,6 +11,8 @@ export default class CompsManager extends EventEmitter {
#loaded = false; #loaded = false;
#compositionLoaded = false; #compositionLoaded = false;
#telemetryProcessors = {}; #telemetryProcessors = {};
#loadVersion = 0;
#currentLoadPromise = null;
constructor(openmct, domainObject) { constructor(openmct, domainObject) {
super(); super();
@ -59,7 +60,9 @@ export default class CompsManager extends EventEmitter {
name: `${this.#getNextAlphabeticalParameterName()}`, name: `${this.#getNextAlphabeticalParameterName()}`,
valueToUse, valueToUse,
testValue: 0, testValue: 0,
timeMetaData timeMetaData,
accumulateValues: false,
sampleSize: null
}); });
this.emit('parameterAdded', this.#domainObject); this.emit('parameterAdded', this.#domainObject);
} }
@ -108,23 +111,56 @@ export default class CompsManager extends EventEmitter {
} }
async load(telemetryOptions) { async load(telemetryOptions) {
if (!_.isEqual(telemetryOptions, this.#telemetryOptions)) { // Increment the load version to mark a new load operation
const loadVersion = ++this.#loadVersion;
if (!_.isEqual(this.#telemetryOptions, telemetryOptions)) {
console.debug( console.debug(
`😩 Reloading comps manager ${this.#domainObject.name} due to telemetry options change.`, `😩 Reloading comps manager ${this.#domainObject.name} due to telemetry options change.`,
telemetryOptions telemetryOptions
); );
this.#destroy(); this.#destroy();
} }
this.#telemetryOptions = telemetryOptions; this.#telemetryOptions = telemetryOptions;
// Start the load process and store the promise
this.#currentLoadPromise = (async () => {
// Load composition if not already loaded
if (!this.#compositionLoaded) { if (!this.#compositionLoaded) {
await this.#loadComposition(); await this.#loadComposition();
// Check if a newer load has been initiated
if (loadVersion !== this.#loadVersion) {
console.debug(
`🔄 Reloading comps manager in composition wait ${this.#domainObject.name} due to newer load.`
);
await this.#currentLoadPromise;
return;
}
this.#compositionLoaded = true; this.#compositionLoaded = true;
} }
// Start listening to telemetry if not already done
if (!this.#loaded) { if (!this.#loaded) {
await this.#startListeningToUnderlyingTelemetry(); await this.#startListeningToUnderlyingTelemetry();
this.#telemetryLoadedPromises = []; // Check again for newer load
if (loadVersion !== this.#loadVersion) {
console.debug(
`🔄 Reloading comps manager in telemetry wait ${this.#domainObject.name} due to newer load.`
);
await this.#currentLoadPromise;
return;
}
console.debug(
`✅ Comps manager ${this.#domainObject.name} is ready.`,
this.#telemetryCollections
);
this.#loaded = true; this.#loaded = true;
} }
})();
// Await the load process
await this.#currentLoadPromise;
} }
async #startListeningToUnderlyingTelemetry() { async #startListeningToUnderlyingTelemetry() {
@ -173,27 +209,45 @@ export default class CompsManager extends EventEmitter {
} }
} }
getFullDataFrame(newTelemetry) { #getParameterForKeyString(keyString) {
const dataFrame = {}; return this.#domainObject.configuration.comps.parameters.find(
// can assume on data item (parameter) => parameter.keyString === keyString
const newTelemetryKey = Object.keys(newTelemetry)[0];
const newTelemetryData = newTelemetry[newTelemetryKey];
const otherTelemetryKeys = Object.keys(this.#telemetryCollections).filter(
(keyString) => keyString !== newTelemetryKey
); );
// initialize the data frame with the new telemetry data }
dataFrame[newTelemetryKey] = newTelemetryData;
// initialize the other telemetry data getTelemetryForComps(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) => { otherTelemetryKeys.forEach((keyString) => {
dataFrame[keyString] = []; telemetryForComps[keyString] = [];
}); });
// march through the new telemetry data and add data to the frame from the other telemetry objects const otherTelemetryKeysNotAccumulating = otherTelemetryKeys.filter(
// using LOCF (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) => { newTelemetryData.forEach((newDatum) => {
otherTelemetryKeys.forEach((otherKeyString) => { otherTelemetryKeysNotAccumulating.forEach((otherKeyString) => {
const otherCollection = this.#telemetryCollections[otherKeyString]; const otherCollection = this.#telemetryCollections[otherKeyString];
// otherwise we need to find the closest datum to the new datum
let insertionPointForNewData = otherCollection._sortedIndex(newDatum); let insertionPointForNewData = otherCollection._sortedIndex(newDatum);
const otherCollectionData = otherCollection.getAll(); const otherCollectionData = otherCollection.getAll();
if (insertionPointForNewData && insertionPointForNewData >= otherCollectionData.length) { if (insertionPointForNewData && insertionPointForNewData >= otherCollectionData.length) {
@ -202,11 +256,11 @@ export default class CompsManager extends EventEmitter {
// get the closest datum to the new datum // get the closest datum to the new datum
const closestDatum = otherCollectionData[insertionPointForNewData]; const closestDatum = otherCollectionData[insertionPointForNewData];
if (closestDatum) { if (closestDatum) {
dataFrame[otherKeyString].push(closestDatum); telemetryForComps[otherKeyString].push(closestDatum);
} }
}); });
}); });
return dataFrame; return telemetryForComps;
} }
#removeTelemetryObject = (telemetryObjectIdentifier) => { #removeTelemetryObject = (telemetryObjectIdentifier) => {

View File

@ -5,17 +5,19 @@ onconnect = function (e) {
const port = e.ports[0]; const port = e.ports[0];
port.onmessage = function (event) { port.onmessage = function (event) {
const { type, callbackID, telemetryForComps, expression, parameters } = event.data; const { type, callbackID, telemetryForComps, expression, parameters, newTelemetry } =
event.data;
let responseType = 'unknown'; let responseType = 'unknown';
let error = null; let error = null;
let result = []; let result = [];
try { try {
if (type === 'calculateRequest') { if (type === 'calculateRequest') {
responseType = 'calculationRequestResult'; responseType = 'calculationRequestResult';
console.debug(`📫 Received new calculation request with callback ID ${callbackID}`);
result = calculateRequest(telemetryForComps, parameters, expression); result = calculateRequest(telemetryForComps, parameters, expression);
} else if (type === 'calculateSubscription') { } else if (type === 'calculateSubscription') {
responseType = 'calculationSubscriptionResult'; responseType = 'calculationSubscriptionResult';
result = calculateSubscription(telemetryForComps, parameters, expression); result = calculateSubscription(telemetryForComps, newTelemetry, parameters, expression);
} else if (type === 'init') { } else if (type === 'init') {
port.postMessage({ type: 'ready' }); port.postMessage({ type: 'ready' });
return; return;
@ -25,6 +27,7 @@ onconnect = function (e) {
} catch (errorInCalculation) { } catch (errorInCalculation) {
error = errorInCalculation; error = errorInCalculation;
} }
console.debug(`📭 Sending response for callback ID ${callbackID}`, result);
port.postMessage({ type: responseType, callbackID, result, error }); port.postMessage({ type: responseType, callbackID, result, error });
}; };
}; };
@ -40,9 +43,16 @@ function getFullDataFrame(telemetryForComps, parameters) {
return dataFrame; return dataFrame;
} }
function calculateSubscription(telemetryForComps, parameters, expression) { function calculateSubscription(telemetryForComps, newTelemetry, parameters, expression) {
const dataFrame = getFullDataFrame(telemetryForComps, parameters); const dataFrame = getFullDataFrame(telemetryForComps, parameters);
return calculate(dataFrame, parameters, expression); 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) { function calculateRequest(telemetryForComps, parameters, expression) {
@ -56,14 +66,40 @@ function calculate(dataFrame, parameters, expression) {
if (!expression) { if (!expression) {
return sumResults; 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 // take the first parameter keyString as the reference
const referenceParameter = parameters[0]; const referenceParameter = parameters[0];
const otherParameters = parameters.slice(1); const otherParameters = parameters.slice(1);
// iterate over the reference telemetry data // iterate over the reference telemetry data
const referenceTelemetry = dataFrame[referenceParameter.keyString]; const referenceTelemetry = dataFrame[referenceParameter.keyString];
referenceTelemetry?.forEach((referenceTelemetryItem) => { 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 = { const scope = {
[referenceParameter.name]: referenceTelemetryItem[referenceParameter.valueToUse] [referenceParameter.name]: referenceValue
}; };
const referenceTime = referenceTelemetryItem[referenceParameter.timeKey]; const referenceTime = referenceTelemetryItem[referenceParameter.timeKey];
// iterate over the other parameters to set the scope // iterate over the other parameters to set the scope
@ -75,7 +111,12 @@ function calculate(dataFrame, parameters, expression) {
missingData = true; missingData = true;
return; return;
} }
scope[parameter.name] = otherTelemetry[parameter.valueToUse]; let otherValue = otherTelemetry[parameter.valueToUse];
if (parameter.accumulateValues) {
accumulatedData[parameter.name].push(referenceValue);
otherValue = accumulatedData[referenceParameter.name];
}
scope[parameter.name] = otherValue;
}); });
if (missingData) { if (missingData) {
return; return;

View File

@ -61,6 +61,7 @@ export default class CompsMetadataProvider {
key: 'compsOutput', key: 'compsOutput',
source: 'compsOutput', source: 'compsOutput',
name: 'Output', name: 'Output',
derived: true,
formatString: specificCompsManager.getOutputFormat(), formatString: specificCompsManager.getOutputFormat(),
hints: { hints: {
range: 1 range: 1

View File

@ -95,7 +95,7 @@ export default class CompsTelemetryProvider {
return; return;
} }
const expression = specificCompsManager.getExpression(); const expression = specificCompsManager.getExpression();
const telemetryForComps = specificCompsManager.getFullDataFrame(newTelemetry); const telemetryForComps = specificCompsManager.getTelemetryForComps(newTelemetry);
const parameters = JSON.parse(JSON.stringify(specificCompsManager.getParameters())); const parameters = JSON.parse(JSON.stringify(specificCompsManager.getParameters()));
if (!expression || !parameters) { if (!expression || !parameters) {
return; return;
@ -103,6 +103,7 @@ export default class CompsTelemetryProvider {
const payload = { const payload = {
type: 'calculateSubscription', type: 'calculateSubscription',
telemetryForComps, telemetryForComps,
newTelemetry,
expression, expression,
parameters, parameters,
callbackID callbackID
@ -134,10 +135,6 @@ export default class CompsTelemetryProvider {
); );
return () => { return () => {
delete this.#subscriptionCallbacks[callbackID]; delete this.#subscriptionCallbacks[callbackID];
console.debug(
`🛑 Stopping subscription for ${domainObject.name} with callback ID ${callbackID}. We now have ${Object.keys(this.#subscriptionCallbacks).length} subscribers`,
this.#subscriptionCallbacks
);
specificCompsManager.stopListeningToUnderlyingTelemetry(); specificCompsManager.stopListeningToUnderlyingTelemetry();
specificCompsManager.off('underlyingTelemetryUpdated', boundComputeOnNewTelemetry); specificCompsManager.off('underlyingTelemetryUpdated', boundComputeOnNewTelemetry);
}; };

View File

@ -95,6 +95,37 @@
</option> </option>
</select> </select>
<div v-else>{{ parameter.valueToUse }}</div> <div v-else>{{ parameter.valueToUse }}</div>
<div
:class="[
'c-comps__refs-controls c-cdef__controls',
{ disabled: !parameters?.length }
]"
>
<label v-if="isEditing" 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-test-datum__string"
>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--md"
@change="updateParameters"
/>
</div>
</span> </span>
<span v-if="isEditing" class="c-test-datum__string">Test value</span> <span v-if="isEditing" class="c-test-datum__string">Test value</span>
@ -104,7 +135,7 @@
:aria-label="`Reference Test Value for ${parameter.name}`" :aria-label="`Reference Test Value for ${parameter.name}`"
type="text" type="text"
class="c-input--md" class="c-input--md"
@change="updateParameters" @change="updateTestValue(parameter)"
/> />
</div> </div>
</div> </div>
@ -235,6 +266,22 @@ function updateParameters() {
applyTestData(); applyTestData();
} }
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() { function toggleTestData() {
testDataApplied.value = !testDataApplied.value; testDataApplied.value = !testDataApplied.value;
if (testDataApplied.value) { if (testDataApplied.value) {
@ -270,6 +317,20 @@ function applyTestData() {
} }
return acc; 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 { try {
const testOutput = evaluate(expression.value, scope); const testOutput = evaluate(expression.value, scope);
const formattedData = getValueFormatter().format(testOutput); const formattedData = getValueFormatter().format(testOutput);

View File

@ -69,7 +69,6 @@ const INNER_TEXT_PADDING = 15;
const TEXT_LEFT_PADDING = 5; const TEXT_LEFT_PADDING = 5;
const ROW_PADDING = 5; const ROW_PADDING = 5;
const SWIMLANE_PADDING = 3; const SWIMLANE_PADDING = 3;
const RESIZE_POLL_INTERVAL = 200;
const ROW_HEIGHT = 22; const ROW_HEIGHT = 22;
const MAX_TEXT_WIDTH = 300; const MAX_TEXT_WIDTH = 300;
const MIN_ACTIVITY_WIDTH = 2; const MIN_ACTIVITY_WIDTH = 2;
@ -143,13 +142,15 @@ export default {
this.canvasContext = canvas.getContext('2d'); this.canvasContext = canvas.getContext('2d');
this.setDimensions(); this.setDimensions();
this.setTimeContext(); this.setTimeContext();
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.handleConfigurationChange(this.configuration); this.handleConfigurationChange(this.configuration);
this.planViewConfiguration.on('change', this.handleConfigurationChange); this.planViewConfiguration.on('change', this.handleConfigurationChange);
this.loadComposition(); this.loadComposition();
this.resizeObserver = new ResizeObserver(this.resize);
this.resizeObserver.observe(this.$refs.plan);
}, },
beforeUnmount() { beforeUnmount() {
clearInterval(this.resizeTimer); this.resizeObserver.disconnect();
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();

View File

@ -1,5 +1,5 @@
<!-- <!--
Open MCT, Copyright (c) 2014-2023, United States Government Open MCT, Copyright (c) 2014-2024, United States Government
as represented by the Administrator of the National Aeronautics and Space as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved. Administration. All rights reserved.

View File

@ -1,5 +1,5 @@
<!-- <!--
Open MCT, Copyright (c) 2014-2023, United States Government Open MCT, Copyright (c) 2014-2024, United States Government
as represented by the Administrator of the National Aeronautics and Space as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved. Administration. All rights reserved.

View File

@ -225,7 +225,13 @@ export default class PlotSeries extends Model {
try { try {
const points = await this.openmct.telemetry.request(this.domainObject, options); const points = await this.openmct.telemetry.request(this.domainObject, options);
const data = this.getSeriesData(); // if derived, we can't use the old data
let data = this.getSeriesData();
if (this.metadata.value(this.get('yKey')).derived) {
data = [];
}
// eslint-disable-next-line you-dont-need-lodash-underscore/concat // eslint-disable-next-line you-dont-need-lodash-underscore/concat
const newPoints = _(data) const newPoints = _(data)
.concat(points) .concat(points)

View File

@ -133,7 +133,6 @@ export default {
if (!styleObj || !elemToStyle) { if (!styleObj || !elemToStyle) {
return; return;
} }
// handle visibility separately // handle visibility separately
if (styleObj.isStyleInvisible !== undefined) { if (styleObj.isStyleInvisible !== undefined) {
elemToStyle.classList.toggle(STYLE_CONSTANTS.isStyleInvisible, styleObj.isStyleInvisible); elemToStyle.classList.toggle(STYLE_CONSTANTS.isStyleInvisible, styleObj.isStyleInvisible);

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government * Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government * Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -152,17 +152,20 @@ export default class RemoteClock extends DefaultClock {
*/ */
#waitForReady() { #waitForReady() {
const waitForInitialTick = (resolve) => { const waitForInitialTick = (resolve) => {
const tickListener = () => {
if (this.lastTick > 0) { if (this.lastTick > 0) {
const offsets = this.openmct.time.getClockOffsets(); const offsets = this.openmct.time.getClockOffsets();
this.openmct.time.off('tick', tickListener); // Unregister the tick listener
resolve({ resolve({
start: this.lastTick + offsets.start, start: this.lastTick + offsets.start,
end: this.lastTick + offsets.end end: this.lastTick + offsets.end
}); });
} else {
setTimeout(() => waitForInitialTick(resolve), 100);
} }
}; };
this.openmct.time.on('tick', tickListener);
};
return new Promise(waitForInitialTick); return new Promise(waitForInitialTick);
} }
} }

View File

@ -20,16 +20,43 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/**
* Intercepts requests to ensure the remote clock is ready.
*
* @param {import('../../openmct').OpenMCT} openmct - The OpenMCT instance.
* @param {import('../../openmct').Identifier} _remoteClockIdentifier - The identifier for the remote clock.
* @param {Function} waitForBounds - A function that returns a promise resolving to the initial bounds.
* @returns {Object} The request interceptor.
*/
function remoteClockRequestInterceptor(openmct, _remoteClockIdentifier, waitForBounds) { function remoteClockRequestInterceptor(openmct, _remoteClockIdentifier, waitForBounds) {
let remoteClockLoaded = false; let remoteClockLoaded = false;
return { return {
appliesTo: () => { /**
* Determines if the interceptor applies to the given request.
*
* @param {Object} _ - Unused parameter.
* @param {import('../../api/telemetry/TelemetryAPI').TelemetryRequestOptions} request - The request object.
* @returns {boolean} True if the interceptor applies, false otherwise.
*/
appliesTo: (_, request) => {
// Get the activeClock from the Global Time Context // Get the activeClock from the Global Time Context
/** @type {import("../../api/time/TimeContext").default} */
const { activeClock } = openmct.time; const { activeClock } = openmct.time;
// this type of request does not rely on clock having bounds
if (request.strategy === 'latest' && request.timeContext.isRealTime()) {
return false;
}
return activeClock?.key === 'remote-clock' && !remoteClockLoaded; return activeClock?.key === 'remote-clock' && !remoteClockLoaded;
}, },
/**
* Invokes the interceptor to modify the request.
*
* @param {Object} request - The request object.
* @returns {Promise<Object>} The modified request object.
*/
invoke: async (request) => { invoke: async (request) => {
const timeContext = request?.timeContext ?? openmct.time; const timeContext = request?.timeContext ?? openmct.time;

View File

@ -150,13 +150,13 @@ export default class TableRowCollection extends EventEmitter {
} }
insertOrUpdateRows(rowsToAdd, addToBeginning) { insertOrUpdateRows(rowsToAdd, addToBeginning) {
rowsToAdd.forEach((row) => { rowsToAdd.forEach((row, addRowsIndex) => {
const index = this.getInPlaceUpdateIndex(row); const index = this.getInPlaceUpdateIndex(row);
if (index > -1) { if (index > -1) {
this.updateRowInPlace(row, index); this.updateRowInPlace(row, index);
} else { } else {
if (addToBeginning) { if (addToBeginning) {
this.rows.unshift(row); this.rows.splice(addRowsIndex, 0, row);
} else { } else {
this.rows.push(row); this.rows.push(row);
} }

View File

@ -150,7 +150,7 @@ export default {
mounted() { mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300, { this.handleNewBounds = _.throttle(this.handleNewBounds, 300, {
leading: true, leading: true,
trailing: false trailing: true
}); });
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem())); this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem); this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
@ -181,6 +181,8 @@ export default {
} }
}, },
stopFollowingTime() { stopFollowingTime() {
this.handleNewBounds.cancel();
if (this.timeContext) { if (this.timeContext) {
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds); this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets); this.timeContext.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);

View File

@ -1,16 +1,13 @@
/******************************************************** PROGRESS BAR */ /******************************************************** PROGRESS BAR */
@keyframes progressIndeterminate { @keyframes progressIndeterminate {
0% { 0% {
left: 0; transform:scaleX(0);
width: 0;
} }
70% { 90% {
left: 0; transform:scaleX(1);
width: 100%;
opacity: 1; opacity: 1;
} }
100% { 100% {
left: 100%;
opacity: 0; opacity: 0;
} }
} }
@ -24,11 +21,10 @@
&__bar { &__bar {
background: $colorProgressBar; background: $colorProgressBar;
height: 100%; transform-origin: left;
min-height: $progressBarMinH;
&.--indeterminate { &.--indeterminate {
position: absolute; @include abs();
animation: progressIndeterminate 1.5s ease-in infinite; animation: progressIndeterminate 1.5s ease-in infinite;
} }
} }

View File

@ -33,7 +33,7 @@
<h1 class="l-title s-title">Open MCT</h1> <h1 class="l-title s-title">Open MCT</h1>
<div class="l-description s-description"> <div class="l-description s-description">
<p> <p>
Open MCT, Copyright &copy; 2014-2023, United States Government as represented by the Open MCT, Copyright &copy; 2014-2024, United States Government as represented by the
Administrator of the National Aeronautics and Space Administration. All rights reserved. Administrator of the National Aeronautics and Space Administration. All rights reserved.
</p> </p>
<p> <p>