Compare commits

..

20 Commits

Author SHA1 Message Date
978b2ee3f9 move get/set of sort options to where it should be in table configuration class 2024-10-11 11:06:12 -07:00
142805b827 const instead of let and remove irrelevant comment 2024-10-10 10:55:29 -07:00
67a38abe45 Merge branch 'master' into sort-fixes 2024-10-08 17:13:48 -07:00
deb73ae9bf adding trailing space that was removed by accident 2024-10-08 16:59:03 -07:00
62fdc86d64 removing maps 2024-10-08 16:56:28 -07:00
7f41b55cbd if in performance mode and sorting, rerequest telemetry 2024-10-08 16:43:22 -07:00
a29fbc6736 missed a spot for in memory sort 2024-10-08 16:23:57 -07:00
69c974cafa maps for debuggin 2024-10-08 16:03:51 -07:00
341fbe558e store sort in memory if not able to be stored on config 2024-10-08 15:00:49 -07:00
4415fe7952 Fix complex displays not loading (#7858)
Clock
* pass in default timeContext in request options
* observe for clock tick, instead of polling, to determine if clock has time set

Progress Bars
* use scale instead of move animation

Plan
* use a ResizeObserver instead of polling for size changes

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-10-08 13:10:35 -07:00
83e4a124e2 Fix table sorting in descending order (#7863)
When adding sorted arrays to the beginning, make sure to insert them in the same order and not accidentally reverse them while inserting.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-10-08 13:56:28 +00:00
47f0b66c7e [CI] Fix flake with clocks and state generators (#7867)
* add two appActions

* replace with appAction

* replace with determinsitic appAction

* fix lint

* speed
2024-10-04 15:47:25 -07:00
55c023d1eb chore: it is 2024. (#7864) 2024-10-03 06:28:26 -07:00
37b2660f27 [Conditionals] Increase performance, switch to TelemetryCollections (#7841)
* adding telemetry collections to condition manager

* handling telemetry collection data not datum

* adding from maaster

* addressing PR comments

* update unit test to work with telemetry collections

* fixing tests

* removing unnecessary addition

* removing focused describe

* removing focused it

* fix weird test bleed

* adding test for conditional styling

* removing some auto fix es-lint

* got a bit overzealous

* clarification

* using raf utility which handles it correctly and moving visiblity handling into the raf for consistency and performance

* using raf correctly

* removing raf, was causing issues

* move the test and add some determinism

* oops only

* missed lint

* got it!

* fix comments

* test(condStyling): stabilize test

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-10-02 14:14:15 -07:00
43cc963328 chore: bump @playwright/test to 1.47.2 (#7859) 2024-10-01 21:01:02 +00:00
ad30a0e2d0 feat(Fault Management): allow fault providers to define shelve durations (#7849)
* refactor: clean up FaultManagementView code

* feat: providers can now provide "Shelve Duration" options

* fix(exampleFaultSource): support `getShelveDurations`

* a11y: aria label for fault management list item

* a11y(FaultManagement): more labels

* refactor: eliminate some faultUtils and refactor locator() out of tests

* docs: add some more docs to fault management api

* refactor: make for loop more readable

* test: use static faults when testing

* fix: set a timestamp for static faults and subtract so we get faults in order

* refactor: autoformat

* chore: add missing copyright header

* fix: use as default parameter to get value as method is called

* refactor: make magic number a const

* fix(codecov): use codecov github action to upload

* fix: generate the report

* build: update circleci yml to use codecov orb

* build: remove codecov scripts and package

* build: don't use the orb because things can't be easy

- nasa org disallows "third party" orbs

* build: only use `sudo` if we ain't da root user

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-10-01 10:41:18 -07:00
29f1956d1a Improve telemetry buffering implementation (#7837)
* Simplifies the implementation of telemetry buffering in Open MCT.
* Switches from per-parameter buffering to a shared queue. Per-parameter buffering is too easily overwhelmed by bursts of telemetry from high-frequency parameters. A single shared buffer has more overhead to grow when a burst arrives without overflowing, because the buffer is shared across all parameters.
* Removes the need for plugins to pass serialized code to a worker.
* Switched to a "one-size-fits-all" batching strategy removing the need for plugins to define a batching strategy at all.
* Captures buffering statistics for display in the PerformanceIndicator.
2024-09-30 14:36:40 -07:00
c498f7d20c Fix bad color value for Gauge 'Needle' type (#7821)
* Closes #7820
- Fix 0 opacity fill in theme constants files for Gauge type needle.

* move gauge plugin to its own suite

* add two more snapshots

* driveby: fix some flake

* bug: update linting rule

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-09-28 09:17:32 -07:00
a8fbabe695 fix(#7825): imagery pause (#7842)
* more readable

* unpause explicitly

* fix jsdoc

* e2e testing multiple image removal

* prettier

* fix to remove multiple images from history

* move tests that use playwright clock api into own file

* fix playwright clock tests

* add aria-label to element

* prevent straggler debounced function call on unmount

* clean up and fix tests

* update paths

* lint fix

* lint fix

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-09-27 14:32:14 -07:00
e792403788 Bar graphs should only get latest historical datum (#7811)
* Only as for latest historical telemetry

* Add test for size 1 request when a bar graph is loaded

* Use strategy latest instead of size 1 for historical request

* Fix linting issues

* Add size and strategy

* Remove bar graph tests

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-09-25 09:37:38 -07:00
65 changed files with 2093 additions and 2409 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)
@ -37,14 +37,45 @@ commands:
ls -latR >> /tmp/artifacts/dir.txt ls -latR >> /tmp/artifacts/dir.txt
- store_artifacts: - store_artifacts:
path: /tmp/artifacts/ path: /tmp/artifacts/
download_verify_codecov_cli:
description: 'Download and verify Codecov CLI'
steps:
- run:
name: Download and verify Codecov CLI
command: |
# Download Codecov CLI
curl -Os https://cli.codecov.io/latest/linux/codecov
# Import Codecov's GPG key
curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import
# Download and verify the SHA256SUM and its signature
curl -Os https://cli.codecov.io/latest/linux/codecov.SHA256SUM
curl -Os https://cli.codecov.io/latest/linux/codecov.SHA256SUM.sig
gpgv codecov.SHA256SUM.sig codecov.SHA256SUM
# Verify the checksum
shasum -a 256 -c codecov.SHA256SUM
# Make the codecov executable
[[ $EUID -ne 0 ]] && sudo chmod +x codecov || chmod +x codecov
./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
steps: steps:
- run: npm run cov:e2e:report || true - run: npm run cov:e2e:report || true
- run: npm run cov:e2e:<<parameters.suite>>:publish - download_verify_codecov_cli
- run:
name: Upload coverage report to Codecov
command: |
./codecov --verbose upload-process --disable-search \
-t $CODECOV_TOKEN \
-n 'e2e-<<parameters.suite>>'-${CIRCLE_WORKFLOW_ID} \
-F e2e-<<parameters.suite>> \
-f ./coverage/e2e/lcov.info
jobs: jobs:
npm-audit: npm-audit:
parameters: parameters:
@ -81,7 +112,15 @@ jobs:
mkdir -p dist/reports/tests/ mkdir -p dist/reports/tests/
TESTFILES=$(circleci tests glob "src/**/*Spec.js") TESTFILES=$(circleci tests glob "src/**/*Spec.js")
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
- run: npm run cov:unit:publish - download_verify_codecov_cli
- run:
name: Upload coverage report to Codecov
command: |
./codecov --verbose upload-process --disable-search \
-t $CODECOV_TOKEN \
-n 'unit-test'-${CIRCLE_WORKFLOW_ID} \
-F unit \
-f ./coverage/unit/lcov.info
- store_test_results: - store_test_results:
path: dist/reports/tests/ path: dist/reports/tests/
- store_artifacts: - store_artifacts:
@ -96,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:
@ -159,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
@ -284,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: |
@ -51,11 +51,18 @@ jobs:
env: env:
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }} COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
run: npm run test:e2e:couchdb run: npm run test:e2e:couchdb
- name: Generate Code Coverage Report
run: npm run cov:e2e:report
- name: Publish Results to Codecov.io - name: Publish Results to Codecov.io
env: uses: codecov/codecov-action@v4
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }} with:
run: npm run cov:e2e:full:publish token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/e2e/lcov.info
flags: e2e-full
fail_ci_if_error: true
verbose: true
- name: Archive test results - name: Archive test results
if: success() || failure() if: success() || failure()

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

@ -27,6 +27,12 @@ module.exports = {
rules: { rules: {
'playwright/no-raw-locators': 'off' 'playwright/no-raw-locators': 'off'
} }
},
{
files: ['**/*.visual.spec.js'],
rules: {
'playwright/no-networkidle': 'off' //https://github.com/nasa/openmct/issues/7549
}
} }
] ]
}; };

View File

@ -469,6 +469,7 @@ By adhering to this principle, we can create tests that are both robust and refl
//Select from object //Select from object
await percySnapshot(page, `object selected (theme: ${theme})`) await percySnapshot(page, `object selected (theme: ${theme})`)
``` ```
8. **Use `networkidle` to wait for network requests to complete**: This is necessary to ensure that all network requests have completed before taking a snapshot. This ensures that icons are loaded and other assets are available. https://github.com/nasa/openmct/issues/7549
#### How to write a great network test #### How to write a great network test

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

@ -25,6 +25,7 @@ import { expect } from '../pluginFixtures.js';
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @returns {Promise<void>}
*/ */
export async function navigateToFaultManagementWithExample(page) { export async function navigateToFaultManagementWithExample(page) {
await page.addInitScript({ await page.addInitScript({
@ -36,6 +37,7 @@ export async function navigateToFaultManagementWithExample(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @returns {Promise<void>}
*/ */
export async function navigateToFaultManagementWithStaticExample(page) { export async function navigateToFaultManagementWithStaticExample(page) {
await page.addInitScript({ await page.addInitScript({
@ -47,6 +49,7 @@ export async function navigateToFaultManagementWithStaticExample(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @returns {Promise<void>}
*/ */
export async function navigateToFaultManagementWithoutExample(page) { export async function navigateToFaultManagementWithoutExample(page) {
await page.addInitScript({ await page.addInitScript({
@ -58,6 +61,7 @@ export async function navigateToFaultManagementWithoutExample(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @returns {Promise<void>}
*/ */
async function navigateToFaultItemInTree(page) { async function navigateToFaultItemInTree(page) {
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
@ -77,6 +81,8 @@ async function navigateToFaultItemInTree(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} rowNumber
* @returns {Promise<void>}
*/ */
export async function acknowledgeFault(page, rowNumber) { export async function acknowledgeFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber); await openFaultRowMenu(page, rowNumber);
@ -86,6 +92,8 @@ export async function acknowledgeFault(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {...number} nums
* @returns {Promise<void>}
*/ */
export async function shelveMultipleFaults(page, ...nums) { export async function shelveMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => { const selectRows = nums.map((num) => {
@ -99,6 +107,8 @@ export async function shelveMultipleFaults(page, ...nums) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {...number} nums
* @returns {Promise<void>}
*/ */
export async function acknowledgeMultipleFaults(page, ...nums) { export async function acknowledgeMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => { const selectRows = nums.map((num) => {
@ -106,50 +116,43 @@ export async function acknowledgeMultipleFaults(page, ...nums) {
}); });
await Promise.all(selectRows); await Promise.all(selectRows);
await page.locator('button:has-text("Acknowledge")').click(); await page.getByLabel('Acknowledge selected faults').click();
await page.getByLabel('Save').click(); await page.getByLabel('Save').click();
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} rowNumber
* @returns {Promise<void>}
*/ */
export async function shelveFault(page, rowNumber) { export async function shelveFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber); await openFaultRowMenu(page, rowNumber);
await page.locator('.c-menu >> text="Shelve"').click(); await page.getByLabel('Shelve', { exact: true }).click();
// Click [aria-label="Save"]
await page.getByLabel('Save').click(); await page.getByLabel('Save').click();
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ * @param {'severity' | 'newest-first' | 'oldest-first'} sort
export async function changeViewTo(page, view) { * @returns {Promise<void>}
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
}
/**
* @param {import('@playwright/test').Page} page
*/ */
export async function sortFaultsBy(page, sort) { export async function sortFaultsBy(page, sort) {
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort); await page.getByTitle('Sort By').getByRole('combobox').selectOption(sort);
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {'acknowledged' | 'shelved' | 'standard view'} view
* @returns {Promise<void>}
*/ */
export async function enterSearchTerm(page, term) { export async function changeViewTo(page, view) {
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term); await page.getByTitle('View Filter').getByRole('combobox').selectOption(view);
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function clearSearch(page) {
await enterSearchTerm(page, '');
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} rowNumber
* @returns {Promise<void>}
*/ */
export async function selectFaultItem(page, rowNumber) { export async function selectFaultItem(page, rowNumber) {
await page await page
@ -165,71 +168,37 @@ export async function selectFaultItem(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ * @param {number} rowNumber
export async function getHighestSeverity(page) { * @returns {import('@playwright/test').Locator}
const criticalCount = await page.locator('[title=CRITICAL]').count();
const warningCount = await page.locator('[title=WARNING]').count();
if (criticalCount > 0) {
return 'CRITICAL';
} else if (warningCount > 0) {
return 'WARNING';
}
return 'WATCH';
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function getLowestSeverity(page) {
const warningCount = await page.locator('[title=WARNING]').count();
const watchCount = await page.locator('[title=WATCH]').count();
if (watchCount > 0) {
return 'WATCH';
} else if (warningCount > 0) {
return 'WARNING';
}
return 'CRITICAL';
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function getFaultResultCount(page) {
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
return count;
}
/**
* @param {import('@playwright/test').Page} page
*/ */
export function getFault(page, rowNumber) { export function getFault(page, rowNumber) {
const fault = page.locator( const fault = page.getByLabel('Fault triggered at').nth(rowNumber - 1);
`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`
);
return fault; return fault;
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {string} name
* @returns {import('@playwright/test').Locator}
*/ */
export function getFaultByName(page, name) { export function getFaultByName(page, name) {
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`); const fault = page.getByLabel('Fault triggered at').filter({
hasText: name
});
return fault; return fault;
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} rowNumber
* @returns {Promise<string>}
*/ */
export async function getFaultName(page, rowNumber) { export async function getFaultName(page, rowNumber) {
const faultName = await page const faultName = await page
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`) .getByLabel('Fault name', { exact: true })
.nth(rowNumber - 1)
.textContent(); .textContent();
return faultName; return faultName;
@ -237,21 +206,13 @@ export async function getFaultName(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ * @param {number} rowNumber
export async function getFaultSeverity(page, rowNumber) { * @returns {Promise<string>}
const faultSeverity = await page
.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`)
.getAttribute('title');
return faultSeverity;
}
/**
* @param {import('@playwright/test').Page} page
*/ */
export async function getFaultNamespace(page, rowNumber) { export async function getFaultNamespace(page, rowNumber) {
const faultNamespace = await page const faultNamespace = await page
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`) .getByLabel('Fault namespace')
.nth(rowNumber - 1)
.textContent(); .textContent();
return faultNamespace; return faultNamespace;
@ -259,10 +220,13 @@ export async function getFaultNamespace(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} rowNumber
* @returns {Promise<string>}
*/ */
export async function getFaultTriggerTime(page, rowNumber) { export async function getFaultTriggerTime(page, rowNumber) {
const faultTriggerTime = await page const faultTriggerTime = await page
.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`) .getByLabel('Last Trigger Time')
.nth(rowNumber - 1)
.textContent(); .textContent();
return faultTriggerTime.toString().trim(); return faultTriggerTime.toString().trim();
@ -270,11 +234,14 @@ export async function getFaultTriggerTime(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} rowNumber
* @returns {Promise<void>}
*/ */
export async function openFaultRowMenu(page, rowNumber) { export async function openFaultRowMenu(page, rowNumber) {
// select // select
await page await page
.getByLabel('Disposition actions') .getByLabel('Fault triggered at')
.nth(rowNumber - 1) .nth(rowNumber - 1)
.getByLabel('Disposition Actions')
.click(); .click();
} }

View File

@ -0,0 +1,33 @@
import { createDomainObjectWithDefaults } from '../appActions.js';
import { expect } from '../pluginFixtures.js';
const IMAGE_LOAD_DELAY = 5 * 1000;
const FIVE_MINUTES = 1000 * 60 * 5;
const THIRTY_SECONDS = 1000 * 30;
const MOUSE_WHEEL_DELTA_Y = 120;
/**
* @param {import('@playwright/test').Page} page
*/
async function createImageryViewWithShortDelay(page, { name, parent }) {
await createDomainObjectWithDefaults(page, {
name,
type: 'Example Imagery',
parent
});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.getByLabel('More actions').click();
await page.getByLabel('Edit Properties').click();
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
await page.getByLabel('Save').click();
}
export {
createImageryViewWithShortDelay,
FIVE_MINUTES,
IMAGE_LOAD_DELAY,
MOUSE_WHEEL_DELTA_Y,
THIRTY_SECONDS
};

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": {
@ -24,4 +24,4 @@
"url": "https://www.nasa.gov" "url": "https://www.nasa.gov"
}, },
"license": "Apache-2.0" "license": "Apache-2.0"
} }

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

@ -24,19 +24,13 @@ import {
acknowledgeFault, acknowledgeFault,
acknowledgeMultipleFaults, acknowledgeMultipleFaults,
changeViewTo, changeViewTo,
clearSearch,
enterSearchTerm,
getFault, getFault,
getFaultByName, getFaultByName,
getFaultName, getFaultName,
getFaultNamespace, getFaultNamespace,
getFaultResultCount,
getFaultSeverity,
getFaultTriggerTime, getFaultTriggerTime,
getHighestSeverity,
getLowestSeverity,
navigateToFaultManagementWithExample,
navigateToFaultManagementWithoutExample, navigateToFaultManagementWithoutExample,
navigateToFaultManagementWithStaticExample,
selectFaultItem, selectFaultItem,
shelveFault, shelveFault,
shelveMultipleFaults, shelveMultipleFaults,
@ -46,7 +40,7 @@ import { expect, test } from '../../../../pluginFixtures.js';
test.describe('The Fault Management Plugin using example faults', () => { test.describe('The Fault Management Plugin using example faults', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await navigateToFaultManagementWithExample(page); await navigateToFaultManagementWithStaticExample(page);
}); });
test('Shows a criticality icon for every fault', async ({ page }) => { test('Shows a criticality icon for every fault', async ({ page }) => {
@ -56,7 +50,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
expect(faultCount).toEqual(criticalityIconCount); expect(faultCount).toEqual(criticalityIconCount);
}); });
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({ test('When selecting a fault, it has an "is-selected" class and its information shows in the inspector', async ({
page page
}) => { }) => {
await selectFaultItem(page, 1); await selectFaultItem(page, 1);
@ -67,9 +61,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
.getByLabel('Source inspector properties') .getByLabel('Source inspector properties')
.getByLabel('inspector property value'); .getByLabel('inspector property value');
await expect( await expect(page.getByLabel('Fault triggered at').first()).toHaveClass(/is-selected/);
page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()
).toHaveClass(/is-selected/);
await expect(inspectorFaultName).toHaveCount(1); await expect(inspectorFaultName).toHaveCount(1);
}); });
@ -79,23 +71,18 @@ test.describe('The Fault Management Plugin using example faults', () => {
await selectFaultItem(page, 1); await selectFaultItem(page, 1);
await selectFaultItem(page, 2); await selectFaultItem(page, 2);
const selectedRows = page.locator( const selectedRows = page.getByRole('checkbox', { checked: true });
'.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname' await expect(selectedRows).toHaveCount(2);
);
expect(await selectedRows.count()).toEqual(2);
await page.getByRole('tab', { name: 'Config' }).click(); await page.getByRole('tab', { name: 'Config' }).click();
const firstSelectedFaultName = await selectedRows.nth(0).textContent(); const firstSelectedFaultName = await selectedRows.nth(0).textContent();
const secondSelectedFaultName = await selectedRows.nth(1).textContent(); const secondSelectedFaultName = await selectedRows.nth(1).textContent();
const firstNameInInspectorCount = await page await expect(
.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`) page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`)
.count(); ).toHaveCount(0);
const secondNameInInspectorCount = await page await expect(
.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`) page.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
.count(); ).toHaveCount(0);
expect(firstNameInInspectorCount).toEqual(0);
expect(secondNameInInspectorCount).toEqual(0);
}); });
test('Allows you to shelve a fault', async ({ page }) => { test('Allows you to shelve a fault', async ({ page }) => {
@ -186,44 +173,60 @@ test.describe('The Fault Management Plugin using example faults', () => {
const faultFiveTriggerTime = await getFaultTriggerTime(page, 5); const faultFiveTriggerTime = await getFaultTriggerTime(page, 5);
// should be all faults (5) // should be all faults (5)
let faultResultCount = await getFaultResultCount(page); await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
expect(faultResultCount).toEqual(5);
// search namespace // search namespace
await enterSearchTerm(page, faultThreeNamespace); await page
.getByLabel('Fault Management Object View')
.getByLabel('Search Input')
.fill(faultThreeNamespace);
faultResultCount = await getFaultResultCount(page); await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
expect(faultResultCount).toEqual(1);
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace); expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
// all faults // all faults
await clearSearch(page); await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('');
faultResultCount = await getFaultResultCount(page); await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
expect(faultResultCount).toEqual(5);
// search name // search name
await enterSearchTerm(page, faultTwoName); await page
.getByLabel('Fault Management Object View')
.getByLabel('Search Input')
.fill(faultTwoName);
faultResultCount = await getFaultResultCount(page); await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
expect(faultResultCount).toEqual(1);
expect(await getFaultName(page, 1)).toEqual(faultTwoName); expect(await getFaultName(page, 1)).toEqual(faultTwoName);
// all faults // all faults
await clearSearch(page); await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('');
faultResultCount = await getFaultResultCount(page); await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
expect(faultResultCount).toEqual(5);
// search triggerTime // search triggerTime
await enterSearchTerm(page, faultFiveTriggerTime); await page
.getByLabel('Fault Management Object View')
.getByLabel('Search Input')
.fill(faultFiveTriggerTime);
faultResultCount = await getFaultResultCount(page); await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
expect(faultResultCount).toEqual(1);
expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime); expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
}); });
test('Allows you to sort faults', async ({ page }) => { test('Allows you to sort faults', async ({ page }) => {
const highestSeverity = await getHighestSeverity(page); /**
const lowestSeverity = await getLowestSeverity(page); * Compares two severity levels and returns a number indicating their relative order.
*
* @param {'CRITICAL' | 'WARNING' | 'WATCH'} severity1 - The first severity level to compare.
* @param {'CRITICAL' | 'WARNING' | 'WATCH'} severity2 - The second severity level to compare.
* @returns {number} - A negative number if severity1 is less severe than severity2,
* a positive number if severity1 is more severe than severity2,
* or 0 if they are equally severe.
*/
// eslint-disable-next-line func-style
const compareSeverity = (severity1, severity2) => {
const severityOrder = ['WATCH', 'WARNING', 'CRITICAL'];
return severityOrder.indexOf(severity1) - severityOrder.indexOf(severity2);
};
const faultOneName = 'Example Fault 1'; const faultOneName = 'Example Fault 1';
const faultFiveName = 'Example Fault 5'; const faultFiveName = 'Example Fault 5';
let firstFaultName = await getFaultName(page, 1); let firstFaultName = await getFaultName(page, 1);
@ -237,10 +240,19 @@ test.describe('The Fault Management Plugin using example faults', () => {
await sortFaultsBy(page, 'severity'); await sortFaultsBy(page, 'severity');
const sortedHighestSeverity = await getFaultSeverity(page, 1); const firstFaultSeverityLabel = await page
const sortedLowestSeverity = await getFaultSeverity(page, 5); .getByLabel('Severity:')
expect(sortedHighestSeverity).toEqual(highestSeverity); .first()
expect(sortedLowestSeverity).toEqual(lowestSeverity); .getAttribute('aria-label');
const firstFaultSeverity = firstFaultSeverityLabel.split(' ').slice(1).join(' ');
const lastFaultSeverityLabel = await page
.getByLabel('Severity:')
.last()
.getAttribute('aria-label');
const lastFaultSeverity = lastFaultSeverityLabel.split(' ').slice(1).join(' ');
expect(compareSeverity(firstFaultSeverity, lastFaultSeverity)).toBeGreaterThan(0);
}); });
}); });
@ -250,24 +262,18 @@ test.describe('The Fault Management Plugin without using example faults', () =>
}); });
test('Shows no faults when no faults are provided', async ({ page }) => { test('Shows no faults when no faults are provided', async ({ page }) => {
const faultCount = await page.locator('c-fault-mgmt__list').count(); await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
expect(faultCount).toEqual(0);
await changeViewTo(page, 'acknowledged'); await changeViewTo(page, 'acknowledged');
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count(); await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
expect(acknowledgedCount).toEqual(0);
await changeViewTo(page, 'shelved'); await changeViewTo(page, 'shelved');
const shelvedCount = await page.locator('c-fault-mgmt__list').count(); await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
expect(shelvedCount).toEqual(0);
}); });
test('Will return no faults when searching', async ({ page }) => { test('Will return no faults when searching', async ({ page }) => {
await enterSearchTerm(page, 'fault'); await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('fault');
const faultCount = await page.locator('c-fault-mgmt__list').count(); await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
expect(faultCount).toEqual(0);
}); });
}); });

View File

@ -30,16 +30,19 @@ import {
navigateToObjectWithRealTime, navigateToObjectWithRealTime,
setRealTimeMode setRealTimeMode
} from '../../../../appActions.js'; } from '../../../../appActions.js';
import { MISSION_TIME } from '../../../../constants.js'; import {
createImageryViewWithShortDelay,
FIVE_MINUTES,
IMAGE_LOAD_DELAY,
MOUSE_WHEEL_DELTA_Y,
THIRTY_SECONDS
} from '../../../../helper/imageryUtils.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt']; const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
const tagHotkey = ['Shift', 'Alt']; const tagHotkey = ['Shift', 'Alt'];
const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan'; const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';
const thumbnailUrlParamsRegexp = /\?w=100&h=100/; const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
const IMAGE_LOAD_DELAY = 5 * 1000;
const MOUSE_WHEEL_DELTA_Y = 120;
const FIVE_MINUTES = 1000 * 60 * 5;
const THIRTY_SECONDS = 1000 * 30;
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects. //The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
test.describe('Example Imagery Object', () => { test.describe('Example Imagery Object', () => {
@ -357,15 +360,10 @@ test.describe('Example Imagery Object', () => {
}); });
}); });
test.describe('Example Imagery in Display Layout @clock', () => { test.describe('Example Imagery in Display Layout', () => {
let displayLayout; let displayLayout;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// We mock the clock so that we don't need to wait for time driven events
// to verify functionality.
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
// Go to baseURL // Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
@ -428,12 +426,7 @@ test.describe('Example Imagery in Display Layout @clock', () => {
await expect.soft(pausePlayButton).toHaveClass(/is-paused/); await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
}); });
test('Imagery View operations @clock', async ({ page }) => { test('Imagery View operations', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265'
});
// Edit mode // Edit mode
await page.getByLabel('Edit Object').click(); await page.getByLabel('Edit Object').click();
@ -526,14 +519,9 @@ test.describe('Example Imagery in Display Layout @clock', () => {
}); });
}); });
test.describe('Example Imagery in Flexible layout @clock', () => { test.describe('Example Imagery in Flexible layout', () => {
let flexibleLayout; let flexibleLayout;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// We mock the clock so that we don't need to wait for time driven events
// to verify functionality.
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' }); flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
@ -562,7 +550,7 @@ test.describe('Example Imagery in Flexible layout @clock', () => {
await page.getByRole('button', { name: 'Close' }).click(); await page.getByRole('button', { name: 'Close' }).click();
}); });
test('Imagery View operations @clock', async ({ page, browserName }) => { test('Imagery View operations', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
@ -573,14 +561,10 @@ test.describe('Example Imagery in Flexible layout @clock', () => {
}); });
}); });
test.describe('Example Imagery in Tabs View @clock', () => { test.describe('Example Imagery in Tabs View', () => {
let tabsView; let tabsView;
test.beforeEach(async ({ page }) => {
// We mock the clock so that we don't need to wait for time driven events
// to verify functionality.
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' }); tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
@ -607,7 +591,8 @@ test.describe('Example Imagery in Tabs View @clock', () => {
// Wait for image thumbnail auto-scroll to complete // Wait for image thumbnail auto-scroll to complete
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
}); });
test('Imagery View operations @clock', async ({ page }) => {
test('Imagery View operations', async ({ page }) => {
await performImageryViewOperationsAndAssert(page, tabsView); await performImageryViewOperationsAndAssert(page, tabsView);
}); });
}); });
@ -668,16 +653,19 @@ test.describe('Example Imagery in Time Strip', () => {
* 3. Can pan the image using the pan hotkey + mouse drag * 3. Can pan the image using the pan hotkey + mouse drag
* 4. Clicking on the left arrow button pauses imagery and moves to the previous image * 4. Clicking on the left arrow button pauses imagery and moves to the previous image
* 5. Imagery is updated as new images stream in, regardless of pause status * 5. Imagery is updated as new images stream in, regardless of pause status
* 6. Old images are discarded when new images stream in * 6. Old images are discarded when their timestamps fall out of bounds
* 7. Image brightness/contrast can be adjusted by dragging the sliders * 7. Multiple images can be discarded when their timestamps fall out of bounds
* 8. Image brightness/contrast can be adjusted by dragging the sliders
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function performImageryViewOperationsAndAssert(page, layoutObject) { async function performImageryViewOperationsAndAssert(page, layoutObject) {
// Verify that imagery thumbnails use a thumbnail url await test.step('Verify that imagery thumbnails use a thumbnail url', async () => {
const thumbnailImages = page.getByLabel('Image thumbnail from').locator('.c-thumb__image'); const thumbnailImages = page.getByLabel('Image thumbnail from').locator('.c-thumb__image');
const mainImage = page.locator('.c-imagery__main-image__image'); const mainImage = page.locator('.c-imagery__main-image__image');
await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp); await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp); await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
});
// Click previous image button // Click previous image button
const previousImageButton = page.getByLabel('Previous image'); const previousImageButton = page.getByLabel('Previous image');
await expect(previousImageButton).toBeVisible(); await expect(previousImageButton).toBeVisible();
@ -736,19 +724,6 @@ async function performImageryViewOperationsAndAssert(page, layoutObject) {
// Unpause imagery // Unpause imagery
await page.locator('.pause-play').click(); await page.locator('.pause-play').click();
// verify that old images are discarded
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
expect(lastImageTimestamp).not.toBeNull();
// go forward in time to ensure old images are discarded
await page.clock.fastForward(IMAGE_LOAD_DELAY);
await page.clock.resume();
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
//Get background-image url from background-image css prop
await assertBackgroundImageUrlFromBackgroundCss(page);
// Open the image filter menu // Open the image filter menu
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
@ -815,24 +790,6 @@ async function assertBackgroundImageBrightness(page, expected) {
expect(actual).toBe(expected); expect(actual).toBe(expected);
} }
/**
* @param {import('@playwright/test').Page} page
*/
async function assertBackgroundImageUrlFromBackgroundCss(page) {
const backgroundImage = page.getByLabel('Focused Image Element');
const backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window
.getComputedStyle(el)
.getPropertyValue('background-image')
.match(/url\(([^)]+)\)/)[1];
});
// go forward in time to ensure old images are discarded
await page.clock.fastForward(IMAGE_LOAD_DELAY);
await page.clock.resume();
await expect(backgroundImage).not.toHaveJSProperty('background-image', backgroundImageUrl);
}
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
@ -918,62 +875,66 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function buttonZoomOnImageAndAssert(page) { async function buttonZoomOnImageAndAssert(page) {
// Lock the zoom and pan so it doesn't reset if a new image comes in await test.step('Can zoom using buttons', async () => {
await page.getByLabel('Focused Image Element').hover({ trial: true }); // Lock the zoom and pan so it doesn't reset if a new image comes in
const lockButton = page.getByRole('button', {
name: 'Lock current zoom and pan across all images'
});
if (!(await lockButton.isVisible())) {
await page.getByLabel('Focused Image Element').hover({ trial: true }); await page.getByLabel('Focused Image Element').hover({ trial: true });
} const lockButton = page.getByRole('button', {
await lockButton.click(); name: 'Lock current zoom and pan across all images'
});
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( await lockButton.isVisible();
'style.transform', // if (!(await lockButton.isVisible())) {
'scale(1) translate(0px, 0px)' // await page.getByLabel('Focused Image Element').hover({ trial: true });
); // }
await lockButton.click();
// Get initial image dimensions await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); 'style.transform',
'scale(1) translate(0px, 0px)'
);
// Zoom in twice via button // Get initial image dimensions
await zoomIntoImageryByButton(page); const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(2) translate(0px, 0px)'
);
await zoomIntoImageryByButton(page);
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(3) translate(0px, 0px)'
);
// Get and assert zoomed in image dimensions // Zoom in twice via button
const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); await zoomIntoImageryByButton(page);
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); 'style.transform',
'scale(2) translate(0px, 0px)'
);
await zoomIntoImageryByButton(page);
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(3) translate(0px, 0px)'
);
// Zoom out once via button // Get and assert zoomed in image dimensions
await zoomOutOfImageryByButton(page); const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
'style.transform', expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
'scale(2) translate(0px, 0px)'
);
// Get and assert zoomed out image dimensions // Zoom out once via button
const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); await zoomOutOfImageryByButton(page);
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); 'style.transform',
'scale(2) translate(0px, 0px)'
);
// Zoom out again via button, assert against the initial image dimensions // Get and assert zoomed out image dimensions
await zoomOutOfImageryByButton(page); const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
'style.transform', expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
'scale(1) translate(0px, 0px)'
);
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); // Zoom out again via button, assert against the initial image dimensions
expect(finalBoundingBox).toEqual(initialBoundingBox); await zoomOutOfImageryByButton(page);
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(1) translate(0px, 0px)'
);
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(finalBoundingBox).toEqual(initialBoundingBox);
});
} }
/** /**
@ -1035,24 +996,6 @@ async function resetImageryPanAndZoom(page) {
await expect(page.locator('.c-thumb__viewable-area')).toBeHidden(); await expect(page.locator('.c-thumb__viewable-area')).toBeHidden();
} }
/**
* @param {import('@playwright/test').Page} page
*/
async function createImageryViewWithShortDelay(page, { name, parent }) {
await createDomainObjectWithDefaults(page, {
name,
type: 'Example Imagery',
parent
});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.getByLabel('More actions').click();
await page.getByLabel('Edit Properties').click();
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
await page.getByLabel('Save').click();
}
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */

View File

@ -0,0 +1,489 @@
/*****************************************************************************
* 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 testing how imagery functions over time.
It only assumes that example imagery is present.
It uses https://playwright.dev/docs/clock to have control over time
*/
import {
createDomainObjectWithDefaults,
navigateToObjectWithRealTime,
setRealTimeMode,
setStartOffset
} from '../../../../appActions.js';
import { MISSION_TIME } from '../../../../constants.js';
import {
createImageryViewWithShortDelay,
FIVE_MINUTES,
IMAGE_LOAD_DELAY,
THIRTY_SECONDS
} from '../../../../helper/imageryUtils.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Example Imagery Object with Controlled Clock @clock', () => {
test.beforeEach(async ({ page }) => {
// We mock the clock so that we don't need to wait for time driven events
// to verify functionality.
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
//Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a default 'Example Imagery' object
// Click the Create button
await page.getByRole('button', { name: 'Create' }).click();
// Click text=Example Imagery
await page.getByRole('menuitem', { name: 'Example Imagery' }).click();
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').clear();
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
await page.getByLabel('Save').click();
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
'Unnamed Example Imagery'
);
await page.getByLabel('Focused Image Element').hover({ trial: true });
// set realtime mode
await setRealTimeMode(page);
await setStartOffset(page, { startMins: '05' });
});
test('Imagery Time Bounding', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265'
});
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7825'
});
// verify that old images are discarded
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
expect(lastImageTimestamp).not.toBeNull();
// go forward in time to ensure old images are discarded
await page.clock.fastForward(IMAGE_LOAD_DELAY);
await page.clock.resume();
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
// go way forward in time to ensure multiple images are discarded
const IMAGES_TO_DISCARD_COUNT = 5;
const lastImageToDiscard = page
.getByLabel('Image thumbnail from')
.nth(IMAGES_TO_DISCARD_COUNT - 1);
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
expect(lastImageToDiscardTimestamp).not.toBeNull();
const imageAfterLastImageToDiscard = page
.getByLabel('Image thumbnail from')
.nth(IMAGES_TO_DISCARD_COUNT);
const imageAfterLastImageToDiscardTimestamp =
await imageAfterLastImageToDiscard.getAttribute('title');
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
await page.clock.resume();
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
});
test('Get background-image url from background-image css prop', async ({ page }) => {
await assertBackgroundImageUrlFromBackgroundCss(page);
});
});
test.describe('Example Imagery in Display Layout with Controlled Clock @clock', () => {
let displayLayout;
test.beforeEach(async ({ page }) => {
// We mock the clock so that we don't need to wait for time driven events
// to verify functionality.
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
// Create Example Imagery inside Display Layout
await createImageryViewWithShortDelay(page, {
name: 'Unnamed Example Imagery',
parent: displayLayout.uuid
});
// set realtime mode
await navigateToObjectWithRealTime(
page,
displayLayout.url,
`${FIVE_MINUTES}`,
`${THIRTY_SECONDS}`
);
});
test('Imagery Time Bounding', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265'
});
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7825'
});
// Edit mode
await page.getByLabel('Edit Object').click();
// Click on example imagery to expose toolbar
await page.locator('.c-so-view__header').click();
// Adjust object height
await page.locator('div[title="Resize object height"] > input').click();
await page.locator('div[title="Resize object height"] > input').fill('50');
// Adjust object width
await page.locator('div[title="Resize object width"] > input').click();
await page.locator('div[title="Resize object width"] > input').fill('50');
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
// verify that old images are discarded
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
expect(lastImageTimestamp).not.toBeNull();
// go forward in time to ensure old images are discarded
await page.clock.fastForward(IMAGE_LOAD_DELAY);
await page.clock.resume();
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
// go way forward in time to ensure multiple images are discarded
const IMAGES_TO_DISCARD_COUNT = 5;
const lastImageToDiscard = page
.getByLabel('Image thumbnail from')
.nth(IMAGES_TO_DISCARD_COUNT - 1);
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
expect(lastImageToDiscardTimestamp).not.toBeNull();
const imageAfterLastImageToDiscard = page
.getByLabel('Image thumbnail from')
.nth(IMAGES_TO_DISCARD_COUNT);
const imageAfterLastImageToDiscardTimestamp =
await imageAfterLastImageToDiscard.getAttribute('title');
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
await page.clock.resume();
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
});
test('Get background-image url from background-image css prop @clock', async ({ page }) => {
await assertBackgroundImageUrlFromBackgroundCss(page);
});
});
test.describe('Example Imagery in Flexible layout with Controlled Clock @clock', () => {
let flexibleLayout;
test.beforeEach(async ({ page }) => {
// We mock the clock so that we don't need to wait for time driven events
// to verify functionality.
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
await page.goto('./', { waitUntil: 'domcontentloaded' });
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
// Create Example Imagery inside the Flexible Layout
await createImageryViewWithShortDelay(page, {
name: 'Unnamed Example Imagery',
parent: flexibleLayout.uuid
});
// set realtime mode
await navigateToObjectWithRealTime(
page,
flexibleLayout.url,
`${FIVE_MINUTES}`,
`${THIRTY_SECONDS}`
);
// Wait for image thumbnail auto-scroll to complete
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
});
test('Imagery Time Bounding @clock', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5326'
});
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7825'
});
// verify that old images are discarded
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
expect(lastImageTimestamp).not.toBeNull();
// go forward in time to ensure old images are discarded
await page.clock.fastForward(IMAGE_LOAD_DELAY);
await page.clock.resume();
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
// go way forward in time to ensure multiple images are discarded
const IMAGES_TO_DISCARD_COUNT = 5;
const lastImageToDiscard = page
.getByLabel('Image thumbnail from')
.nth(IMAGES_TO_DISCARD_COUNT - 1);
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
expect(lastImageToDiscardTimestamp).not.toBeNull();
const imageAfterLastImageToDiscard = page
.getByLabel('Image thumbnail from')
.nth(IMAGES_TO_DISCARD_COUNT);
const imageAfterLastImageToDiscardTimestamp =
await imageAfterLastImageToDiscard.getAttribute('title');
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
await page.clock.resume();
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
});
test('Get background-image url from background-image css prop @clock', async ({ page }) => {
await assertBackgroundImageUrlFromBackgroundCss(page);
});
});
test.describe('Example Imagery in Tabs View with Controlled Clock @clock', () => {
let timeStripObject;
test.beforeEach(async ({ page }) => {
// We mock the clock so that we don't need to wait for time driven events
// to verify functionality.
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
await page.goto('./', { waitUntil: 'domcontentloaded' });
timeStripObject = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
await page.goto(timeStripObject.url);
/* Create Example Imagery with minimum Image Load Delay */
// Click the Create button
await page.getByRole('button', { name: 'Create' }).click();
// Click text=Example Imagery
await page.getByRole('menuitem', { name: 'Example Imagery' }).click();
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').clear();
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
await page.getByLabel('Save').click();
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
'Unnamed Example Imagery'
);
// set realtime mode
await navigateToObjectWithRealTime(
page,
timeStripObject.url,
`${FIVE_MINUTES}`,
`${THIRTY_SECONDS}`
);
// Wait for image thumbnail auto-scroll to complete
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
});
test('Imagery Time Bounding @clock', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265'
});
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7825'
});
// verify that old images are discarded
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
expect(lastImageTimestamp).not.toBeNull();
// go forward in time to ensure old images are discarded
await page.clock.fastForward(IMAGE_LOAD_DELAY);
await page.clock.resume();
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
// go way forward in time to ensure multiple images are discarded
const IMAGES_TO_DISCARD_COUNT = 5;
const lastImageToDiscard = page
.getByLabel('Image thumbnail from')
.nth(IMAGES_TO_DISCARD_COUNT - 1);
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
expect(lastImageToDiscardTimestamp).not.toBeNull();
const imageAfterLastImageToDiscard = page
.getByLabel('Image thumbnail from')
.nth(IMAGES_TO_DISCARD_COUNT);
const imageAfterLastImageToDiscardTimestamp =
await imageAfterLastImageToDiscard.getAttribute('title');
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
await page.clock.resume();
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
});
test('Get background-image url from background-image css prop @clock', async ({ page }) => {
await assertBackgroundImageUrlFromBackgroundCss(page);
});
});
test.describe('Example Imagery in Time Strip with Controlled Clock @clock', () => {
let timeStripObject;
test.beforeEach(async ({ page }) => {
// We mock the clock so that we don't need to wait for time driven events
// to verify functionality.
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
await page.goto('./', { waitUntil: 'domcontentloaded' });
timeStripObject = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
await page.goto(timeStripObject.url);
/* Create Example Imagery with minimum Image Load Delay */
// Click the Create button
await page.getByRole('button', { name: 'Create' }).click();
// Click text=Example Imagery
await page.getByRole('menuitem', { name: 'Example Imagery' }).click();
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').clear();
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
await page.getByLabel('Save').click();
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
'Unnamed Example Imagery'
);
// set realtime mode
await navigateToObjectWithRealTime(
page,
timeStripObject.url,
`${FIVE_MINUTES}`,
`${THIRTY_SECONDS}`
);
// Wait for image thumbnail auto-scroll to complete
await expect(page.getByLabel('wrapper-').last()).toBeInViewport();
});
test('Imagery Time Bounding @clock', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265'
});
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7825'
});
// verify that old images are discarded
const lastImageInBounds = page.getByLabel('wrapper-').first();
const lastImageTimestamp = await lastImageInBounds.getAttribute('aria-label');
expect(lastImageTimestamp).not.toBeNull();
// go forward in time to ensure old images are discarded
await page.clock.fastForward(IMAGE_LOAD_DELAY);
await page.clock.resume();
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
// go way forward in time to ensure multiple images are discarded
const IMAGES_TO_DISCARD_COUNT = 5;
const lastImageToDiscard = page.getByLabel('wrapper-').nth(IMAGES_TO_DISCARD_COUNT - 1);
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('aria-label');
expect(lastImageToDiscardTimestamp).not.toBeNull();
const imageAfterLastImageToDiscard = page.getByLabel('wrapper-').nth(IMAGES_TO_DISCARD_COUNT);
const imageAfterLastImageToDiscardTimestamp =
await imageAfterLastImageToDiscard.getAttribute('aria-label');
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
await page.clock.resume();
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
});
});
/**
* @param {import('@playwright/test').Page} page
*/
async function assertBackgroundImageUrlFromBackgroundCss(page) {
const backgroundImage = page.getByLabel('Focused Image Element');
const backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window
.getComputedStyle(el)
.getPropertyValue('background-image')
.match(/url\(([^)]+)\)/)[1];
});
// go forward in time to ensure old images are discarded
await page.clock.fastForward(IMAGE_LOAD_DELAY);
await page.clock.resume();
await expect(backgroundImage).not.toHaveJSProperty('background-image', backgroundImageUrl);
}

View File

@ -0,0 +1,72 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('The performance indicator', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => {
const openmct = window.openmct;
openmct.install(openmct.plugins.PerformanceIndicator());
});
});
test('can be installed', ({ page }) => {
const performanceIndicator = page.getByTitle('Performance Indicator');
expect(performanceIndicator).toBeDefined();
});
test('Shows a numerical FPS value', async ({ page }) => {
// Frames Per Second. We need to wait at least 1 second to get a value.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
await expect(page.getByTitle('Performance Indicator')).toHaveText(/\d\d? fps/);
});
test('Supports showing optional extended performance information in an overlay for debugging', async ({
page
}) => {
const performanceMeasurementLabel = 'Some measurement';
const performanceMeasurementValue = 'Some value';
await page.evaluate(
({ performanceMeasurementLabel: label, performanceMeasurementValue: value }) => {
const openmct = window.openmct;
openmct.performance.measurements.set(label, value);
},
{ performanceMeasurementLabel, performanceMeasurementValue }
);
const performanceIndicator = page.getByTitle('Performance Indicator');
await performanceIndicator.click();
//Performance overlay is a crude debugging tool, it's evaluated once per second.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
const performanceOverlay = page.getByTitle('Performance Overlay');
await expect(performanceOverlay).toBeVisible();
await expect(performanceOverlay).toHaveText(new RegExp(`${performanceMeasurementLabel}.*`));
await expect(performanceOverlay).toHaveText(new RegExp(`.*${performanceMeasurementValue}`));
//Confirm that it disappears if we click on it again.
await performanceIndicator.click();
await expect(performanceOverlay).toBeHidden();
});
});

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

@ -85,16 +85,6 @@ test.describe('Visual - Default @a11y', () => {
await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`); await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);
}); });
test('Visual - Default Gauge', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Default Gauge'
});
// Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
});
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title); await scanForA11yViolations(page, testInfo.title);
}); });

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

@ -0,0 +1,81 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults } from '../../appActions.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js';
test.describe('Visual - Gauges', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
test('Visual - Default Gauge', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Default Gauge'
});
// Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
});
test('Visual - Needle Gauge with State Generator', async ({ page, theme }) => {
const needleGauge = await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Needle Gauge'
});
//Modify the Gauge to be a Needle Gauge
await page.getByLabel('More actions').click();
await page.getByLabel('Edit Properties...').click();
await page.getByLabel('Gauge type', { exact: true }).selectOption('dial-needle');
await page.getByText('Ok').click();
//Add a State Generator to the Gauge
await page.goto(needleGauge.url + '?hideTree=true&hideInspector=true', {
waitUntil: 'domcontentloaded'
});
// Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Needle Gauge with no telemetry source (theme: '${theme}')`);
//Add a State Generator to the Gauge. Note this requires that snapshots are taken within 5 seconds
await page.getByLabel('Create', { exact: true }).click();
await page.getByLabel('State Generator').click();
await page.getByLabel('Modal Overlay').getByLabel('Navigate to Needle Gauge').click();
await page.getByLabel('Save').click();
//Add a State Generator to the Gauge
await page.goto(needleGauge.url + '?hideTree=true&hideInspector=true', {
waitUntil: 'domcontentloaded'
});
// Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Needle Gauge with State Generator (theme: '${theme}')`);
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@ -98,7 +98,7 @@ test.describe('Visual - Notebook @a11y', () => {
await page.getByLabel('Expand My Items folder').click(); await page.getByLabel('Expand My Items folder').click();
await page.goto(notebook.url); await page.goto(notebook.url, { waitUntil: 'networkidle' });
await page await page
.getByLabel('Navigate to Dropped Overlay Plot') .getByLabel('Navigate to Dropped Overlay Plot')

View File

@ -53,7 +53,7 @@ test.describe('Grand Search @a11y', () => {
theme theme
}) => { }) => {
// Navigate to display layout // Navigate to display layout
await page.goto(displayLayout.url); await page.goto(displayLayout.url, { waitUntil: 'networkidle' });
// Search for the object // Search for the object
await page.getByRole('searchbox', { name: 'Search Input' }).click(); await page.getByRole('searchbox', { name: 'Search Input' }).click();

View File

@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { DEFAULT_SHELVE_DURATIONS } from '../../src/api/faultmanagement/FaultManagementAPI.js';
import { acknowledgeFault, randomFaults, shelveFault } from './utils.js'; import { acknowledgeFault, randomFaults, shelveFault } from './utils.js';
export default function (staticFaults = false) { export default function (staticFaults = false) {
@ -56,6 +57,9 @@ export default function (staticFaults = false) {
return Promise.resolve({ return Promise.resolve({
success: true success: true
}); });
},
getShelveDurations() {
return DEFAULT_SHELVE_DURATIONS;
} }
}); });
}; };

View File

@ -1,4 +1,27 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const SEVERITIES = ['WATCH', 'WARNING', 'CRITICAL']; const SEVERITIES = ['WATCH', 'WARNING', 'CRITICAL'];
const MOONWALK_TIMESTAMP = 14159040000;
const NAMESPACE = '/Example/fault-'; const NAMESPACE = '/Example/fault-';
const getRandom = { const getRandom = {
severity: () => SEVERITIES[Math.floor(Math.random() * 3)], severity: () => SEVERITIES[Math.floor(Math.random() * 3)],
@ -13,7 +36,8 @@ const getRandom = {
val = num; val = num;
severity = SEVERITIES[severityIndex - 1]; severity = SEVERITIES[severityIndex - 1];
time = num; // Subtract `num` from the timestamp so that the faults are in order
time = MOONWALK_TIMESTAMP - num; // Mon, 21 Jul 1969 02:56:00 GMT 🌔👨‍🚀👨‍🚀👨‍🚀
} }
return { return {
@ -43,14 +67,7 @@ const getRandom = {
} }
}; };
export function shelveFault( export function shelveFault(fault, opts = { shelved: true, comment: '', shelveDuration: 90000 }) {
fault,
opts = {
shelved: true,
comment: '',
shelveDuration: 90000
}
) {
fault.shelved = true; fault.shelved = true;
setTimeout(() => { setTimeout(() => {
@ -65,8 +82,8 @@ export function acknowledgeFault(fault) {
export function randomFaults(staticFaults, count = 5) { export function randomFaults(staticFaults, count = 5) {
let faults = []; let faults = [];
for (let x = 1, y = count + 1; x < y; x++) { for (let i = 1; i <= count; i++) {
faults.push(getRandom.fault(x, staticFaults)); faults.push(getRandom.fault(i, staticFaults));
} }
return faults; return faults;

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

180
package-lock.json generated
View File

@ -24,7 +24,6 @@
"@vue/compiler-sfc": "3.4.3", "@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0", "babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1", "babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4", "comma-separated-values": "3.6.4",
"copy-webpack-plugin": "12.0.2", "copy-webpack-plugin": "12.0.2",
"cspell": "7.3.8", "cspell": "7.3.8",
@ -104,7 +103,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": {
@ -1548,12 +1547,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"
@ -1586,15 +1586,6 @@
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
"dev": true "dev": true
}, },
"node_modules/@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.5", "version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@ -2308,18 +2299,6 @@
"node": ">=8.9" "node": ">=8.9"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/aggregate-error": { "node_modules/aggregate-error": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@ -2454,16 +2433,6 @@
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
}, },
"node_modules/argv": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz",
"integrity": "sha512-dEamhpPEwRUBpLNHeuCm/v+g0anFByHahxodVO/BbAarHVBBg2MccCwf9K+o1Pof+2btdnkJelYVUWjW/VrATw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"dev": true,
"engines": {
"node": ">=0.6.10"
}
},
"node_modules/array-flatten": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -3013,26 +2982,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/codecov": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.3.tgz",
"integrity": "sha512-Y8Hw+V3HgR7V71xWH2vQ9lyS358CbGCldWlJFR0JirqoGtOoas3R3/OclRTvgUYFK29mmJICDPauVKmpqbwhOA==",
"deprecated": "https://about.codecov.io/blog/codecov-uploader-deprecation-plan/",
"dev": true,
"dependencies": {
"argv": "0.0.2",
"ignore-walk": "3.0.4",
"js-yaml": "3.14.1",
"teeny-request": "7.1.1",
"urlgrey": "1.0.0"
},
"bin": {
"codecov": "bin/codecov"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -5582,21 +5531,6 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true "dev": true
}, },
"node_modules/fast-url-parser": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz",
"integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==",
"dev": true,
"dependencies": {
"punycode": "^1.3.2"
}
},
"node_modules/fast-url-parser/node_modules/punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"dev": true
},
"node_modules/fastest-levenshtein": { "node_modules/fastest-levenshtein": {
"version": "1.0.16", "version": "1.0.16",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
@ -6387,20 +6321,6 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/http-proxy-agent": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
"integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
"dev": true,
"dependencies": {
"@tootallnate/once": "1",
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/http-proxy-middleware": { "node_modules/http-proxy-middleware": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
@ -6431,19 +6351,6 @@
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true "dev": true
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dev": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -6486,15 +6393,6 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/ignore-walk": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz",
"integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==",
"dev": true,
"dependencies": {
"minimatch": "^3.0.4"
}
},
"node_modules/image-size": { "node_modules/image-size": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
@ -8764,12 +8662,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"
@ -8782,10 +8681,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"
}, },
@ -8799,6 +8699,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"
@ -10346,15 +10247,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/stream-events": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
"dev": true,
"dependencies": {
"stubs": "^3.0.0"
}
},
"node_modules/streamroller": { "node_modules/streamroller": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
@ -10537,12 +10429,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/stubs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
"dev": true
},
"node_modules/style-loader": { "node_modules/style-loader": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz",
@ -10608,31 +10494,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/teeny-request": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.1.1.tgz",
"integrity": "sha512-iwY6rkW5DDGq8hE2YgNQlKbptYpY5Nn2xecjQiNjOXWbKzPGUfmeUBCSQbbr306d7Z7U2N0TPl+/SwYRfua1Dg==",
"dev": true,
"dependencies": {
"http-proxy-agent": "^4.0.0",
"https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.1",
"stream-events": "^1.0.5",
"uuid": "^8.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/teeny-request/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/terser": { "node_modules/terser": {
"version": "5.29.1", "version": "5.29.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
@ -11013,15 +10874,6 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/urlgrey": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-1.0.0.tgz",
"integrity": "sha512-hJfIzMPJmI9IlLkby8QrsCykQ+SXDeO2W5Q9QTW3QpqZVTx4a/K7p8/5q+/isD8vsbVaFgql/gvAoQCRQ2Cb5w==",
"dev": true,
"dependencies": {
"fast-url-parser": "^1.1.3"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -27,7 +27,6 @@
"@vue/compiler-sfc": "3.4.3", "@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0", "babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1", "babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4", "comma-separated-values": "3.6.4",
"copy-webpack-plugin": "12.0.2", "copy-webpack-plugin": "12.0.2",
"cspell": "7.3.8", "cspell": "7.3.8",
@ -128,12 +127,9 @@
"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",
"cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
"cov:e2e:ci:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-ci",
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
"prepare": "npm run build:prod && npx tsc" "prepare": "npm run build:prod && npx tsc"
}, },
"homepage": "https://nasa.github.io/openmct", "homepage": "https://nasa.github.io/openmct",
@ -160,4 +156,4 @@
"keywords": [ "keywords": [
"nasa" "nasa"
] ]
} }

View File

@ -20,6 +20,32 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/** @type {ShelveDuration[]} */
export const DEFAULT_SHELVE_DURATIONS = [
{
name: '5 Minutes',
value: 300000
},
{
name: '10 Minutes',
value: 600000
},
{
name: '15 Minutes',
value: 900000
},
{
name: 'Indefinite',
value: null
}
];
/**
* Provides an API for managing faults within Open MCT.
* It allows for the addition of a fault provider, checking for provider support, and
* performing various operations such as requesting, subscribing to, acknowledging,
* and shelving faults.
*/
export default class FaultManagementAPI { export default class FaultManagementAPI {
/** /**
* @param {import("openmct").OpenMCT} openmct * @param {import("openmct").OpenMCT} openmct
@ -29,14 +55,20 @@ export default class FaultManagementAPI {
} }
/** /**
* @param {*} provider * Sets the provider for the Fault Management API.
* The provider should implement methods for acknowledging and shelving faults.
*
* @param {*} provider - The provider to be set.
*/ */
addProvider(provider) { addProvider(provider) {
this.provider = provider; this.provider = provider;
} }
/** /**
* @returns {boolean} * Checks if the current provider supports fault management actions.
* Specifically, it checks if the provider has methods for acknowledging and shelving faults.
*
* @returns {boolean} - Returns true if the provider supports fault management actions, otherwise false.
*/ */
supportsActions() { supportsActions() {
return ( return (
@ -45,48 +77,78 @@ export default class FaultManagementAPI {
} }
/** /**
* @param {import('openmct').DomainObject} domainObject * Requests fault data for a given domain object.
* @returns {Promise.<FaultAPIResponse[]>} * This method checks if the current provider supports the request operation for the given domain object.
* If supported, it delegates the request to the provider's request method.
* If not supported, it returns a rejected promise.
*
* @param {import('openmct').DomainObject} domainObject - The domain object for which fault data is requested.
* @returns {Promise.<FaultAPIResponse[]>} - A promise that resolves to an array of fault API responses.
*/ */
request(domainObject) { request(domainObject) {
if (!this.provider?.supportsRequest(domainObject)) { if (!this.provider?.supportsRequest(domainObject)) {
return Promise.reject(); return Promise.reject('Provider does not support request operation');
} }
return this.provider.request(domainObject); return this.provider.request(domainObject);
} }
/** /**
* @param {import('openmct').DomainObject} domainObject * Subscribes to fault data updates for a given domain object.
* @param {Function} callback * This method checks if the current provider supports the subscribe operation for the given domain object.
* @returns {Function} unsubscribe * If supported, it delegates the subscription to the provider's subscribe method.
* If not supported, it returns a rejected promise.
*
* @param {import('openmct').DomainObject} domainObject - The domain object for which to subscribe to fault data updates.
* @param {Function} callback - The callback function to be called with fault data updates.
* @returns {Function} unsubscribe - A function to unsubscribe from the fault data updates.
*/ */
subscribe(domainObject, callback) { subscribe(domainObject, callback) {
if (!this.provider?.supportsSubscribe(domainObject)) { if (!this.provider?.supportsSubscribe(domainObject)) {
return Promise.reject(); return Promise.reject('Provider does not support subscribe operation');
} }
return this.provider.subscribe(domainObject, callback); return this.provider.subscribe(domainObject, callback);
} }
/** /**
* @param {Fault} fault * Acknowledges a fault using the provider's acknowledgeFault method.
* @param {*} ackData *
* @param {Fault} fault - The fault object to be acknowledged.
* @param {*} ackData - Additional data required for acknowledging the fault.
* @returns {Promise.<T>} - A promise that resolves when the fault is acknowledged.
*/ */
acknowledgeFault(fault, ackData) { acknowledgeFault(fault, ackData) {
return this.provider.acknowledgeFault(fault, ackData); return this.provider.acknowledgeFault(fault, ackData);
} }
/** /**
* @param {Fault} fault * Shelves a fault using the provider's shelveFault method.
* @param {*} shelveData *
* @returns {Promise.<T>} * @param {Fault} fault - The fault object to be shelved.
* @param {*} shelveData - Additional data required for shelving the fault.
* @returns {Promise.<T>} - A promise that resolves when the fault is shelved.
*/ */
shelveFault(fault, shelveData) { shelveFault(fault, shelveData) {
return this.provider.shelveFault(fault, shelveData); return this.provider.shelveFault(fault, shelveData);
} }
/**
* Retrieves the available shelve durations from the provider, or the default durations if the
* provider does not provide any.
* @returns {ShelveDuration[]}
*/
getShelveDurations() {
return this.provider?.getShelveDurations() ?? DEFAULT_SHELVE_DURATIONS;
}
} }
/**
* @typedef {Object} ShelveDuration
* @property {string} name - The name of the shelve duration
* @property {number|null} value - The value of the shelve duration in milliseconds, or null for indefinite
*/
/** /**
* @typedef {Object} TriggerValueInfo * @typedef {Object} TriggerValueInfo
* @property {number} value * @property {number} value

View File

@ -20,25 +20,46 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import installWorker from './WebSocketWorker.js'; import installWorker from './WebSocketWorker.js';
/** /**
* Describes the strategy to be used when batching WebSocket messages * @typedef RequestIdleCallbackOptions
* * @prop {Number} timeout If the number of milliseconds represented by this
* @typedef BatchingStrategy * parameter has elapsed and the callback has not already been called, invoke
* @property {Function} shouldBatchMessage a function that accepts a single * the callback.
* argument - the raw message received from the websocket. Every message * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
* received will be evaluated against this function so it should be performant.
* Note also that this function is executed in a worker, so it must be
* completely self-contained with no external dependencies. The function
* should return `true` if the message should be batched, and `false` if not.
* @property {Function} getBatchIdFromMessage a function that accepts a
* single argument - the raw message received from the websocket. Only messages
* where `shouldBatchMessage` has evaluated to true will be passed into this
* function. The function should return a unique value on which to batch the
* messages. For example a telemetry, channel, or parameter identifier.
*/ */
/** /**
* Provides a reliable and convenient WebSocket abstraction layer that handles * Mocks requestIdleCallback for Safari using setTimeout. Functionality will be
* a lot of boilerplate common to managing WebSocket connections such as: * identical to setTimeout in Safari, which is to fire the callback function
* after the provided timeout period.
*
* In browsers that support requestIdleCallback, this const is just a
* pointer to the native function.
*
* @param {Function} callback a callback to be invoked during the next idle period, or
* after the specified timeout
* @param {RequestIdleCallbackOptions} options
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
*
*/
function requestIdleCallbackPolyfill(callback, options) {
return (
// eslint-disable-next-line compat/compat
window.requestIdleCallback ??
((fn, { timeout }) =>
setTimeout(() => {
fn({ didTimeout: false });
}, timeout))
);
}
const requestIdleCallback = requestIdleCallbackPolyfill();
const ONE_SECOND = 1000;
/**
* Provides a WebSocket abstraction layer that handles a lot of boilerplate common
* to managing WebSocket connections such as:
* - Establishing a WebSocket connection to a server * - Establishing a WebSocket connection to a server
* - Reconnecting on error, with a fallback strategy * - Reconnecting on error, with a fallback strategy
* - Queuing messages so that clients can send messages without concern for the current * - Queuing messages so that clients can send messages without concern for the current
@ -49,22 +70,19 @@ import installWorker from './WebSocketWorker.js';
* and batching of messages without blocking either the UI or server. * and batching of messages without blocking either the UI or server.
* *
*/ */
// Shim for Internet Explorer, I mean Safari. It doesn't support requestIdleCallback, but it's in a tech preview, so it will be dropping soon.
const requestIdleCallback =
// eslint-disable-next-line compat/compat
window.requestIdleCallback ?? ((fn, { timeout }) => setTimeout(fn, timeout));
const ONE_SECOND = 1000;
const FIVE_SECONDS = 5 * ONE_SECOND;
class BatchingWebSocket extends EventTarget { class BatchingWebSocket extends EventTarget {
#worker; #worker;
#openmct; #openmct;
#showingRateLimitNotification; #showingRateLimitNotification;
#maxBatchSize; #maxBufferSize;
#applicationIsInitializing; #throttleRate;
#maxBatchWait;
#firstBatchReceived; #firstBatchReceived;
#lastBatchReceived;
#peakBufferSize = Number.NEGATIVE_INFINITY;
/**
* @param {import('openmct.js').OpenMCT} openmct
*/
constructor(openmct) { constructor(openmct) {
super(); super();
// Install worker, register listeners etc. // Install worker, register listeners etc.
@ -74,9 +92,8 @@ class BatchingWebSocket extends EventTarget {
this.#worker = new Worker(workerUrl); this.#worker = new Worker(workerUrl);
this.#openmct = openmct; this.#openmct = openmct;
this.#showingRateLimitNotification = false; this.#showingRateLimitNotification = false;
this.#maxBatchSize = Number.POSITIVE_INFINITY; this.#maxBufferSize = Number.POSITIVE_INFINITY;
this.#maxBatchWait = ONE_SECOND; this.#throttleRate = ONE_SECOND;
this.#applicationIsInitializing = true;
this.#firstBatchReceived = false; this.#firstBatchReceived = false;
const routeMessageToHandler = this.#routeMessageToHandler.bind(this); const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
@ -89,20 +106,6 @@ class BatchingWebSocket extends EventTarget {
}, },
{ once: true } { once: true }
); );
openmct.once('start', () => {
// An idle callback is a pretty good indication that a complex display is done loading. At that point set the batch size more conservatively.
// Force it after 5 seconds if it hasn't happened yet.
requestIdleCallback(
() => {
this.#applicationIsInitializing = false;
this.setMaxBatchSize(this.#maxBatchSize);
},
{
timeout: FIVE_SECONDS
}
);
});
} }
/** /**
@ -137,57 +140,48 @@ class BatchingWebSocket extends EventTarget {
} }
/** /**
* Set the strategy used to both decide which raw messages to batch, and how to group * @param {number} maxBufferSize the maximum length of the receive buffer in characters.
* them.
* @param {BatchingStrategy} strategy The batching strategy to use when evaluating
* raw messages from the WebSocket.
*/
setBatchingStrategy(strategy) {
const serializedStrategy = {
shouldBatchMessage: strategy.shouldBatchMessage.toString(),
getBatchIdFromMessage: strategy.getBatchIdFromMessage.toString()
};
this.#worker.postMessage({
type: 'setBatchingStrategy',
serializedStrategy
});
}
/**
* @param {number} maxBatchSize the maximum length of a batch of messages. For example,
* the maximum number of telemetry values to batch before dropping them
* Note that this is a fail-safe that is only invoked if performance drops to the * Note that this is a fail-safe that is only invoked if performance drops to the
* point where Open MCT cannot keep up with the amount of telemetry it is receiving. * point where Open MCT cannot keep up with the amount of telemetry it is receiving.
* In this event it will sacrifice the oldest telemetry in the batch in favor of the * In this event it will sacrifice the oldest telemetry in the batch in favor of the
* most recent telemetry. The user will be informed that telemetry has been dropped. * most recent telemetry. The user will be informed that telemetry has been dropped.
* *
* This should be set appropriately for the expected data rate. eg. If telemetry * This should be set appropriately for the expected data rate. eg. If typical usage
* is received at 10Hz for each telemetry point, then a minimal combination of batch * sees 2000 messages arriving at a client per second, with an average message size
* size and rate is 10 and 1000 respectively. Ideally you would add some margin, so * of 500 bytes, then 2000 * 500 = 1000000 characters will be right on the limit.
* 15 would probably be a better batch size. * In this scenario, a buffer size of 1500000 character might be more appropriate
* to allow some overhead for bursty telemetry, and temporary UI load during page
* load.
*
* The PerformanceIndicator plugin (openmct.plugins.PerformanceIndicator) gives
* statistics on buffer utilization. It can be used to scale the buffer appropriately.
*/ */
setMaxBatchSize(maxBatchSize) { setMaxBufferSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize; this.#maxBufferSize = maxBatchSize;
if (!this.#applicationIsInitializing) { this.#sendMaxBufferSizeToWorker(this.#maxBufferSize);
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
}
} }
setMaxBatchWait(wait) { setThrottleRate(throttleRate) {
this.#maxBatchWait = wait; this.#throttleRate = throttleRate;
this.#sendBatchWaitToWorker(this.#maxBatchWait); this.#sendThrottleRateToWorker(this.#throttleRate);
} }
#sendMaxBatchSizeToWorker(maxBatchSize) { setThrottleMessagePattern(throttleMessagePattern) {
this.#worker.postMessage({ this.#worker.postMessage({
type: 'setMaxBatchSize', type: 'setThrottleMessagePattern',
maxBatchSize throttleMessagePattern
}); });
} }
#sendBatchWaitToWorker(maxBatchWait) { #sendMaxBufferSizeToWorker(maxBufferSize) {
this.#worker.postMessage({ this.#worker.postMessage({
type: 'setMaxBatchWait', type: 'setMaxBufferSize',
maxBatchWait maxBufferSize
});
}
#sendThrottleRateToWorker(throttleRate) {
this.#worker.postMessage({
type: 'setThrottleRate',
throttleRate
}); });
} }
@ -203,9 +197,38 @@ class BatchingWebSocket extends EventTarget {
#routeMessageToHandler(message) { #routeMessageToHandler(message) {
if (message.data.type === 'batch') { if (message.data.type === 'batch') {
this.start = Date.now();
const batch = message.data.batch; const batch = message.data.batch;
if (batch.dropped === true && !this.#showingRateLimitNotification) { const now = performance.now();
let currentBufferLength = message.data.currentBufferLength;
let maxBufferSize = message.data.maxBufferSize;
let parameterCount = batch.length;
if (this.#peakBufferSize < currentBufferLength) {
this.#peakBufferSize = currentBufferLength;
}
if (this.#openmct.performance !== undefined) {
if (!isNaN(this.#lastBatchReceived)) {
const elapsed = (now - this.#lastBatchReceived) / 1000;
this.#lastBatchReceived = now;
this.#openmct.performance.measurements.set(
'Parameters/s',
Math.floor(parameterCount / elapsed)
);
}
this.#openmct.performance.measurements.set(
'Buff. Util. (bytes)',
`${currentBufferLength} / ${maxBufferSize}`
);
this.#openmct.performance.measurements.set(
'Peak Buff. Util. (bytes)',
`${this.#peakBufferSize} / ${maxBufferSize}`
);
}
this.start = Date.now();
const dropped = message.data.dropped;
if (dropped === true && !this.#showingRateLimitNotification) {
const notification = this.#openmct.notifications.alert( const notification = this.#openmct.notifications.alert(
'Telemetry dropped due to client rate limiting.', 'Telemetry dropped due to client rate limiting.',
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' } { hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
@ -240,18 +263,16 @@ class BatchingWebSocket extends EventTarget {
console.warn(`Event loop is too busy to process batch.`); console.warn(`Event loop is too busy to process batch.`);
this.#waitUntilIdleAndRequestNextBatch(batch); this.#waitUntilIdleAndRequestNextBatch(batch);
} else { } else {
// After ingesting a telemetry batch, wait until the event loop is idle again before
// informing the worker we are ready for another batch.
this.#readyForNextBatch(); this.#readyForNextBatch();
} }
} else { } else {
if (waitedFor > ONE_SECOND) { if (waitedFor > this.#throttleRate) {
console.warn(`Warning, batch processing took ${waitedFor}ms`); console.warn(`Warning, batch processing took ${waitedFor}ms`);
} }
this.#readyForNextBatch(); this.#readyForNextBatch();
} }
}, },
{ timeout: ONE_SECOND } { timeout: this.#throttleRate }
); );
} }
} }

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();
this._watchBounds(); this._watchBounds();
@ -116,8 +116,7 @@ export default class TelemetryCollection extends EventEmitter {
} }
/** /**
* This will start the requests for historical and realtime data, * @returns {Array} All bounded telemetry
* as well as setting up initial values and watchers
*/ */
getAll() { getAll() {
return this.boundedTelemetry; return this.boundedTelemetry;
@ -128,7 +127,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

@ -24,10 +24,6 @@ export default function installWorker() {
const ONE_SECOND = 1000; const ONE_SECOND = 1000;
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000]; const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
/**
* @typedef {import('./BatchingWebSocket').BatchingStrategy} BatchingStrategy
*/
/** /**
* Provides a WebSocket connection that is resilient to errors and dropouts. * Provides a WebSocket connection that is resilient to errors and dropouts.
* On an error or dropout, will automatically reconnect. * On an error or dropout, will automatically reconnect.
@ -215,17 +211,17 @@ export default function installWorker() {
case 'message': case 'message':
this.#websocket.enqueueMessage(message.data.message); this.#websocket.enqueueMessage(message.data.message);
break; break;
case 'setBatchingStrategy':
this.setBatchingStrategy(message);
break;
case 'readyForNextBatch': case 'readyForNextBatch':
this.#messageBatcher.readyForNextBatch(); this.#messageBatcher.readyForNextBatch();
break; break;
case 'setMaxBatchSize': case 'setMaxBufferSize':
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize); this.#messageBatcher.setMaxBufferSize(message.data.maxBufferSize);
break; break;
case 'setMaxBatchWait': case 'setThrottleRate':
this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait); this.#messageBatcher.setThrottleRate(message.data.throttleRate);
break;
case 'setThrottleMessagePattern':
this.#messageBatcher.setThrottleMessagePattern(message.data.throttleMessagePattern);
break; break;
default: default:
throw new Error(`Unknown message type: ${type}`); throw new Error(`Unknown message type: ${type}`);
@ -238,122 +234,69 @@ export default function installWorker() {
disconnect() { disconnect() {
this.#websocket.disconnect(); this.#websocket.disconnect();
} }
setBatchingStrategy(message) {
const { serializedStrategy } = message.data;
const batchingStrategy = {
// eslint-disable-next-line no-new-func
shouldBatchMessage: new Function(`return ${serializedStrategy.shouldBatchMessage}`)(),
// eslint-disable-next-line no-new-func
getBatchIdFromMessage: new Function(`return ${serializedStrategy.getBatchIdFromMessage}`)()
// Will also include maximum batch length here
};
this.#messageBatcher.setBatchingStrategy(batchingStrategy);
}
} }
/** /**
* Received messages from the WebSocket, and passes them along to the * Responsible for buffering messages
* Worker interface and back to the main thread.
*/ */
class WebSocketToWorkerMessageBroker { class MessageBuffer {
#worker; #buffer;
#messageBatcher; #currentBufferLength;
#dropped;
constructor(messageBatcher, worker) { #maxBufferSize;
this.#messageBatcher = messageBatcher;
this.#worker = worker;
}
routeMessageToHandler(data) {
if (this.#messageBatcher.shouldBatchMessage(data)) {
this.#messageBatcher.addMessageToBatch(data);
} else {
this.#worker.postMessage({
type: 'message',
message: data
});
}
}
}
/**
* Responsible for batching messages according to the defined batching strategy.
*/
class MessageBatcher {
#batch;
#batchingStrategy;
#hasBatch = false;
#maxBatchSize;
#readyForNextBatch; #readyForNextBatch;
#worker; #worker;
#throttledSendNextBatch; #throttledSendNextBatch;
#throttleMessagePattern;
constructor(worker) { constructor(worker) {
// No dropping telemetry unless we're explicitly told to. // No dropping telemetry unless we're explicitly told to.
this.#maxBatchSize = Number.POSITIVE_INFINITY; this.#maxBufferSize = Number.POSITIVE_INFINITY;
this.#readyForNextBatch = false; this.#readyForNextBatch = false;
this.#worker = worker; this.#worker = worker;
this.#resetBatch(); this.#resetBatch();
this.setMaxBatchWait(ONE_SECOND); this.setThrottleRate(ONE_SECOND);
} }
#resetBatch() { #resetBatch() {
this.#batch = {}; //this.#batch = {};
this.#hasBatch = false; this.#buffer = [];
this.#currentBufferLength = 0;
this.#dropped = false;
} }
/**
* @param {BatchingStrategy} strategy addMessageToBuffer(message) {
*/ this.#buffer.push(message);
setBatchingStrategy(strategy) { this.#currentBufferLength += message.length;
this.#batchingStrategy = strategy;
} for (
/** let i = 0;
* Applies the `shouldBatchMessage` function from the supplied batching strategy this.#currentBufferLength > this.#maxBufferSize && i < this.#buffer.length;
* to each message to determine if it should be added to a batch. If not batched, i++
* the message is immediately sent over the worker to the main thread. ) {
* @param {any} message the message received from the WebSocket. See the WebSocket const messageToConsider = this.#buffer[i];
* documentation for more details - if (this.#shouldThrottle(messageToConsider)) {
* https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data this.#buffer.splice(i, 1);
* @returns this.#currentBufferLength -= messageToConsider.length;
*/ this.#dropped = true;
shouldBatchMessage(message) { }
return (
this.#batchingStrategy.shouldBatchMessage &&
this.#batchingStrategy.shouldBatchMessage(message)
);
}
/**
* Adds the given message to a batch. The batch group that the message is added
* to will be determined by the value returned by `getBatchIdFromMessage`.
* @param {any} message the message received from the WebSocket. See the WebSocket
* documentation for more details -
* https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
*/
addMessageToBatch(message) {
const batchId = this.#batchingStrategy.getBatchIdFromMessage(message);
let batch = this.#batch[batchId];
if (batch === undefined) {
this.#hasBatch = true;
batch = this.#batch[batchId] = [message];
} else {
batch.push(message);
}
if (batch.length > this.#maxBatchSize) {
console.warn(
`Exceeded max batch size of ${this.#maxBatchSize} for ${batchId}. Dropping value.`
);
batch.shift();
this.#batch.dropped = true;
} }
if (this.#readyForNextBatch) { if (this.#readyForNextBatch) {
this.#throttledSendNextBatch(); this.#throttledSendNextBatch();
} }
} }
setMaxBatchSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize; #shouldThrottle(message) {
return (
this.#throttleMessagePattern !== undefined && this.#throttleMessagePattern.test(message)
);
} }
setMaxBatchWait(maxBatchWait) {
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), maxBatchWait); setMaxBufferSize(maxBufferSize) {
this.#maxBufferSize = maxBufferSize;
}
setThrottleRate(throttleRate) {
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), throttleRate);
} }
/** /**
* Indicates that client code is ready to receive the next batch of * Indicates that client code is ready to receive the next batch of
@ -362,21 +305,33 @@ export default function installWorker() {
* any new data is available. * any new data is available.
*/ */
readyForNextBatch() { readyForNextBatch() {
if (this.#hasBatch) { if (this.#hasData()) {
this.#throttledSendNextBatch(); this.#throttledSendNextBatch();
} else { } else {
this.#readyForNextBatch = true; this.#readyForNextBatch = true;
} }
} }
#sendNextBatch() { #sendNextBatch() {
const batch = this.#batch; const buffer = this.#buffer;
const dropped = this.#dropped;
const currentBufferLength = this.#currentBufferLength;
this.#resetBatch(); this.#resetBatch();
this.#worker.postMessage({ this.#worker.postMessage({
type: 'batch', type: 'batch',
batch dropped,
currentBufferLength: currentBufferLength,
maxBufferSize: this.#maxBufferSize,
batch: buffer
}); });
this.#readyForNextBatch = false; this.#readyForNextBatch = false;
this.#hasBatch = false; }
#hasData() {
return this.#currentBufferLength > 0;
}
setThrottleMessagePattern(priorityMessagePattern) {
this.#throttleMessagePattern = new RegExp(priorityMessagePattern, 'm');
} }
} }
@ -408,15 +363,14 @@ export default function installWorker() {
} }
const websocket = new ResilientWebSocket(self); const websocket = new ResilientWebSocket(self);
const messageBatcher = new MessageBatcher(self); const messageBuffer = new MessageBuffer(self);
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher); const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBuffer);
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
self.addEventListener('message', (message) => { self.addEventListener('message', (message) => {
workerBroker.routeMessageToHandler(message); workerBroker.routeMessageToHandler(message);
}); });
websocket.registerMessageCallback((data) => { websocket.registerMessageCallback((data) => {
websocketBroker.routeMessageToHandler(data); messageBuffer.addMessageToBuffer(data);
}); });
self.websocketInstance = websocket; self.websocketInstance = websocket;

View File

@ -257,7 +257,9 @@ export default {
return { return {
end, end,
start start,
size: 1,
strategy: 'latest'
}; };
}, },
loadComposition() { loadComposition() {

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

@ -21,7 +21,12 @@
--> -->
<template> <template>
<div class="c-fault-mgmt__list data-selectable" :class="classesFromState"> <div
role="listitem"
:aria-label="listItemAriaLabel"
class="c-fault-mgmt__list data-selectable"
:class="classesFromState"
>
<div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox"> <div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
<input <input
type="checkbox" type="checkbox"
@ -33,29 +38,44 @@
<div class="c-fault-mgmt-item"> <div class="c-fault-mgmt-item">
<div <div
class="c-fault-mgmt__list-severity" class="c-fault-mgmt__list-severity"
:aria-label="fault.severity" :aria-label="severityAriaLabel"
:title="fault.severity"
:class="['is-severity-' + severity]" :class="['is-severity-' + severity]"
></div> ></div>
</div> </div>
<div class="c-fault-mgmt-item c-fault-mgmt__list-content"> <div class="c-fault-mgmt-item c-fault-mgmt__list-content">
<div class="c-fault-mgmt-item c-fault-mgmt__list-pathname"> <div class="c-fault-mgmt-item c-fault-mgmt__list-pathname">
<div class="c-fault-mgmt__list-path">{{ fault.namespace }}</div> <div class="c-fault-mgmt__list-path" aria-label="Fault namespace">
<div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div> {{ fault.namespace }}
</div>
<div class="c-fault-mgmt__list-faultname" aria-label="Fault name">{{ fault.name }}</div>
</div> </div>
<div class="c-fault-mgmt__list-content-right"> <div class="c-fault-mgmt__list-content-right">
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigVal"> <div class="c-fault-mgmt-item c-fault-mgmt__list-trigVal">
<div class="c-fault-mgmt-item__value" :class="tripValueClassname" title="Trip Value"> <div
class="c-fault-mgmt-item__value"
:class="tripValueClassname"
title="Trip Value"
aria-label="Trip Value"
>
{{ fault.triggerValueInfo.value }} {{ fault.triggerValueInfo.value }}
</div> </div>
</div> </div>
<div class="c-fault-mgmt-item c-fault-mgmt__list-curVal"> <div class="c-fault-mgmt-item c-fault-mgmt__list-curVal">
<div class="c-fault-mgmt-item__value" :class="liveValueClassname" title="Live Value"> <div
class="c-fault-mgmt-item__value"
:class="liveValueClassname"
title="Live Value"
aria-label="Live Value"
>
{{ fault.currentValueInfo.value }} {{ fault.currentValueInfo.value }}
</div> </div>
</div> </div>
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigTime"> <div class="c-fault-mgmt-item c-fault-mgmt__list-trigTime">
<div class="c-fault-mgmt-item__value" title="Last Trigger Time"> <div
class="c-fault-mgmt-item__value"
title="Last Trigger Time"
aria-label="Last Trigger Time"
>
{{ fault.triggerTime }} {{ fault.triggerTime }}
</div> </div>
</div> </div>
@ -98,7 +118,7 @@ export default {
emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected', 'clear-all-selected'], emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected', 'clear-all-selected'],
computed: { computed: {
checkBoxAriaLabel() { checkBoxAriaLabel() {
return `Select fault: ${this.fault.name}`; return `Select fault: ${this.fault.name || 'Unknown'} in ${this.fault.namespace || 'Unknown'}`;
}, },
classesFromState() { classesFromState() {
const exclusiveStates = [ const exclusiveStates = [
@ -165,6 +185,12 @@ export default {
classname += SEVERITY_CLASS[triggerValueInfo.monitoringResult] || ''; classname += SEVERITY_CLASS[triggerValueInfo.monitoringResult] || '';
return classname.trim(); return classname.trim();
},
listItemAriaLabel() {
return `Fault triggered at ${this.fault.triggerTime || 'Unknown'} with severity ${this.fault.severity || 'Unknown'} in ${this.fault.namespace || 'Unknown'}`;
},
severityAriaLabel() {
return `Severity: ${this.fault.severity || 'Unknown'}`;
} }
}, },
methods: { methods: {

View File

@ -68,7 +68,6 @@
import { import {
FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_ALARMS,
FAULT_MANAGEMENT_GLOBAL_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS,
FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
FILTER_ITEMS, FILTER_ITEMS,
SORT_ITEMS SORT_ITEMS
} from './constants.js'; } from './constants.js';
@ -88,6 +87,13 @@ const SEARCH_KEYS = [
'namespace' 'namespace'
]; ];
// Helper function for filtering faults
function filterFaultsByTerm(faults, searchTerm) {
return faults.filter((fault) =>
SEARCH_KEYS.some((key) => fault[key]?.toString().toLowerCase().includes(searchTerm))
);
}
export default { export default {
components: { components: {
FaultManagementListHeader, FaultManagementListHeader,
@ -111,23 +117,18 @@ export default {
}, },
filteredFaultsList() { filteredFaultsList() {
const filterName = FILTER_ITEMS[this.filterIndex]; const filterName = FILTER_ITEMS[this.filterIndex];
let list = this.faultsList; let list = this.faultsList.filter((fault) =>
filterName === 'Shelved' ? fault.shelved : !fault.shelved
// Exclude shelved alarms from all views except the Shelved view );
if (filterName !== 'Shelved') {
list = list.filter((fault) => fault.shelved !== true);
}
if (filterName === 'Acknowledged') { if (filterName === 'Acknowledged') {
list = list.filter((fault) => fault.acknowledged); list = list.filter((fault) => fault.acknowledged);
} else if (filterName === 'Unacknowledged') { } else if (filterName === 'Unacknowledged') {
list = list.filter((fault) => !fault.acknowledged); list = list.filter((fault) => !fault.acknowledged);
} else if (filterName === 'Shelved') {
list = list.filter((fault) => fault.shelved);
} }
if (this.searchTerm.length > 0) { if (this.searchTerm.length > 0) {
list = list.filter(this.filterUsingSearchTerm); list = filterFaultsByTerm(list, this.searchTerm);
} }
list.sort(SORT_ITEMS[this.sortBy].sortFunction); list.sort(SORT_ITEMS[this.sortBy].sortFunction);
@ -138,6 +139,9 @@ export default {
return this.openmct.faults.supportsActions(); return this.openmct.faults.supportsActions();
} }
}, },
created() {
this.shelveDurations = this.openmct.faults.getShelveDurations();
},
mounted() { mounted() {
this.unsubscribe = this.openmct.faults.subscribe(this.domainObject, this.updateFault); this.unsubscribe = this.openmct.faults.subscribe(this.domainObject, this.updateFault);
}, },
@ -158,14 +162,13 @@ export default {
}); });
} }
}, },
updateFaultList() { async updateFaultList() {
this.openmct.faults.request(this.domainObject).then((faultsData) => { const faultsData = await this.openmct.faults.request(this.domainObject);
if (faultsData?.length > 0) { if (faultsData?.length > 0) {
this.faultsList = faultsData.map((fd) => fd.fault); this.faultsList = faultsData.map((fd) => fd.fault);
} else { } else {
this.faultsList = []; this.faultsList = [];
} }
});
}, },
filterUsingSearchTerm(fault) { filterUsingSearchTerm(fault) {
if (!fault) { if (!fault) {
@ -223,14 +226,29 @@ export default {
); );
}, },
async toggleAcknowledgeSelected(faults = this.selectedFaults) { async toggleAcknowledgeSelected(faults = this.selectedFaults) {
let title = ''; const title = this.getAcknowledgeTitle(faults);
if (faults.length > 1) {
title = `Acknowledge ${faults.length} selected faults`;
} else if (faults.length === 1) {
title = `Acknowledge fault: ${faults[0].name}`;
}
const formStructure = { const formStructure = this.getAcknowledgeFormStructure(title);
try {
const data = await this.openmct.forms.showForm(formStructure);
this.acknowledgeFaults(faults, data);
} catch (err) {
console.error(err);
} finally {
this.resetSelectedFaultMap();
}
},
getAcknowledgeTitle(faults) {
if (faults.length > 1) {
return `Acknowledge ${faults.length} selected faults`;
} else if (faults.length === 1) {
return `Acknowledge fault: ${faults[0].name}`;
}
return '';
},
getAcknowledgeFormStructure(title) {
return {
title, title,
sections: [ sections: [
{ {
@ -253,17 +271,11 @@ export default {
} }
} }
}; };
},
try { acknowledgeFaults(faults, data) {
const data = await this.openmct.forms.showForm(formStructure); faults.forEach((fault) => {
faults.forEach((fault) => { this.openmct.faults.acknowledgeFault(fault, data);
this.openmct.faults.acknowledgeFault(fault, data); });
});
} catch (err) {
console.error(err);
} finally {
this.resetSelectedFaultMap();
}
}, },
resetSelectedFaultMap() { resetSelectedFaultMap() {
Object.keys(this.selectedFaultMap).forEach((key) => { Object.keys(this.selectedFaultMap).forEach((key) => {
@ -273,7 +285,7 @@ export default {
async toggleShelveSelected(faults = this.selectedFaults, shelveData = {}) { async toggleShelveSelected(faults = this.selectedFaults, shelveData = {}) {
const { shelved = true } = shelveData; const { shelved = true } = shelveData;
if (shelved) { if (shelved) {
let title = const title =
faults.length > 1 faults.length > 1
? `Shelve ${faults.length} selected faults` ? `Shelve ${faults.length} selected faults`
: `Shelve fault: ${faults[0].name}`; : `Shelve fault: ${faults[0].name}`;
@ -295,10 +307,10 @@ export default {
key: 'shelveDuration', key: 'shelveDuration',
control: 'select', control: 'select',
name: 'Shelve duration', name: 'Shelve duration',
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, options: this.shelveDurations,
required: false, required: false,
cssClass: 'l-input-lg', cssClass: 'l-input-lg',
value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value value: this.shelveDurations[0].value
} }
] ]
} }
@ -318,19 +330,16 @@ export default {
} }
shelveData.comment = data.comment || ''; shelveData.comment = data.comment || '';
shelveData.shelveDuration = shelveData.shelveDuration = data.shelveDuration ?? this.shelveDurations[0].value;
data.shelveDuration !== undefined
? data.shelveDuration
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
} else { } else {
shelveData = { shelveData = {
shelved: false shelved: false
}; };
} }
Object.values(faults).forEach((selectedFault) => { await Promise.all(
this.openmct.faults.shelveFault(selectedFault, shelveData); faults.map((selectedFault) => this.openmct.faults.shelveFault(selectedFault, shelveData))
}); );
this.selectedFaultMap = {}; this.selectedFaultMap = {};
}, },

View File

@ -370,6 +370,7 @@ export default {
createImageWrapper(index, image, showImagePlaceholders) { createImageWrapper(index, image, showImagePlaceholders) {
const id = `${ID_PREFIX}${image.time}`; const id = `${ID_PREFIX}${image.time}`;
let imageWrapper = document.createElement('div'); let imageWrapper = document.createElement('div');
imageWrapper.ariaLabel = id;
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS); imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
imageWrapper.style.left = `${this.xScale(image.time)}px`; imageWrapper.style.left = `${this.xScale(image.time)}px`;
this.setNSAttributesForElement(imageWrapper, { this.setNSAttributesForElement(imageWrapper, {

View File

@ -620,7 +620,7 @@ export default {
if (matchIndex > -1) { if (matchIndex > -1) {
this.setFocusedImage(matchIndex); this.setFocusedImage(matchIndex);
} else { } else {
this.paused(); this.paused(false);
} }
} }
@ -1082,7 +1082,7 @@ export default {
paused(state) { paused(state) {
this.isPaused = Boolean(state); this.isPaused = Boolean(state);
if (!state) { if (!this.isPaused) {
this.previousFocusedImage = null; this.previousFocusedImage = null;
this.setFocusedImage(this.nextImageIndex); this.setFocusedImage(this.nextImageIndex);
this.autoScroll = true; this.autoScroll = true;

View File

@ -90,16 +90,17 @@ export default {
dataCleared() { dataCleared() {
this.imageHistory = []; this.imageHistory = [];
}, },
dataRemoved(dataToRemove) { dataRemoved(removed) {
this.imageHistory = this.imageHistory.filter((existingDatum) => { const removedTimestamps = {};
const shouldKeep = dataToRemove.some((datumToRemove) => { removed.forEach((_removed) => {
const existingDatumTimestamp = this.parseTime(existingDatum); const removedTimestamp = this.parseTime(_removed);
const datumToRemoveTimestamp = this.parseTime(datumToRemove); removedTimestamps[removedTimestamp] = true;
});
return existingDatumTimestamp !== datumToRemoveTimestamp; this.imageHistory = this.imageHistory.filter((image) => {
}); const imageTimestamp = this.parseTime(image);
return shouldKeep; return !removedTimestamps[imageTimestamp];
}); });
}, },
setDataTimeContext() { setDataTimeContext() {

View File

@ -19,14 +19,23 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const PERFORMANCE_OVERLAY_RENDER_INTERVAL = 1000;
export default function PerformanceIndicator() { export default function PerformanceIndicator() {
return function install(openmct) { return function install(openmct) {
let frames = 0; let frames = 0;
let lastCalculated = performance.now(); let lastCalculated = performance.now();
const indicator = openmct.indicators.simpleIndicator(); openmct.performance = {
measurements: new Map()
};
const indicator = openmct.indicators.simpleIndicator();
indicator.key = 'performance-indicator';
indicator.text('~ fps'); indicator.text('~ fps');
indicator.description('Performance Indicator');
indicator.statusClass('s-status-info'); indicator.statusClass('s-status-info');
indicator.on('click', showOverlay);
openmct.indicators.add(indicator); openmct.indicators.add(indicator);
let rafHandle = requestAnimationFrame(incrementFrames); let rafHandle = requestAnimationFrame(incrementFrames);
@ -58,5 +67,58 @@ export default function PerformanceIndicator() {
indicator.statusClass('s-status-error'); indicator.statusClass('s-status-error');
} }
} }
function showOverlay() {
const overlayStylesText = `
#c-performance-indicator--overlay {
background-color:rgba(0,0,0,0.5);
position: absolute;
width: 300px;
left: calc(50% - 300px);
}
`;
const overlayMarkup = `
<div id="c-performance-indicator--overlay" title="Performance Overlay">
<table id="c-performance-indicator--table">
<tr class="c-performance-indicator--row"><td class="c-performance-indicator--measurement-name"></td><td class="c-performance-indicator--measurement-value"></td></tr>
</table>
</div>
`;
const overlayTemplate = document.createElement('div');
overlayTemplate.innerHTML = overlayMarkup;
const overlay = overlayTemplate.cloneNode(true);
overlay.querySelector('.c-performance-indicator--row').remove();
const overlayStyles = document.createElement('style');
overlayStyles.appendChild(document.createTextNode(overlayStylesText));
document.head.appendChild(overlayStyles);
document.body.appendChild(overlay);
indicator.off('click', showOverlay);
const interval = setInterval(() => {
overlay.querySelector('#c-performance-indicator--table').innerHTML = '';
for (const [name, value] of openmct.performance.measurements.entries()) {
const newRow = overlayTemplate
.querySelector('.c-performance-indicator--row')
.cloneNode(true);
newRow.querySelector('.c-performance-indicator--measurement-name').innerText = name;
newRow.querySelector('.c-performance-indicator--measurement-value').innerText = value;
overlay.querySelector('#c-performance-indicator--table').appendChild(newRow);
}
}, PERFORMANCE_OVERLAY_RENDER_INTERVAL);
indicator.on(
'click',
() => {
overlayStyles.remove();
overlay.remove();
indicator.on('click', showOverlay);
clearInterval(interval);
},
{ once: true, capture: true }
);
}
}; };
} }

File diff suppressed because it is too large Load Diff

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,17 +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) {
// Don't ever resolve, this triggers the bug. 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

@ -25,7 +25,7 @@ import _ from 'lodash';
import StalenessUtils from '../../utils/staleness.js'; import StalenessUtils from '../../utils/staleness.js';
import TableRowCollection from './collections/TableRowCollection.js'; import TableRowCollection from './collections/TableRowCollection.js';
import { MODE, ORDER } from './constants.js'; import { MODE } from './constants.js';
import TelemetryTableColumn from './TelemetryTableColumn.js'; import TelemetryTableColumn from './TelemetryTableColumn.js';
import TelemetryTableConfiguration from './TelemetryTableConfiguration.js'; import TelemetryTableConfiguration from './TelemetryTableConfiguration.js';
import TelemetryTableNameColumn from './TelemetryTableNameColumn.js'; import TelemetryTableNameColumn from './TelemetryTableNameColumn.js';
@ -130,18 +130,9 @@ export default class TelemetryTable extends EventEmitter {
createTableRowCollections() { createTableRowCollections() {
this.tableRows = new TableRowCollection(); this.tableRows = new TableRowCollection();
//Fetch any persisted default sort
let sortOptions = this.configuration.getConfiguration().sortOptions;
//If no persisted sort order, default to sorting by time system, descending.
sortOptions = sortOptions || {
key: this.openmct.time.getTimeSystem().key,
direction: ORDER.DESCENDING
};
this.updateRowLimit(); this.updateRowLimit();
this.tableRows.sortBy(sortOptions); this.tableRows.sortBy(this.configuration.getSortOptions());
this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData); this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);
} }
@ -172,8 +163,7 @@ export default class TelemetryTable extends EventEmitter {
this.removeTelemetryCollection(keyString); this.removeTelemetryCollection(keyString);
let sortOptions = this.configuration.getConfiguration().sortOptions; requestOptions.order = this.configuration.getSortOptions().direction;
requestOptions.order = sortOptions?.direction ?? ORDER.DESCENDING; // default to descending
if (this.telemetryMode === MODE.PERFORMANCE) { if (this.telemetryMode === MODE.PERFORMANCE) {
requestOptions.size = this.rowLimit; requestOptions.size = this.rowLimit;
@ -442,12 +432,13 @@ export default class TelemetryTable extends EventEmitter {
} }
sortBy(sortOptions) { sortBy(sortOptions) {
this.tableRows.sortBy(sortOptions); this.configuration.setSortOptions(sortOptions);
if (this.openmct.editor.isEditing()) { if (this.telemetryMode === MODE.PERFORMANCE) {
let configuration = this.configuration.getConfiguration(); this.tableRows.setSortOptions(sortOptions);
configuration.sortOptions = sortOptions; this.clearAndResubscribe();
this.configuration.updateConfiguration(configuration); } else {
this.tableRows.sortBy(sortOptions);
} }
} }

View File

@ -23,7 +23,11 @@
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import _ from 'lodash'; import _ from 'lodash';
import { ORDER } from './constants';
export default class TelemetryTableConfiguration extends EventEmitter { export default class TelemetryTableConfiguration extends EventEmitter {
#sortOptions;
constructor(domainObject, openmct, options) { constructor(domainObject, openmct, options) {
super(); super();
@ -211,6 +215,26 @@ export default class TelemetryTableConfiguration extends EventEmitter {
this.updateConfiguration(configuration); this.updateConfiguration(configuration);
} }
getSortOptions() {
return (
this.#sortOptions ||
this.getConfiguration().sortOptions || {
key: this.openmct.time.getTimeSystem().key,
direction: ORDER.DESCENDING
}
);
}
setSortOptions(sortOptions) {
this.#sortOptions = sortOptions;
if (this.openmct.editor.isEditing()) {
let configuration = this.getConfiguration();
configuration.sortOptions = sortOptions;
this.updateConfiguration(configuration);
}
}
destroy() { destroy() {
this.unlistenFromMutation(); this.unlistenFromMutation();
} }

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);
} }
@ -273,7 +273,7 @@ export default class TableRowCollection extends EventEmitter {
*/ */
sortBy(sortOptions) { sortBy(sortOptions) {
if (arguments.length > 0) { if (arguments.length > 0) {
this.sortOptions = sortOptions; this.setSortOptions(sortOptions);
this.rows = _.orderBy( this.rows = _.orderBy(
this.rows, this.rows,
(row) => row.getParsedValue(sortOptions.key), (row) => row.getParsedValue(sortOptions.key),
@ -286,6 +286,10 @@ export default class TableRowCollection extends EventEmitter {
return Object.assign({}, this.sortOptions); return Object.assign({}, this.sortOptions);
} }
setSortOptions(sortOptions) {
this.sortOptions = sortOptions;
}
setColumnFilter(columnKey, filter) { setColumnFilter(columnKey, filter) {
filter = filter.trim().toLowerCase(); filter = filter.trim().toLowerCase();
let wasBlank = this.columnFilters[columnKey] === undefined; let wasBlank = this.columnFilters[columnKey] === undefined;

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

@ -99,6 +99,7 @@ export default {
this.composition.off('remove', this.removeItem); this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.reorder); this.composition.off('reorder', this.reorder);
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
this.handleContentResize.cancel();
this.contentResizeObserver.disconnect(); this.contentResizeObserver.disconnect();
}, },
mounted() { mounted() {

View File

@ -461,7 +461,7 @@ $colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid o
$colorGaugeRange: $colorBodyFg; // Range text color $colorGaugeRange: $colorBodyFg; // Range text color
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4); $colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
$colorGaugeLimitLow: $colorGaugeLimitHigh; $colorGaugeLimitLow: $colorGaugeLimitHigh;
$colorGaugeNeedle: rgba(#fff, 0); $colorGaugeNeedle: $colorGaugeValue; // Color of needle in a needle gauge.
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions $transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges $marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
$gaugeMeterValueShadow: rgba(255, 255, 255, 0); $gaugeMeterValueShadow: rgba(255, 255, 255, 0);

View File

@ -477,7 +477,7 @@ $colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid o
$colorGaugeRange: $colorBodyFg; // Range text color $colorGaugeRange: $colorBodyFg; // Range text color
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4); $colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
$colorGaugeLimitLow: $colorGaugeLimitHigh; $colorGaugeLimitLow: $colorGaugeLimitHigh;
$colorGaugeNeedle: rgba(#fff, 0); $colorGaugeNeedle: $colorGaugeValue; // Color of needle in a needle gauge.
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions $transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges $marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
$gaugeMeterValueShadow: rgba(255, 255, 255, 0); $gaugeMeterValueShadow: rgba(255, 255, 255, 0);

View File

@ -460,7 +460,7 @@ $colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid o
$colorGaugeRange: $colorBodyFg; // Range text color $colorGaugeRange: $colorBodyFg; // Range text color
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.2); $colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.2);
$colorGaugeLimitLow: $colorGaugeLimitHigh; $colorGaugeLimitLow: $colorGaugeLimitHigh;
$colorGaugeNeedle: rgba(#fff, 0); $colorGaugeNeedle: $colorGaugeValue; // Color of needle in a needle gauge.
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions $transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges $marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
$gaugeMeterValueShadow: rgba(255, 255, 255, 0); $gaugeMeterValueShadow: rgba(255, 255, 255, 0);

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>