Merge remote-tracking branch 'origin/master' into telemetry-comps-with-acc

This commit is contained in:
Scott Bell
2024-10-09 13:53:23 +02:00
35 changed files with 676 additions and 225 deletions

View File

@ -5,11 +5,11 @@ orbs:
executors: executors:
pw-focal-development: pw-focal-development:
docker: docker:
- image: mcr.microsoft.com/playwright:v1.45.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:
@ -198,7 +198,7 @@ jobs:
steps: steps:
- build_and_install: - build_and_install:
node-version: lts/hydrogen node-version: lts/hydrogen
- run: npx playwright@1.45.2 install #Necessary for bare ubuntu machine - run: npx playwright@1.47.2 install #Necessary for bare ubuntu machine
- run: | - run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs) export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@ -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

@ -37,7 +37,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.45.2 install - run: npx playwright@1.47.2 install
- name: Start CouchDB Docker Container and Init with Setup Scripts - name: Start CouchDB Docker Container and Init with Setup Scripts
run: | run: |

View File

@ -30,7 +30,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.45.2 install - run: npx playwright@1.47.2 install
- run: npm ci --no-audit --progress=false - run: npm ci --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times) - name: Run E2E Tests (Repeated 10 Times)

View File

@ -28,7 +28,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.45.2 install - run: npx playwright@1.47.2 install
- run: npm ci --no-audit --progress=false - run: npm ci --no-audit --progress=false
- run: npm run test:perf:localhost - run: npm run test:perf:localhost
- run: npm run test:perf:contract - run: npm run test:perf:contract

View File

@ -33,7 +33,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.45.2 install - run: npx playwright@1.47.2 install
- run: npx playwright install chrome-beta - run: npx playwright install chrome-beta
- run: npm ci --no-audit --progress=false - run: npm ci --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40 - run: npm run test:e2e:full -- --max-failures=40

116
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,116 @@
# GitHub Actions Workflow for Automated Releases
name: Automated Release Workflow
on:
schedule:
# Nightly builds at 6 PM PST every day
- cron: '0 2 * * *'
release:
types:
- created
- published
jobs:
nightly-build:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
name: Nightly Build and Release
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/iron' # Specify your Node.js version
registry-url: 'https://registry.npmjs.org/'
- name: Install Dependencies
run: npm ci
- name: Bump Version for Nightly
id: bump_version
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
DATE=$(date +%Y%m%d)
NIGHTLY_VERSION=$(echo $PACKAGE_VERSION | awk -F. -v OFS=. '{$NF+=1; print}')-nightly-$DATE
echo "NIGHTLY_VERSION=${NIGHTLY_VERSION}" >> $GITHUB_ENV
- name: Update package.json
run: |
npm version $NIGHTLY_VERSION --no-git-tag-version
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json
git commit -m "chore: bump version to $NIGHTLY_VERSION for nightly build"
- name: Push Changes
uses: ad-m/github-push-action@v0.6.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
- name: Build Project
run: npm run build:prod
- name: Publish Nightly to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm publish --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
prerelease-build:
if: github.event.release.prerelease == true
runs-on: ubuntu-latest
name: Pre-release (Beta) Build and Publish
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: '16' # Specify your Node.js version
registry-url: 'https://registry.npmjs.org/'
- name: Install Dependencies
run: npm ci
- name: Build Project
run: npm run build:prod
- name: Publish Beta to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
stable-release-build:
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
name: Stable Release Build and Publish
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: '16' # Specify your Node.js version
registry-url: 'https://registry.npmjs.org/'
- name: Install Dependencies
run: npm ci
- name: Build Project
run: npm run build:prod
- name: Publish to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -227,6 +227,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.
@ -629,13 +660,33 @@ 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,
linkParameterToObject,
navigateToObjectWithFixedTimeBounds, navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime, navigateToObjectWithRealTime,
setEndOffset, setEndOffset,

View File

@ -16,7 +16,7 @@
"devDependencies": { "devDependencies": {
"@percy/cli": "1.27.4", "@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4", "@percy/playwright": "1.0.4",
"@playwright/test": "1.45.2", "@playwright/test": "1.47.2",
"@axe-core/playwright": "4.8.5" "@axe-core/playwright": "4.8.5"
}, },
"author": { "author": {

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

28
package-lock.json generated
View File

@ -104,7 +104,7 @@
"@axe-core/playwright": "4.8.5", "@axe-core/playwright": "4.8.5",
"@percy/cli": "1.27.4", "@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4", "@percy/playwright": "1.0.4",
"@playwright/test": "1.45.2" "@playwright/test": "1.47.2"
} }
}, },
"e2e/node_modules/@percy/cli": { "e2e/node_modules/@percy/cli": {
@ -1560,12 +1560,13 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.45.2", "version": "1.47.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz",
"integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", "integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.45.2" "playwright": "1.47.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8741,12 +8742,13 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.45.2", "version": "1.47.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz",
"integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", "integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.45.2" "playwright-core": "1.47.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8759,10 +8761,11 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.45.2", "version": "1.47.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz",
"integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==", "integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },
@ -8776,6 +8779,7 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"

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
@ -137,7 +137,7 @@ export default class TelemetryCollection extends EventEmitter {
* @private * @private
*/ */
async _requestHistoricalTelemetry() { async _requestHistoricalTelemetry() {
let options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options }); const 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

View File

@ -39,7 +39,7 @@ export default class ConditionManager extends EventEmitter {
this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this); this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this);
this.compositionLoad = this.composition.load(); this.compositionLoad = this.composition.load();
this.subscriptions = {}; this.telemetryCollections = {};
this.telemetryObjects = {}; this.telemetryObjects = {};
this.testData = { this.testData = {
conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData, conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData,
@ -48,55 +48,46 @@ export default class ConditionManager extends EventEmitter {
this.initialize(); this.initialize();
} }
async requestLatestValue(endpoint) { subscribeToTelemetry(telemetryObject) {
const options = { const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (this.telemetryCollections[keyString]) {
return;
}
const requestOptions = {
size: 1, size: 1,
strategy: 'latest' strategy: 'latest'
}; };
const latestData = await this.openmct.telemetry.request(endpoint, options);
if (!latestData) { this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection(
throw new Error('Telemetry request failed by returning a falsy response'); telemetryObject,
} requestOptions
if (latestData.length === 0) {
return;
}
this.telemetryReceived(endpoint, latestData[0]);
}
subscribeToTelemetry(endpoint) {
const telemetryKeyString = this.openmct.objects.makeKeyString(endpoint.identifier);
if (this.subscriptions[telemetryKeyString]) {
return;
}
const metadata = this.openmct.telemetry.getMetadata(endpoint);
this.telemetryObjects[telemetryKeyString] = Object.assign({}, endpoint, {
telemetryMetaData: metadata ? metadata.valueMetadatas : []
});
// get latest telemetry value (in case subscription is cached and no new data is coming in)
this.requestLatestValue(endpoint);
this.subscriptions[telemetryKeyString] = this.openmct.telemetry.subscribe(
endpoint,
this.telemetryReceived.bind(this, endpoint)
); );
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
const telemetryMetaData = metadata ? metadata.valueMetadatas : [];
this.telemetryObjects[keyString] = { ...telemetryObject, telemetryMetaData };
this.telemetryCollections[keyString].on(
'add',
this.telemetryReceived.bind(this, telemetryObject)
);
this.telemetryCollections[keyString].load();
this.updateConditionTelemetryObjects(); this.updateConditionTelemetryObjects();
} }
unsubscribeFromTelemetry(endpointIdentifier) { unsubscribeFromTelemetry(endpointIdentifier) {
const id = this.openmct.objects.makeKeyString(endpointIdentifier); const keyString = this.openmct.objects.makeKeyString(endpointIdentifier);
if (!this.subscriptions[id]) { if (!this.telemetryCollections[keyString]) {
console.log('no subscription to remove');
return; return;
} }
this.subscriptions[id](); this.telemetryCollections[keyString].destroy();
delete this.subscriptions[id]; this.telemetryCollections[keyString] = null;
delete this.telemetryObjects[id]; this.telemetryObjects[keyString] = null;
this.removeConditionTelemetryObjects(); this.removeConditionTelemetryObjects();
//force re-computation of condition set result as we might be in a state where //force re-computation of condition set result as we might be in a state where
@ -107,7 +98,7 @@ export default class ConditionManager extends EventEmitter {
this.timeSystems, this.timeSystems,
this.openmct.time.getTimeSystem() this.openmct.time.getTimeSystem()
); );
this.updateConditionResults({ id: id }); this.updateConditionResults({ id: keyString });
this.updateCurrentCondition(latestTimestamp); this.updateCurrentCondition(latestTimestamp);
if (Object.keys(this.telemetryObjects).length === 0) { if (Object.keys(this.telemetryObjects).length === 0) {
@ -410,11 +401,13 @@ export default class ConditionManager extends EventEmitter {
return this.openmct.time.getBounds().end >= currentTimestamp; return this.openmct.time.getBounds().end >= currentTimestamp;
} }
telemetryReceived(endpoint, datum) { telemetryReceived(endpoint, data) {
if (!this.isTelemetryUsed(endpoint)) { if (!this.isTelemetryUsed(endpoint)) {
return; return;
} }
const datum = data[0];
const normalizedDatum = this.createNormalizedDatum(datum, endpoint); const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.getTimeSystem().key; const timeSystemKey = this.openmct.time.getTimeSystem().key;
let timestamp = {}; let timestamp = {};
@ -507,8 +500,9 @@ export default class ConditionManager extends EventEmitter {
destroy() { destroy() {
this.composition.off('add', this.subscribeToTelemetry, this); this.composition.off('add', this.subscribeToTelemetry, this);
this.composition.off('remove', this.unsubscribeFromTelemetry, this); this.composition.off('remove', this.unsubscribeFromTelemetry, this);
Object.values(this.subscriptions).forEach((unsubscribe) => unsubscribe()); Object.values(this.telemetryCollections).forEach((telemetryCollection) =>
delete this.subscriptions; telemetryCollection.destroy()
);
this.conditions.forEach((condition) => { this.conditions.forEach((condition) => {
condition.destroy(); condition.destroy();

View File

@ -720,50 +720,69 @@ describe('the plugin', function () {
}; };
}); });
it('should evaluate as old when telemetry is not received in the allotted time', (done) => { it('should evaluate as old when telemetry is not received in the allotted time', async () => {
let onAddResolve;
const onAddCalledPromise = new Promise((resolve) => {
onAddResolve = resolve;
});
const mockTelemetryCollection = {
load: jasmine.createSpy('load'),
on: jasmine.createSpy('on').and.callFake((event, callback) => {
if (event === 'add') {
onAddResolve();
}
})
};
openmct.telemetry = jasmine.createSpyObj('telemetry', [ openmct.telemetry = jasmine.createSpyObj('telemetry', [
'subscribe',
'getMetadata', 'getMetadata',
'request', 'request',
'getValueFormatter', 'getValueFormatter',
'abortAllRequests' 'abortAllRequests',
'requestCollection'
]); ]);
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
openmct.telemetry.getMetadata.and.returnValue({ openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry, ...testTelemetryObject.telemetry,
valueMetadatas: [] valueMetadatas: testTelemetryObject.telemetry.values,
valuesForHints: jasmine
.createSpy('valuesForHints')
.and.returnValue(testTelemetryObject.telemetry.values),
value: jasmine.createSpy('value').and.callFake((key) => {
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
})
}); });
openmct.telemetry.request.and.returnValue(Promise.resolve([])); openmct.telemetry.requestCollection.and.returnValue(mockTelemetryCollection);
openmct.telemetry.getValueFormatter.and.returnValue({ openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) { parse: function (value) {
return value; return value;
} }
}); });
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = { conditionMgr.telemetryObjects = {
'test-object': testTelemetryObject 'test-object': testTelemetryObject
}; };
conditionMgr.updateConditionTelemetryObjects(); conditionMgr.updateConditionTelemetryObjects();
setTimeout(() => { // Wait for the 'on' callback to be called
expect(mockListener).toHaveBeenCalledWith({ await onAddCalledPromise;
output: 'Any old telemetry',
id: { // Simulate the passage of time and no data received
namespace: '', await new Promise((resolve) => setTimeout(resolve, 400));
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
}, expect(mockListener).toHaveBeenCalledWith({
conditionId: '39584410-cbf9-499e-96dc-76f27e69885d', output: 'Any old telemetry',
utc: undefined id: {
}); namespace: '',
done(); key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
}, 400); },
conditionId: '39584410-cbf9-499e-96dc-76f27e69885d',
utc: undefined
});
}); });
it('should not evaluate as old when telemetry is received in the allotted time', (done) => { it('should not evaluate as old when telemetry is received in the allotted time', async () => {
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: testTelemetryObject.telemetry.values
});
const testDatum = { const testDatum = {
'some-key2': '', 'some-key2': '',
utc: 1, utc: 1,
@ -771,8 +790,49 @@ describe('the plugin', function () {
'some-key': null, 'some-key': null,
id: 'test-object' id: 'test-object'
}; };
openmct.telemetry.request = jasmine.createSpy('request');
let onAddResolve;
let onAddCallback;
const onAddCalledPromise = new Promise((resolve) => {
onAddResolve = resolve;
});
const mockTelemetryCollection = {
load: jasmine.createSpy('load'),
on: jasmine.createSpy('on').and.callFake((event, callback) => {
if (event === 'add') {
onAddCallback = callback;
onAddResolve();
}
})
};
openmct.telemetry = jasmine.createSpyObj('telemetry', [
'getMetadata',
'getValueFormatter',
'request',
'subscribe',
'requestCollection'
]);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum])); openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum]));
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: testTelemetryObject.telemetry.values,
valuesForHints: jasmine
.createSpy('valuesForHints')
.and.returnValue(testTelemetryObject.telemetry.values),
value: jasmine.createSpy('value').and.callFake((key) => {
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
})
});
openmct.telemetry.requestCollection.and.returnValue(mockTelemetryCollection);
openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) {
return value;
}
});
const date = 1; const date = 1;
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input =
['0.4']; ['0.4'];
@ -782,19 +842,25 @@ describe('the plugin', function () {
'test-object': testTelemetryObject 'test-object': testTelemetryObject
}; };
conditionMgr.updateConditionTelemetryObjects(); conditionMgr.updateConditionTelemetryObjects();
conditionMgr.telemetryReceived(testTelemetryObject, testDatum);
setTimeout(() => { // Wait for the 'on' callback to be called
expect(mockListener).toHaveBeenCalledWith({ await onAddCalledPromise;
output: 'Default',
id: { // Simulate receiving telemetry data
namespace: '', onAddCallback([testDatum]);
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
}, // Wait a bit for the condition manager to process the data
conditionId: '2532d90a-e0d6-4935-b546-3123522da2de', await new Promise((resolve) => setTimeout(resolve, 100));
utc: date
}); expect(mockListener).toHaveBeenCalledWith({
done(); output: 'Default',
}, 300); id: {
namespace: '',
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
},
conditionId: '2532d90a-e0d6-4935-b546-3123522da2de',
utc: date
});
}); });
}); });
@ -902,17 +968,25 @@ describe('the plugin', function () {
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata'); openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({ openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry, ...testTelemetryObject.telemetry,
valueMetadatas: [] valueMetadatas: testTelemetryObject.telemetry.values,
valuesForHints: jasmine
.createSpy('valuesForHints')
.and.returnValue(testTelemetryObject.telemetry.values),
value: jasmine.createSpy('value').and.callFake((key) => {
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
})
}); });
conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = { conditionMgr.telemetryObjects = {
'test-object': testTelemetryObject 'test-object': testTelemetryObject
}; };
conditionMgr.updateConditionTelemetryObjects(); conditionMgr.updateConditionTelemetryObjects();
conditionMgr.telemetryReceived(testTelemetryObject, { conditionMgr.telemetryReceived(testTelemetryObject, [
'some-key': 2, {
utc: date 'some-key': 2,
}); utc: date
}
]);
let result = conditionMgr.conditions.map((condition) => condition.result); let result = conditionMgr.conditions.map((condition) => condition.result);
expect(result[2]).toBeUndefined(); expect(result[2]).toBeUndefined();
}); });
@ -1002,26 +1076,37 @@ describe('the plugin', function () {
} }
}; };
openmct.$injector = jasmine.createSpyObj('$injector', ['get']); openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
// const mockTransactionService = jasmine.createSpyObj(
// 'transactionService',
// ['commit']
// );
openmct.telemetry = jasmine.createSpyObj('telemetry', [ openmct.telemetry = jasmine.createSpyObj('telemetry', [
'isTelemetryObject', 'isTelemetryObject',
'request',
'subscribe', 'subscribe',
'getMetadata', 'getMetadata',
'getValueFormatter', 'getValueFormatter',
'request' 'requestCollection'
]); ]);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {}); openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: testTelemetryObject.telemetry.values,
valuesForHints: jasmine
.createSpy('valuesForHints')
.and.returnValue(testTelemetryObject.telemetry.values),
value: jasmine.createSpy('value').and.callFake((key) => {
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
})
});
openmct.telemetry.getValueFormatter.and.returnValue({ openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) { parse: function (value) {
return value; return value;
} }
}); });
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); openmct.telemetry.requestCollection.and.returnValue({
openmct.telemetry.request.and.returnValue(Promise.resolve([])); load: jasmine.createSpy('load'),
on: jasmine.createSpy('on')
});
const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true); const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true);
spyOn(styleRuleManger, 'subscribeToConditionSet'); spyOn(styleRuleManger, 'subscribeToConditionSet');

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

@ -128,35 +128,22 @@ export default {
} }
}, },
updateStyle(styleObj) { updateStyle(styleObj) {
let elemToStyle = this.getStyleReceiver(); const elemToStyle = this.getStyleReceiver();
if (!styleObj || elemToStyle === undefined) { if (!styleObj || !elemToStyle) {
return; return;
} }
// handle visibility separately
if (styleObj.isStyleInvisible !== undefined) {
elemToStyle.classList.toggle(STYLE_CONSTANTS.isStyleInvisible, styleObj.isStyleInvisible);
styleObj.isStyleInvisible = null;
}
let keys = Object.keys(styleObj); Object.entries(styleObj).forEach(([key, value]) => {
if (typeof value !== 'string' || !value.includes('__no_value')) {
keys.forEach((key) => { elemToStyle.style[key] = value;
if (elemToStyle) { } else {
if (typeof styleObj[key] === 'string' && styleObj[key].indexOf('__no_value') > -1) { elemToStyle.style[key] = ''; // remove the property
if (elemToStyle.style[key]) {
elemToStyle.style[key] = '';
}
} else {
if (
!styleObj.isStyleInvisible &&
elemToStyle.classList.contains(STYLE_CONSTANTS.isStyleInvisible)
) {
elemToStyle.classList.remove(STYLE_CONSTANTS.isStyleInvisible);
} else if (
styleObj.isStyleInvisible &&
!elemToStyle.classList.contains(styleObj.isStyleInvisible)
) {
elemToStyle.classList.add(styleObj.isStyleInvisible);
}
elemToStyle.style[key] = styleObj[key];
}
} }
}); });
} }

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,15 +152,18 @@ export default class RemoteClock extends DefaultClock {
*/ */
#waitForReady() { #waitForReady() {
const waitForInitialTick = (resolve) => { const waitForInitialTick = (resolve) => {
if (this.lastTick > 0) { const tickListener = () => {
const offsets = this.openmct.time.getClockOffsets(); if (this.lastTick > 0) {
resolve({ const offsets = this.openmct.time.getClockOffsets();
start: this.lastTick + offsets.start, this.openmct.time.off('tick', tickListener); // Unregister the tick listener
end: this.lastTick + offsets.end resolve({
}); start: this.lastTick + offsets.start,
} else { end: this.lastTick + offsets.end
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>