Compare commits

..

2 Commits

Author SHA1 Message Date
e664afd468 chore: bump version to 4.0.0 (#7813) 2024-08-14 11:48:36 -07:00
d786452abe cherry-pick(#7806): chore: re-enable perf/mem tests on PR + fix broken locator in imagery perf test (#7812)
chore: re-enable perf/mem tests on PR + fix broken locator in imagery perf test (#7806)

* test: fix broken locator in imagery perf test

* Prevent this from happening

* make rule explicit

* test: maintain `locator()` pattern for contract tests

* test(couchdb): try some new techniques to stabilize the test

* Revert "test(couchdb): try some new techniques to stabilize the test"

This reverts commit 9aa1ea95a1.

* chore: revert to `networkidle` and disable eslint rule

* test: add `@network` annotation for tests with real network requests

---------

Co-authored-by: Hill, John (ARC-TI)[KBR Wyle Services, LLC] <john.c.hill@nasa.gov>
2024-08-13 16:00:43 -07:00
106 changed files with 1365 additions and 3253 deletions

View File

@ -5,11 +5,11 @@ orbs:
executors: executors:
pw-focal-development: pw-focal-development:
docker: docker:
- image: mcr.microsoft.com/playwright:v1.48.1-focal - image: mcr.microsoft.com/playwright:v1.45.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,45 +37,14 @@ 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
- download_verify_codecov_cli - run: npm run cov:e2e:<<parameters.suite>>:publish
- 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:
@ -112,15 +81,7 @@ 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
- download_verify_codecov_cli - run: npm run cov:unit:publish
- 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:
@ -135,13 +96,13 @@ jobs:
suite: #ci or full suite: #ci or full
type: string type: string
executor: pw-focal-development executor: pw-focal-development
parallelism: 8 parallelism: 7
steps: steps:
- build_and_install: - build_and_install:
node-version: lts/hydrogen node-version: lts/hydrogen
- when: #Only install chrome-beta when running the 'full' suite to save $$$ - when: #Only install chrome-beta when running the 'full' suite to save $$$
condition: condition:
equal: ['full', <<parameters.suite>>] equal: ["full", <<parameters.suite>>]
steps: steps:
- run: npx playwright install chrome-beta - run: npx playwright install chrome-beta
- run: - run:
@ -198,7 +159,7 @@ jobs:
steps: steps:
- build_and_install: - build_and_install:
node-version: lts/hydrogen node-version: lts/hydrogen
- run: npx playwright@1.48.1 install #Necessary for bare ubuntu machine - run: npx playwright@1.45.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
@ -286,8 +247,8 @@ workflows:
overall-circleci-commit-status: #These jobs run on every commit overall-circleci-commit-status: #These jobs run on every commit
jobs: jobs:
- lint: - lint:
name: node22-lint name: node20-lint
node-version: '22' node-version: lts/iron
- unit-test: - unit-test:
name: node18-chrome name: node18-chrome
node-version: lts/hydrogen node-version: lts/hydrogen
@ -304,8 +265,8 @@ workflows:
the-nightly: #These jobs do not run on PRs, but against master at night the-nightly: #These jobs do not run on PRs, but against master at night
jobs: jobs:
- unit-test: - unit-test:
name: node22-chrome-nightly name: node20-chrome-nightly
node-version: '22' node-version: lts/iron
- unit-test: - unit-test:
name: node18-chrome name: node18-chrome
node-version: lts/hydrogen node-version: lts/hydrogen
@ -323,7 +284,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.48.1 install - run: npx playwright@1.45.2 install
- name: Start CouchDB Docker Container and Init with Setup Scripts - name: Start CouchDB Docker Container and Init with Setup Scripts
run: | run: |
@ -52,17 +52,10 @@ jobs:
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
uses: codecov/codecov-action@v4 env:
with: SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }} run: npm run cov:e2e:full:publish
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.48.1 install - run: npx playwright@1.45.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.48.1 install - run: npx playwright@1.45.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.47.2 install - run: npx playwright@1.45.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

View File

@ -15,5 +15,5 @@ export default merge(common, {
__OPENMCT_ROOT_RELATIVE__: '""' __OPENMCT_ROOT_RELATIVE__: '""'
}) })
], ],
devtool: 'eval-source-map' devtool: 'source-map'
}); });

View File

@ -27,12 +27,6 @@ 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,7 +469,6 @@ 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,37 +227,6 @@ 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.
@ -510,10 +479,6 @@ async function setTimeConductorBounds(page, { submitChanges = true, ...bounds })
// Open the time conductor popup // Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
// FIXME: https://github.com/nasa/openmct/pull/7818
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
if (startDate) { if (startDate) {
await page.getByLabel('Start date').fill(startDate); await page.getByLabel('Start date').fill(startDate);
} }
@ -664,33 +629,13 @@ 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,7 +25,6 @@ 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({
@ -37,7 +36,6 @@ 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({
@ -49,7 +47,6 @@ 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({
@ -61,7 +58,6 @@ 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' });
@ -81,8 +77,6 @@ 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);
@ -92,8 +86,6 @@ 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) => {
@ -107,8 +99,6 @@ 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) => {
@ -116,43 +106,50 @@ export async function acknowledgeMultipleFaults(page, ...nums) {
}); });
await Promise.all(selectRows); await Promise.all(selectRows);
await page.getByLabel('Acknowledge selected faults').click(); await page.locator('button:has-text("Acknowledge")').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.getByLabel('Shelve', { exact: true }).click(); await page.locator('.c-menu >> text="Shelve"').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
* @returns {Promise<void>}
*/
export async function sortFaultsBy(page, sort) {
await page.getByTitle('Sort By').getByRole('combobox').selectOption(sort);
}
/**
* @param {import('@playwright/test').Page} page
* @param {'acknowledged' | 'shelved' | 'standard view'} view
* @returns {Promise<void>}
*/ */
export async function changeViewTo(page, view) { export async function changeViewTo(page, view) {
await page.getByTitle('View Filter').getByRole('combobox').selectOption(view); await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function sortFaultsBy(page, sort) {
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function enterSearchTerm(page, term) {
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
}
/**
* @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
@ -168,37 +165,71 @@ export async function selectFaultItem(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} rowNumber */
* @returns {import('@playwright/test').Locator} export async function getHighestSeverity(page) {
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.getByLabel('Fault triggered at').nth(rowNumber - 1); const fault = page.locator(
`.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.getByLabel('Fault triggered at').filter({ const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
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
.getByLabel('Fault name', { exact: true }) .locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
.nth(rowNumber - 1)
.textContent(); .textContent();
return faultName; return faultName;
@ -206,13 +237,21 @@ export async function getFaultName(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} rowNumber */
* @returns {Promise<string>} export async function getFaultSeverity(page, rowNumber) {
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
.getByLabel('Fault namespace') .locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
.nth(rowNumber - 1)
.textContent(); .textContent();
return faultNamespace; return faultNamespace;
@ -220,13 +259,10 @@ 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
.getByLabel('Last Trigger Time') .locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`)
.nth(rowNumber - 1)
.textContent(); .textContent();
return faultTriggerTime.toString().trim(); return faultTriggerTime.toString().trim();
@ -234,14 +270,11 @@ 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('Fault triggered at') .getByLabel('Disposition actions')
.nth(rowNumber - 1) .nth(rowNumber - 1)
.getByLabel('Disposition Actions')
.click(); .click();
} }

View File

@ -1,33 +0,0 @@
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

@ -129,7 +129,6 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl
*/ */
export function getEarliestStartTime(planJson) { export function getEarliestStartTime(planJson) {
const activities = Object.values(planJson).flat(); const activities = Object.values(planJson).flat();
return Math.min(...activities.map((activity) => activity.start)); return Math.min(...activities.map((activity) => activity.start));
} }
@ -140,7 +139,6 @@ export function getEarliestStartTime(planJson) {
*/ */
export function getLatestEndTime(planJson) { export function getLatestEndTime(planJson) {
const activities = Object.values(planJson).flat(); const activities = Object.values(planJson).flat();
return Math.max(...activities.map((activity) => activity.end)); return Math.max(...activities.map((activity) => activity.end));
} }
@ -153,7 +151,6 @@ export function getFirstActivity(planJson) {
const groups = Object.keys(planJson); const groups = Object.keys(planJson);
const firstGroupKey = groups[0]; const firstGroupKey = groups[0];
const firstGroupItems = planJson[firstGroupKey]; const firstGroupItems = planJson[firstGroupKey];
return firstGroupItems[0]; return firstGroupItems[0];
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct-e2e", "name": "openmct-e2e",
"version": "4.1.0-next", "version": "4.0.0-next",
"description": "The Open MCT e2e framework", "description": "The Open MCT e2e framework",
"type": "module", "type": "module",
"module": "index.js", "module": "index.js",
@ -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.48.1", "@playwright/test": "1.45.2",
"@axe-core/playwright": "4.8.5" "@axe-core/playwright": "4.8.5"
}, },
"author": { "author": {

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -26,10 +26,8 @@ import {
createExampleTelemetryObject, createExampleTelemetryObject,
createNotification, createNotification,
createPlanFromJSON, createPlanFromJSON,
createStableStateTelemetry,
expandEntireTree, expandEntireTree,
getCanvasPixels, getCanvasPixels,
linkParameterToObject,
navigateToObjectWithFixedTimeBounds, navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime, navigateToObjectWithRealTime,
setEndOffset, setEndOffset,
@ -341,23 +339,4 @@ 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

@ -286,55 +286,6 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
}); });
}); });
test.describe('Generate Conditional Styling Data @localStorage @generatedata', () => {
test('Generate basic condition set', async ({ page, context }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a Condition Set
const conditionSet = await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Condition Set'
});
// Create a Telemetry Object (Sine Wave Generator)
const swg = await createExampleTelemetryObject(page, conditionSet.uuid);
// Edit the Telemetry Object to have a 10hz data rate (Gotta go fast!)
await page.goto(swg.url);
await page.getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page.getByLabel('Period', { exact: true }).fill('5');
await page.getByLabel('Save').click();
// Edit the Condition Set
await page.goto(conditionSet.url);
await page.getByLabel('Edit Object').click();
// Add a Condition to the Condition Set
await page.getByLabel('Add Condition').click();
await page.getByLabel('Condition Name Input').first().fill('Test Condition');
await page.getByLabel('Condition Output Type').first().selectOption('String');
await page.getByLabel('Condition Output String').first().fill('Test Condition Met');
// Condition: True if sine value > 0 (half the time)
await page.getByLabel('Criterion Telemetry Selection').selectOption(swg.name);
await page.getByLabel('Criterion Metadata Selection').selectOption('Sine');
await page.getByLabel('Criterion Comparison Selection').selectOption('is greater than');
await page.getByLabel('Criterion Input').first().fill('0');
// Rename default condition
await page.getByLabel('Condition Output String').nth(1).fill('Test Condition Unmet');
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Save localStorage for future test execution
await context.storageState({
path: fileURLToPath(
new URL('../../../e2e/test-data/condition_set_storage.json', import.meta.url)
)
});
});
});
test.describe('Validate Overlay Plot with Telemetry Object @localStorage @generatedata', () => { test.describe('Validate Overlay Plot with Telemetry Object @localStorage @generatedata', () => {
test.use({ test.use({
storageState: fileURLToPath( storageState: fileURLToPath(

View File

@ -24,13 +24,19 @@ 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,
@ -40,7 +46,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 navigateToFaultManagementWithStaticExample(page); await navigateToFaultManagementWithExample(page);
}); });
test('Shows a criticality icon for every fault', async ({ page }) => { test('Shows a criticality icon for every fault', async ({ page }) => {
@ -50,7 +56,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 its information shows in the inspector', async ({ test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({
page page
}) => { }) => {
await selectFaultItem(page, 1); await selectFaultItem(page, 1);
@ -61,7 +67,9 @@ 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(page.getByLabel('Fault triggered at').first()).toHaveClass(/is-selected/); await expect(
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);
}); });
@ -71,18 +79,23 @@ 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.getByRole('checkbox', { checked: true }); const selectedRows = page.locator(
await expect(selectedRows).toHaveCount(2); '.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname'
);
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();
await expect( const firstNameInInspectorCount = await page
page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`) .locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`)
).toHaveCount(0); .count();
await expect( const secondNameInInspectorCount = await page
page.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`) .locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
).toHaveCount(0); .count();
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 }) => {
@ -173,60 +186,44 @@ 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)
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5); let faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
// search namespace // search namespace
await page await enterSearchTerm(page, faultThreeNamespace);
.getByLabel('Fault Management Object View')
.getByLabel('Search Input')
.fill(faultThreeNamespace);
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1); faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace); expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
// all faults // all faults
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill(''); await clearSearch(page);
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5); faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
// search name // search name
await page await enterSearchTerm(page, faultTwoName);
.getByLabel('Fault Management Object View')
.getByLabel('Search Input')
.fill(faultTwoName);
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1); faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultName(page, 1)).toEqual(faultTwoName); expect(await getFaultName(page, 1)).toEqual(faultTwoName);
// all faults // all faults
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill(''); await clearSearch(page);
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5); faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
// search triggerTime // search triggerTime
await page await enterSearchTerm(page, faultFiveTriggerTime);
.getByLabel('Fault Management Object View')
.getByLabel('Search Input')
.fill(faultFiveTriggerTime);
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1); faultResultCount = await getFaultResultCount(page);
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);
* Compares two severity levels and returns a number indicating their relative order. const lowestSeverity = await getLowestSeverity(page);
*
* @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);
@ -240,19 +237,10 @@ test.describe('The Fault Management Plugin using example faults', () => {
await sortFaultsBy(page, 'severity'); await sortFaultsBy(page, 'severity');
const firstFaultSeverityLabel = await page const sortedHighestSeverity = await getFaultSeverity(page, 1);
.getByLabel('Severity:') const sortedLowestSeverity = await getFaultSeverity(page, 5);
.first() expect(sortedHighestSeverity).toEqual(highestSeverity);
.getAttribute('aria-label'); expect(sortedLowestSeverity).toEqual(lowestSeverity);
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);
}); });
}); });
@ -262,18 +250,24 @@ 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 }) => {
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0); const faultCount = await page.locator('c-fault-mgmt__list').count();
expect(faultCount).toEqual(0);
await changeViewTo(page, 'acknowledged'); await changeViewTo(page, 'acknowledged');
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0); const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
expect(acknowledgedCount).toEqual(0);
await changeViewTo(page, 'shelved'); await changeViewTo(page, 'shelved');
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0); const shelvedCount = await page.locator('c-fault-mgmt__list').count();
expect(shelvedCount).toEqual(0);
}); });
test('Will return no faults when searching', async ({ page }) => { test('Will return no faults when searching', async ({ page }) => {
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('fault'); await enterSearchTerm(page, 'fault');
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0); const faultCount = await page.locator('c-fault-mgmt__list').count();
expect(faultCount).toEqual(0);
}); });
}); });

View File

@ -30,19 +30,16 @@ import {
navigateToObjectWithRealTime, navigateToObjectWithRealTime,
setRealTimeMode setRealTimeMode
} from '../../../../appActions.js'; } from '../../../../appActions.js';
import { import { MISSION_TIME } from '../../../../constants.js';
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', () => {
@ -96,6 +93,9 @@ test.describe('Example Imagery Object', () => {
expect(newPage.url()).toContain('.jpg'); expect(newPage.url()).toContain('.jpg');
}); });
// this requires CORS to be enabled in some fashion
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
test('Can adjust image brightness/contrast by dragging the sliders', async ({ test('Can adjust image brightness/contrast by dragging the sliders', async ({
page, page,
browserName browserName
@ -357,10 +357,15 @@ test.describe('Example Imagery Object', () => {
}); });
}); });
test.describe('Example Imagery in Display Layout', () => { test.describe('Example Imagery in Display Layout @clock', () => {
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' });
@ -423,7 +428,12 @@ test.describe('Example Imagery in Display Layout', () => {
await expect.soft(pausePlayButton).toHaveClass(/is-paused/); await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
}); });
test('Imagery View operations', async ({ page }) => { test('Imagery View operations @clock', 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();
@ -516,9 +526,14 @@ test.describe('Example Imagery in Display Layout', () => {
}); });
}); });
test.describe('Example Imagery in Flexible layout', () => { test.describe('Example Imagery in Flexible layout @clock', () => {
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' });
@ -547,7 +562,7 @@ test.describe('Example Imagery in Flexible layout', () => {
await page.getByRole('button', { name: 'Close' }).click(); await page.getByRole('button', { name: 'Close' }).click();
}); });
test('Imagery View operations', async ({ page, browserName }) => { test('Imagery View operations @clock', 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',
@ -558,10 +573,14 @@ test.describe('Example Imagery in Flexible layout', () => {
}); });
}); });
test.describe('Example Imagery in Tabs View', () => { test.describe('Example Imagery in Tabs View @clock', () => {
let tabsView; let tabsView;
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' });
tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' }); tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
@ -588,8 +607,7 @@ test.describe('Example Imagery in Tabs View', () => {
// 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);
}); });
}); });
@ -650,19 +668,16 @@ 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 their timestamps fall out of bounds * 6. Old images are discarded when new images stream in
* 7. Multiple images can be discarded when their timestamps fall out of bounds * 7. Image brightness/contrast can be adjusted by dragging the sliders
* 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) {
await test.step('Verify that imagery thumbnails use a thumbnail url', async () => { // Verify that imagery thumbnails use a thumbnail url
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();
@ -721,6 +736,19 @@ 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();
@ -787,6 +815,24 @@ 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
*/ */
@ -872,66 +918,62 @@ 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) {
await test.step('Can zoom using buttons', async () => { // Lock the zoom and pan so it doesn't reset if a new image comes in
// Lock the zoom and pan so it doesn't reset if a new image comes in await page.getByLabel('Focused Image Element').hover({ trial: true });
await page.getByLabel('Focused Image Element').hover({ trial: true }); const lockButton = page.getByRole('button', {
const lockButton = page.getByRole('button', { name: 'Lock current zoom and pan across all images'
name: 'Lock current zoom and pan across all images'
});
await lockButton.isVisible();
// if (!(await lockButton.isVisible())) {
// await page.getByLabel('Focused Image Element').hover({ trial: true });
// }
await lockButton.click();
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(1) translate(0px, 0px)'
);
// Get initial image dimensions
const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
// Zoom in twice via button
await zoomIntoImageryByButton(page);
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
const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
// Zoom out once via button
await zoomOutOfImageryByButton(page);
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(2) translate(0px, 0px)'
);
// Get and assert zoomed out image dimensions
const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
// Zoom out again via button, assert against the initial image dimensions
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);
}); });
if (!(await lockButton.isVisible())) {
await page.getByLabel('Focused Image Element').hover({ trial: true });
}
await lockButton.click();
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(1) translate(0px, 0px)'
);
// Get initial image dimensions
const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
// Zoom in twice via button
await zoomIntoImageryByButton(page);
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
const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
// Zoom out once via button
await zoomOutOfImageryByButton(page);
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(2) translate(0px, 0px)'
);
// Get and assert zoomed out image dimensions
const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
// Zoom out again via button, assert against the initial image dimensions
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);
} }
/** /**
@ -993,6 +1035,24 @@ 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

@ -1,489 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
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

@ -1,93 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite verifies modifying the image location of the example imagery object.
*/
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Example Imagery Object Custom Images', () => {
let exampleImagery;
test.beforeEach(async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a default 'Example Imagery' object
exampleImagery = await createDomainObjectWithDefaults(page, {
name: 'Example Imagery',
type: 'Example Imagery'
});
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await page.getByLabel('Focused Image Element').hover({ trial: true });
// Wait for image thumbnail auto-scroll to complete
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
});
// this requires CORS to be enabled in some fashion
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
test('Can provide a custom image location for the example imagery object', async ({ page }) => {
// Modify Example Imagery to create a really stable image which will never let us down
await page.getByRole('button', { name: 'More actions' }).click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page
.locator('#imageLocation-textarea')
.fill(
'https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg,https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg'
);
await page.getByRole('button', { name: 'Save' }).click();
await page.reload({ waitUntil: 'domcontentloaded' });
// Wait for the thumbnails to finish their scroll animation
// (Wait until the rightmost thumbnail is in view)
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
});
test.fixme('Can provide a custom image with spaces in name', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7903'
});
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
// Modify Example Imagery to create a really stable image which will never let us down
await page.getByRole('button', { name: 'More actions' }).click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page
.locator('#imageLocation-textarea')
.fill(
'https://raw.githubusercontent.com/nasa/openmct/d8c64f183400afb70137221fc1a035e091bea912/e2e/test-data/rick%20space%20roll.jpg'
);
await page.getByRole('button', { name: 'Save' }).click();
await page.reload({ waitUntil: 'domcontentloaded' });
// Wait for the thumbnails to finish their scroll animation
// (Wait until the rightmost thumbnail is in view)
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
});
});

View File

@ -1,72 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { 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-2024, United States Government * Open MCT, Copyright (c) 2014-2023, 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,163 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
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,114 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { fileURLToPath } from 'url';
import {
createDomainObjectWithDefaults,
navigateToObjectWithRealTime
} from '../../../../../appActions.js';
import { expect, test } from '../../../../../pluginFixtures.js';
const TINY_IMAGE_BASE64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
test.describe('Display Layout Conditional Styling', () => {
test.use({
storageState: fileURLToPath(
new URL('../../../../../test-data/condition_set_storage.json', import.meta.url)
)
});
let displayLayout;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Test Display Layout'
});
});
test('Image Drawing Object can have visibility toggled conditionally', async ({ page }) => {
await page.getByLabel('Edit Object').click();
// Add Image Drawing Object to the layout
await page.getByLabel('Add Drawing Object').click();
await page.getByLabel('Image').click();
await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64);
await page.getByText('Ok').click();
// Use the "Test Condition Set" for conditional styling on the image
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 Test Condition Set').click();
await page.getByText('Ok').click();
// Set the image to be hidden when the condition is met
await page.getByTitle('Visible').first().click();
await page.getByLabel('Save Style').first().click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Switch to real-time mode and verify that the image toggles visibility
await navigateToObjectWithRealTime(page, displayLayout.url);
await expect(page.getByLabel('Image View')).toBeVisible();
await expect(page.getByLabel('Image View')).toBeHidden();
// Reload the page and verify that the image toggles visibility
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page.getByLabel('Image View')).toBeVisible();
await expect(page.getByLabel('Image View')).toBeHidden();
});
test('Alphanumeric object can have visibility toggled conditionally', async ({ page }) => {
await page.getByLabel('Edit Object').click();
// Add Alphanumeric Object to the layout
await page.getByLabel('Expand My Items folder').click();
await page.getByLabel('Expand Test Condition Set').click();
await page.getByLabel('Preview VIPER Rover Heading').dragTo(page.getByLabel('Layout Grid'));
// Use the "Test Condition Set" for conditional styling on the alphanumeric
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 Test Condition Set').click();
await page.getByText('Ok').click();
// Set the alphanumeric to be hidden when the condition is met
await page.getByTitle('Visible').first().click();
await page.getByLabel('Save Style').first().click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Switch to real-time mode and verify that the image toggles visibility
await navigateToObjectWithRealTime(page, displayLayout.url);
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeVisible();
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeHidden();
// Reload the page and verify that the alphanumeric toggles visibility
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeVisible();
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeHidden();
});
});

View File

@ -57,7 +57,7 @@ test.describe('Tabs View', () => {
await page.goto(tabsView.url); await page.goto(tabsView.url);
// select first tab // select first tab
await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click(); await page.getByLabel(`${table.name} tab`, { exact: true }).click();
// ensure table header visible // ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
@ -92,38 +92,6 @@ test.describe('Tabs View', () => {
// no canvas (i.e., sine wave generator) in the document should be visible // no canvas (i.e., sine wave generator) in the document should be visible
await expect(page.locator('canvas[id=webglContext]')).toBeHidden(); await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
}); });
test('Changing the displayed tab should not be persisted if the view is locked', async ({
page
}) => {
await page.goto(tabsView.url);
//lock the view
await page.getByLabel('Unlocked for editing, click to lock.', { exact: true }).click();
// get the initial tab index
const initialTab = page.getByLabel(/- selected/);
// switch to a different tab in the view
const swgTab = page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true });
await swgTab.click();
await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();
// navigate away from the tabbed view and back
await page.getByRole('treeitem', { name: 'My Items' }).click();
await page.goto(tabsView.url);
// check that the initial tab is displayed
const lockedSelectedTab = page.getByLabel(/- selected/);
await expect(lockedSelectedTab).toHaveText(await initialTab.textContent());
//unlock the view
await page.getByLabel('Locked for editing. Click to unlock.', { exact: true }).click();
// switch to a different tab in the view
await swgTab.click();
await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();
// navigate away from the tabbed view and back
await page.getByRole('treeitem', { name: 'My Items' }).click();
await page.goto(tabsView.url);
// check that the newly selected tab is displayed
const unlockedSelectedTab = page.getByLabel(/- selected/);
await expect(unlockedSelectedTab).toBeVisible();
});
}); });
test.describe('Tabs View CRUD', () => { test.describe('Tabs View CRUD', () => {

View File

@ -117,8 +117,7 @@ test.describe('Telemetry Table', () => {
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5); endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);
const endDate = endTimeStamp.toISOString().split('T')[0]; const endDate = endTimeStamp.toISOString().split('T')[0];
const milliseconds = endTimeStamp.getMilliseconds(); const endTime = endTimeStamp.toISOString().split('T')[1];
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
await setTimeConductorBounds(page, { endDate, endTime }); await setTimeConductorBounds(page, { endDate, endTime });

View File

@ -24,210 +24,65 @@ import {
setEndOffset, setEndOffset,
setFixedTimeMode, setFixedTimeMode,
setRealTimeMode, setRealTimeMode,
setStartOffset setStartOffset,
setTimeConductorBounds
} from '../../../../appActions.js'; } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Time conductor operations', () => { test.describe('Time conductor operations', () => {
const DAY = '2024-01-01'; test('validate start time does not exceed end time', async ({ page }) => {
const DAY_AFTER = '2024-01-02';
const ONE_O_CLOCK = '01:00:00';
const TWO_O_CLOCK = '02:00:00';
test.beforeEach(async ({ page }) => {
// Go to baseURL // Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
}); const year = new Date().getFullYear();
test('validate date and time inputs are validated on input event', async ({ page }) => { // Set initial valid time bounds
const submitButtonLocator = page.getByLabel('Submit time bounds'); const startDate = `${year}-01-01`;
const startTime = '01:00:00';
const endDate = `${year}-01-01`;
const endTime = '02:00:00';
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });
// Open the time conductor popup // Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await test.step('invalid start date disables submit button', async () => { // Test invalid start date
const initialStartDate = await page.getByLabel('Start date').inputValue(); const invalidStartDate = `${year}-01-02`;
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`; await page.getByLabel('Start date').fill(invalidStartDate);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('Start date').fill(startDate);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
await page.getByLabel('Start date').fill(invalidStartDate); // Test invalid end date
await expect(submitButtonLocator).toBeDisabled(); const invalidEndDate = `${year - 1}-12-31`;
await page.getByLabel('Start date').fill(initialStartDate); await page.getByLabel('End date').fill(invalidEndDate);
await expect(submitButtonLocator).toBeEnabled(); await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
}); await page.getByLabel('End date').fill(endDate);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
await test.step('invalid start time disables submit button', async () => { // Test invalid start time
const initialStartTime = await page.getByLabel('Start time').inputValue(); const invalidStartTime = '42:00:00';
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`; await page.getByLabel('Start time').fill(invalidStartTime);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('Start time').fill(startTime);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
await page.getByLabel('Start time').fill(invalidStartTime); // Test invalid end time
await expect(submitButtonLocator).toBeDisabled(); const invalidEndTime = '43:00:00';
await page.getByLabel('Start time').fill(initialStartTime); await page.getByLabel('End time').fill(invalidEndTime);
await expect(submitButtonLocator).toBeEnabled(); await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
}); await page.getByLabel('End time').fill(endTime);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
await test.step('disable/enable submit button also works with multiple invalid inputs', async () => { // Submit valid time bounds
const initialEndDate = await page.getByLabel('End date').inputValue();
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
const initialStartTime = await page.getByLabel('Start time').inputValue();
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
await page.getByLabel('Start time').fill(invalidStartTime);
await expect(submitButtonLocator).toBeDisabled();
await page.getByLabel('End date').fill(invalidEndDate);
await expect(submitButtonLocator).toBeDisabled();
await page.getByLabel('End date').fill(initialEndDate);
await expect(submitButtonLocator).toBeDisabled();
await page.getByLabel('Start time').fill(initialStartTime);
await expect(submitButtonLocator).toBeEnabled();
});
});
test('validate date and time inputs validation is reported on change event', async ({ page }) => {
// Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await test.step('invalid start date is reported on change event, not on input event', async () => {
const initialStartDate = await page.getByLabel('Start date').inputValue();
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
await page.getByLabel('Start date').fill(invalidStartDate);
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
await page.getByLabel('Start date').press('Tab');
await expect(page.getByLabel('Start date')).toHaveAttribute('title', 'Invalid Date');
await page.getByLabel('Start date').fill(initialStartDate);
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
});
await test.step('invalid start time is reported on change event, not on input event', async () => {
const initialStartTime = await page.getByLabel('Start time').inputValue();
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
await page.getByLabel('Start time').fill(invalidStartTime);
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
await page.getByLabel('Start time').press('Tab');
await expect(page.getByLabel('Start time')).toHaveAttribute('title', 'Invalid Time');
await page.getByLabel('Start time').fill(initialStartTime);
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
});
await test.step('invalid end date is reported on change event, not on input event', async () => {
const initialEndDate = await page.getByLabel('End date').inputValue();
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
await page.getByLabel('End date').fill(invalidEndDate);
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
await page.getByLabel('End date').press('Tab');
await expect(page.getByLabel('End date')).toHaveAttribute('title', 'Invalid Date');
await page.getByLabel('End date').fill(initialEndDate);
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
});
await test.step('invalid end time is reported on change event, not on input event', async () => {
const initialEndTime = await page.getByLabel('End time').inputValue();
const invalidEndTime = `${initialEndTime.substring(0, 5)}${initialEndTime.substring(6)}`;
await page.getByLabel('End time').fill(invalidEndTime);
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
await page.getByLabel('End time').press('Tab');
await expect(page.getByLabel('End time')).toHaveAttribute('title', 'Invalid Time');
await page.getByLabel('End time').fill(initialEndTime);
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
});
});
test('validate start time does not exceed end time on submit', async ({ page }) => {
// Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
// FIXME: https://github.com/nasa/openmct/pull/7818
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
await page.getByLabel('Start date').fill(DAY);
await page.getByLabel('Start time').fill(TWO_O_CLOCK);
await page.getByLabel('End date').fill(DAY);
await page.getByLabel('End time').fill(ONE_O_CLOCK);
await page.getByLabel('Submit time bounds').click(); await page.getByLabel('Submit time bounds').click();
await expect(page.getByLabel('Start date')).toHaveAttribute( // Verify the submitted time bounds
'title', await expect(page.getByLabel('Start bounds')).toHaveText(
'Specified start date exceeds end bound' new RegExp(`${startDate} ${startTime}.000Z`)
); );
await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`); await expect(page.getByLabel('End bounds')).toHaveText(
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`); new RegExp(`${endDate} ${endTime}.000Z`)
await page.getByLabel('Start date').fill(DAY);
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
await page.getByLabel('End date').fill(DAY);
await page.getByLabel('End time').fill(TWO_O_CLOCK);
await page.getByLabel('Submit time bounds').click();
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);
});
test('validate start datetime does not exceed end datetime on submit', async ({ page }) => {
// Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
// FIXME: https://github.com/nasa/openmct/pull/7818
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
await page.getByLabel('Start date').fill(DAY_AFTER);
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
await page.getByLabel('End date').fill(DAY);
await page.getByLabel('End time').fill(ONE_O_CLOCK);
await page.getByLabel('Submit time bounds').click();
await expect(page.getByLabel('Start date')).toHaveAttribute(
'title',
'Specified start date exceeds end bound'
); );
await expect(page.getByLabel('Start bounds')).not.toHaveText(
`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`
);
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await page.getByLabel('Start date').fill(DAY);
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
await page.getByLabel('End date').fill(DAY_AFTER);
await page.getByLabel('End time').fill(ONE_O_CLOCK);
await page.getByLabel('Submit time bounds').click();
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
});
test('cancelling form does not set bounds', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7791'
});
// Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await page.getByLabel('Start date').fill(DAY);
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
await page.getByLabel('End date').fill(DAY_AFTER);
await page.getByLabel('End time').fill(ONE_O_CLOCK);
await page.getByLabel('Discard changes and close time popup').click();
await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
// Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await page.getByLabel('Start date').fill(DAY);
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
await page.getByLabel('End date').fill(DAY_AFTER);
await page.getByLabel('End time').fill(ONE_O_CLOCK);
await page.getByLabel('Submit time bounds').click();
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
}); });
}); });
@ -276,6 +131,77 @@ test.describe('Global Time Conductor', () => {
await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible(); await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible();
}); });
test('Input field validation: fixed time mode', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7791'
});
// Switch to fixed time mode
await setFixedTimeMode(page);
// Define valid time bounds for testing
const validBounds = {
startDate: '2024-04-20',
startTime: '00:04:20',
endDate: '2024-04-20',
endTime: '16:04:20'
};
// Set valid time conductor bounds ✌️
await setTimeConductorBounds(page, validBounds);
// Verify that the time bounds are set correctly
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
// Open the Time Conductor Mode popup
await page.getByLabel('Time Conductor Mode').click();
// Test invalid start date
const invalidStartDate = '2024-04-21';
await page.getByLabel('Start date').fill(invalidStartDate);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('Start date').fill(validBounds.startDate);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
// Test invalid end date
const invalidEndDate = '2024-04-19';
await page.getByLabel('End date').fill(invalidEndDate);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('End date').fill(validBounds.endDate);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
// Test invalid start time
const invalidStartTime = '16:04:21';
await page.getByLabel('Start time').fill(invalidStartTime);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('Start time').fill(validBounds.startTime);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
// Test invalid end time
const invalidEndTime = '00:04:19';
await page.getByLabel('End time').fill(invalidEndTime);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('End time').fill(validBounds.endTime);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
// Verify that the time bounds remain unchanged after invalid inputs
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
// Discard changes and verify that bounds remain unchanged
await setTimeConductorBounds(page, {
startDate: validBounds.startDate,
startTime: '04:20:00',
endDate: validBounds.endDate,
endTime: '04:20:20',
submitChanges: false
});
// Verify that the original time bounds are still displayed after discarding changes
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
});
/** /**
* Verify that offsets and url params are preserved when switching * Verify that offsets and url params are preserved when switching
* between fixed timespan and real-time mode. * between fixed timespan and real-time mode.

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government * Open MCT, Copyright (c) 2014-2023, 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-2024, United States Government * Open MCT, Copyright (c) 2014-2023, 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

@ -64,7 +64,7 @@ test.describe('Tabs View', () => {
page.goto(tabsView.url); page.goto(tabsView.url);
// select first tab // select first tab
await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click(); await page.getByLabel(`${table.name} tab`, { exact: true }).click();
// ensure table header visible // ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();

View File

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

View File

@ -1,81 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import 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, { waitUntil: 'networkidle' }); await page.goto(notebook.url);
await page await page
.getByLabel('Navigate to Dropped Overlay Plot') .getByLabel('Navigate to Dropped Overlay Plot')

View File

@ -26,25 +26,14 @@ import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js'; import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js'; import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js'; import { VISUAL_FIXED_URL } from '../../constants.js';
import { import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
getFirstActivity,
setBoundsToSpanAllActivities,
setDraftStatusForPlan
} from '../../helper/planningUtils.js';
const examplePlanSmall2 = JSON.parse( const examplePlanSmall2 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url)) fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
); );
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
test.describe('Visual - Gantt Chart @a11y', () => { test.describe('Visual - Gantt Chart @a11y', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Set the clock to the end of the first activity in the plan
// This is so we can see the "now" line in the plan view
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
await page.clock.resume();
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
}); });
test('Gantt Chart View', async ({ page, theme }) => { test('Gantt Chart View', async ({ page, theme }) => {

View File

@ -27,21 +27,14 @@ import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appAct
import { scanForA11yViolations, test } from '../../avpFixtures.js'; import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { waitForAnimations } from '../../baseFixtures.js'; import { waitForAnimations } from '../../baseFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js'; import { VISUAL_FIXED_URL } from '../../constants.js';
import { getFirstActivity, setBoundsToSpanAllActivities } from '../../helper/planningUtils.js'; import { setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
const examplePlanSmall2 = JSON.parse( const examplePlanSmall2 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url)) fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
); );
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
test.describe('Visual - Time Strip @a11y', () => { test.describe('Visual - Time Strip @a11y', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Set the clock to the end of the first activity in the plan
// This is so we can see the "now" line in the plan view
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
await page.clock.resume();
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
}); });
test('Time Strip View', async ({ page, theme }) => { test('Time Strip View', async ({ page, theme }) => {

View File

@ -42,7 +42,6 @@ const examplePlanSmall2 = JSON.parse(
); );
const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1); const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
test.describe('Visual - Timelist progress bar @clock @a11y', () => { test.describe('Visual - Timelist progress bar @clock @a11y', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@ -60,11 +59,6 @@ test.describe('Visual - Timelist progress bar @clock @a11y', () => {
test.describe('Visual - Plan View @a11y', () => { test.describe('Visual - Plan View @a11y', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Set the clock to the end of the first activity in the plan
// This is so we can see the "now" line in the plan view
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
await page.clock.resume();
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
}); });

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, { waitUntil: 'networkidle' }); await page.goto(displayLayout.url);
// 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,7 +20,6 @@
* 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) {
@ -57,9 +56,6 @@ export default function (staticFaults = false) {
return Promise.resolve({ return Promise.resolve({
success: true success: true
}); });
},
getShelveDurations() {
return DEFAULT_SHELVE_DURATIONS;
} }
}); });
}; };

View File

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

View File

@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { seededRandom } from 'utils/random.js';
const DEFAULT_IMAGE_SAMPLES = [ const DEFAULT_IMAGE_SAMPLES = [
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg',
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18732.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18732.jpg',
@ -164,8 +162,8 @@ export default function () {
}; };
} }
function getCompassValues(min, max, timestamp) { function getCompassValues(min, max) {
return min + seededRandom(timestamp) * (max - min); return min + Math.random() * (max - min);
} }
function getImageSamples(configuration) { function getImageSamples(configuration) {
@ -285,9 +283,9 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
utc: Math.floor(timestamp / delay) * delay, utc: Math.floor(timestamp / delay) * delay,
local: Math.floor(timestamp / delay) * delay, local: Math.floor(timestamp / delay) * delay,
url, url,
sunOrientation: getCompassValues(0, 360, timestamp), sunOrientation: getCompassValues(0, 360),
cameraAzimuth: getCompassValues(0, 360, timestamp), cameraAzimuth: getCompassValues(0, 360),
heading: getCompassValues(0, 360, timestamp), heading: getCompassValues(0, 360),
transformations: navCamTransformations, transformations: navCamTransformations,
imageDownloadName imageDownloadName
}; };

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government * Open MCT, Copyright (c) 2014-2023, 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.
* *

291
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "openmct", "name": "openmct",
"version": "4.1.0-next", "version": "4.0.0-next",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "openmct", "name": "openmct",
"version": "4.1.0-next", "version": "4.0.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"e2e" "e2e"
@ -24,6 +24,7 @@
"@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",
@ -66,7 +67,6 @@
"moment": "2.30.1", "moment": "2.30.1",
"moment-duration-format": "2.3.2", "moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41", "moment-timezone": "0.5.41",
"nano": "10.1.4",
"npm-run-all2": "6.1.2", "npm-run-all2": "6.1.2",
"nyc": "15.1.0", "nyc": "15.1.0",
"painterro": "1.2.87", "painterro": "1.2.87",
@ -93,18 +93,18 @@
"webpack-merge": "5.10.0" "webpack-merge": "5.10.0"
}, },
"engines": { "engines": {
"node": ">=18.14.2 <23" "node": ">=18.14.2 <22"
} }
}, },
"e2e": { "e2e": {
"name": "openmct-e2e", "name": "openmct-e2e",
"version": "4.1.0-next", "version": "4.0.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"@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.48.1" "@playwright/test": "1.45.2"
} }
}, },
"e2e/node_modules/@percy/cli": { "e2e/node_modules/@percy/cli": {
@ -1548,13 +1548,12 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.48.1", "version": "1.45.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz",
"integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==", "integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.48.1" "playwright": "1.45.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -1587,6 +1586,15 @@
"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",
@ -2300,6 +2308,18 @@
"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",
@ -2434,6 +2454,16 @@
"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",
@ -2464,12 +2494,6 @@
"@mdn/browser-compat-data": "^5.2.34" "@mdn/browser-compat-data": "^5.2.34"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/axe-core": { "node_modules/axe-core": {
"version": "4.8.4", "version": "4.8.4",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.4.tgz", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.4.tgz",
@ -2479,17 +2503,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-loader": { "node_modules/babel-loader": {
"version": "9.1.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.0.tgz", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.0.tgz",
@ -2784,9 +2797,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001660", "version": "1.0.30001597",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
"integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2801,8 +2814,7 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ]
"license": "CC-BY-4.0"
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "2.4.2", "version": "2.4.2",
@ -3000,6 +3012,26 @@
"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",
@ -3030,18 +3062,6 @@
"node": ">=0.1.90" "node": ">=0.1.90"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/comma-separated-values": { "node_modules/comma-separated-values": {
"version": "3.6.4", "version": "3.6.4",
"resolved": "https://registry.npmjs.org/comma-separated-values/-/comma-separated-values-3.6.4.tgz", "resolved": "https://registry.npmjs.org/comma-separated-values/-/comma-separated-values-3.6.4.tgz",
@ -4132,15 +4152,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -5570,6 +5581,21 @@
"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",
@ -5794,20 +5820,6 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -6374,6 +6386,20 @@
"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",
@ -6404,6 +6430,19 @@
"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",
@ -6446,6 +6485,15 @@
"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",
@ -7949,35 +7997,6 @@
"multicast-dns": "cli.js" "multicast-dns": "cli.js"
} }
}, },
"node_modules/nano": {
"version": "10.1.4",
"resolved": "https://registry.npmjs.org/nano/-/nano-10.1.4.tgz",
"integrity": "sha512-bJOFIPLExIbF6mljnfExXX9Cub4W0puhDjVMp+qV40xl/DBvgKao7St4+6/GB6EoHZap7eFnrnx4mnp5KYgwJA==",
"dev": true,
"dependencies": {
"axios": "^1.7.4",
"node-abort-controller": "^3.1.1",
"qs": "^6.13.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/nano/node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dev": true,
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@ -8017,12 +8036,6 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true "dev": true
}, },
"node_modules/node-abort-controller": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
"dev": true
},
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@ -8750,13 +8763,12 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.48.1", "version": "1.45.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz",
"integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==", "integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.48.1" "playwright-core": "1.45.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8769,11 +8781,10 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.48.1", "version": "1.45.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz",
"integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==", "integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },
@ -8787,7 +8798,6 @@
"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"
@ -9273,12 +9283,6 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@ -10341,6 +10345,15 @@
"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",
@ -10523,6 +10536,12 @@
"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",
@ -10588,6 +10607,31 @@
"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",
@ -10968,6 +11012,15 @@
"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

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "4.1.0-next", "version": "4.0.0",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"module": "dist/openmct.js", "module": "dist/openmct.js",
"main": "dist/openmct.js", "main": "dist/openmct.js",
@ -27,6 +27,7 @@
"@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",
@ -69,7 +70,6 @@
"moment": "2.30.1", "moment": "2.30.1",
"moment-duration-format": "2.3.2", "moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41", "moment-timezone": "0.5.41",
"nano": "10.1.4",
"npm-run-all2": "6.1.2", "npm-run-all2": "6.1.2",
"nyc": "15.1.0", "nyc": "15.1.0",
"painterro": "1.2.87", "painterro": "1.2.87",
@ -128,9 +128,12 @@
"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\\-2024/gm' ./src/ui/layout/AboutDialog.vue", "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/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",
@ -139,7 +142,7 @@
"url": "git+https://github.com/nasa/openmct.git" "url": "git+https://github.com/nasa/openmct.git"
}, },
"engines": { "engines": {
"node": ">=18.14.2 <23" "node": ">=18.14.2 <22"
}, },
"browserslist": [ "browserslist": [
"Firefox ESR", "Firefox ESR",

View File

@ -20,32 +20,6 @@
* 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: 'Unlimited',
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
@ -55,20 +29,14 @@ export default class FaultManagementAPI {
} }
/** /**
* Sets the provider for the Fault Management API. * @param {*} provider
* 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;
} }
/** /**
* Checks if the current provider supports fault management actions. * @returns {boolean}
* 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 (
@ -77,82 +45,48 @@ export default class FaultManagementAPI {
} }
/** /**
* Requests fault data for a given domain object. * @param {import('openmct').DomainObject} domainObject
* This method checks if the current provider supports the request operation for the given domain object. * @returns {Promise.<FaultAPIResponse[]>}
* 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('Provider does not support request operation'); return Promise.reject();
} }
return this.provider.request(domainObject); return this.provider.request(domainObject);
} }
/** /**
* Subscribes to fault data updates for a given domain object. * @param {import('openmct').DomainObject} domainObject
* This method checks if the current provider supports the subscribe operation for the given domain object. * @param {Function} callback
* If supported, it delegates the subscription to the provider's subscribe method. * @returns {Function} unsubscribe
* 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('Provider does not support subscribe operation'); return Promise.reject();
} }
return this.provider.subscribe(domainObject, callback); return this.provider.subscribe(domainObject, callback);
} }
/** /**
* Acknowledges a fault using the provider's acknowledgeFault method. * @param {Fault} fault
* * @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);
} }
/** /**
* Shelves a fault using the provider's shelveFault method. * @param {Fault} fault
* * @param {*} shelveData
* @param {Fault} fault - The fault object to be shelved. * @returns {Promise.<T>}
* @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[] | undefined}
*/
getShelveDurations() {
if (!this.provider) {
return;
}
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 unlimited
*/
/** /**
* @typedef {Object} TriggerValueInfo * @typedef {Object} TriggerValueInfo
* @property {number} value * @property {number} value

View File

@ -20,46 +20,25 @@
* 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';
/** /**
* @typedef RequestIdleCallbackOptions * Describes the strategy to be used when batching WebSocket messages
* @prop {Number} timeout If the number of milliseconds represented by this *
* parameter has elapsed and the callback has not already been called, invoke * @typedef BatchingStrategy
* the callback. * @property {Function} shouldBatchMessage a function that accepts a single
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback * argument - the raw message received from the websocket. Every message
* 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.
*/ */
/** /**
* Mocks requestIdleCallback for Safari using setTimeout. Functionality will be * Provides a reliable and convenient WebSocket abstraction layer that handles
* identical to setTimeout in Safari, which is to fire the callback function * a lot of boilerplate common to managing WebSocket connections such as:
* 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
@ -70,19 +49,22 @@ const ONE_SECOND = 1000;
* 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;
#maxBufferSize; #maxBatchSize;
#throttleRate; #applicationIsInitializing;
#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.
@ -92,8 +74,9 @@ 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.#maxBufferSize = Number.POSITIVE_INFINITY; this.#maxBatchSize = Number.POSITIVE_INFINITY;
this.#throttleRate = ONE_SECOND; this.#maxBatchWait = ONE_SECOND;
this.#applicationIsInitializing = true;
this.#firstBatchReceived = false; this.#firstBatchReceived = false;
const routeMessageToHandler = this.#routeMessageToHandler.bind(this); const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
@ -106,6 +89,20 @@ 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
}
);
});
} }
/** /**
@ -140,48 +137,57 @@ class BatchingWebSocket extends EventTarget {
} }
/** /**
* @param {number} maxBufferSize the maximum length of the receive buffer in characters. * Set the strategy used to both decide which raw messages to batch, and how to group
* 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 typical usage * This should be set appropriately for the expected data rate. eg. If telemetry
* sees 2000 messages arriving at a client per second, with an average message size * is received at 10Hz for each telemetry point, then a minimal combination of batch
* of 500 bytes, then 2000 * 500 = 1000000 characters will be right on the limit. * size and rate is 10 and 1000 respectively. Ideally you would add some margin, so
* In this scenario, a buffer size of 1500000 character might be more appropriate * 15 would probably be a better batch size.
* 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.
*/ */
setMaxBufferSize(maxBatchSize) { setMaxBatchSize(maxBatchSize) {
this.#maxBufferSize = maxBatchSize; this.#maxBatchSize = maxBatchSize;
this.#sendMaxBufferSizeToWorker(this.#maxBufferSize); if (!this.#applicationIsInitializing) {
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
}
} }
setThrottleRate(throttleRate) { setMaxBatchWait(wait) {
this.#throttleRate = throttleRate; this.#maxBatchWait = wait;
this.#sendThrottleRateToWorker(this.#throttleRate); this.#sendBatchWaitToWorker(this.#maxBatchWait);
} }
setThrottleMessagePattern(throttleMessagePattern) { #sendMaxBatchSizeToWorker(maxBatchSize) {
this.#worker.postMessage({ this.#worker.postMessage({
type: 'setThrottleMessagePattern', type: 'setMaxBatchSize',
throttleMessagePattern maxBatchSize
}); });
} }
#sendMaxBufferSizeToWorker(maxBufferSize) { #sendBatchWaitToWorker(maxBatchWait) {
this.#worker.postMessage({ this.#worker.postMessage({
type: 'setMaxBufferSize', type: 'setMaxBatchWait',
maxBufferSize maxBatchWait
});
}
#sendThrottleRateToWorker(throttleRate) {
this.#worker.postMessage({
type: 'setThrottleRate',
throttleRate
}); });
} }
@ -197,38 +203,9 @@ class BatchingWebSocket extends EventTarget {
#routeMessageToHandler(message) { #routeMessageToHandler(message) {
if (message.data.type === 'batch') { if (message.data.type === 'batch') {
const batch = message.data.batch;
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(); this.start = Date.now();
const dropped = message.data.dropped; const batch = message.data.batch;
if (dropped === true && !this.#showingRateLimitNotification) { if (batch.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.' }
@ -263,16 +240,18 @@ 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 > this.#throttleRate) { if (waitedFor > ONE_SECOND) {
console.warn(`Warning, batch processing took ${waitedFor}ms`); console.warn(`Warning, batch processing took ${waitedFor}ms`);
} }
this.#readyForNextBatch(); this.#readyForNextBatch();
} }
}, },
{ timeout: this.#throttleRate } { timeout: ONE_SECOND }
); );
} }
} }

View File

@ -231,20 +231,26 @@ 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, 'timeContext')) {
options.timeContext = this.openmct.time;
}
if (!Object.hasOwn(options, 'domain')) {
options.domain = options.timeContext.getTimeSystem().key;
}
if (!Object.hasOwn(options, 'start')) { if (!Object.hasOwn(options, 'start')) {
options.start = options.timeContext.getBounds().start; const bounds = options.timeContext?.getBounds();
if (bounds?.start) {
options.start = options.timeContext.getBounds().start;
} else {
options.start = this.openmct.time.getBounds().start;
}
} }
if (!Object.hasOwn(options, 'end')) { if (!Object.hasOwn(options, 'end')) {
options.end = options.timeContext.getBounds().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')) {
options.domain = this.openmct.time.getTimeSystem().key;
} }
return options; return options;

View File

@ -269,40 +269,36 @@ describe('Telemetry API', () => {
await telemetryAPI.request(domainObject); await telemetryAPI.request(domainObject);
const { signal } = new AbortController(); const { signal } = new AbortController();
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system', domain: 'system'
timeContext: openmct.time
}); });
expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, { expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
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(domainObject, { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system', domain: 'system'
timeContext: openmct.time
}); });
expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, { expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system', domain: 'system'
timeContext: openmct.time
}); });
}); });
@ -317,20 +313,18 @@ describe('Telemetry API', () => {
domain: 'someDomain' domain: 'someDomain'
}); });
const { signal } = new AbortController(); const { signal } = new AbortController();
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), {
start: 20, start: 20,
end: 30, end: 30,
domain: 'someDomain', domain: 'someDomain',
signal, signal
timeContext: openmct.time
}); });
expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, { expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
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,6 +62,9 @@ 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;
@ -81,9 +84,6 @@ 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,7 +116,8 @@ export default class TelemetryCollection extends EventEmitter {
} }
/** /**
* @returns {Array} All bounded telemetry * This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/ */
getAll() { getAll() {
return this.boundedTelemetry; return this.boundedTelemetry;
@ -127,7 +128,7 @@ export default class TelemetryCollection extends EventEmitter {
* @private * @private
*/ */
async _requestHistoricalTelemetry() { async _requestHistoricalTelemetry() {
const options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options }); let options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });
const historicalProvider = this.openmct.telemetry.findRequestProvider( const historicalProvider = this.openmct.telemetry.findRequestProvider(
this.domainObject, this.domainObject,
options options

View File

@ -24,6 +24,10 @@ 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.
@ -211,17 +215,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 'setMaxBufferSize': case 'setMaxBatchSize':
this.#messageBatcher.setMaxBufferSize(message.data.maxBufferSize); this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
break; break;
case 'setThrottleRate': case 'setMaxBatchWait':
this.#messageBatcher.setThrottleRate(message.data.throttleRate); this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait);
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}`);
@ -234,69 +238,122 @@ 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);
}
} }
/** /**
* Responsible for buffering messages * Received messages from the WebSocket, and passes them along to the
* Worker interface and back to the main thread.
*/ */
class MessageBuffer { class WebSocketToWorkerMessageBroker {
#buffer; #worker;
#currentBufferLength; #messageBatcher;
#dropped;
#maxBufferSize; constructor(messageBatcher, worker) {
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.#maxBufferSize = Number.POSITIVE_INFINITY; this.#maxBatchSize = Number.POSITIVE_INFINITY;
this.#readyForNextBatch = false; this.#readyForNextBatch = false;
this.#worker = worker; this.#worker = worker;
this.#resetBatch(); this.#resetBatch();
this.setThrottleRate(ONE_SECOND); this.setMaxBatchWait(ONE_SECOND);
} }
#resetBatch() { #resetBatch() {
//this.#batch = {}; this.#batch = {};
this.#buffer = []; this.#hasBatch = false;
this.#currentBufferLength = 0;
this.#dropped = false;
} }
/**
addMessageToBuffer(message) { * @param {BatchingStrategy} strategy
this.#buffer.push(message); */
this.#currentBufferLength += message.length; setBatchingStrategy(strategy) {
this.#batchingStrategy = strategy;
for ( }
let i = 0; /**
this.#currentBufferLength > this.#maxBufferSize && i < this.#buffer.length; * Applies the `shouldBatchMessage` function from the supplied batching strategy
i++ * to each message to determine if it should be added to a batch. If not batched,
) { * the message is immediately sent over the worker to the main thread.
const messageToConsider = this.#buffer[i]; * @param {any} message the message received from the WebSocket. See the WebSocket
if (this.#shouldThrottle(messageToConsider)) { * documentation for more details -
this.#buffer.splice(i, 1); * https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
this.#currentBufferLength -= messageToConsider.length; * @returns
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) {
#shouldThrottle(message) { this.#maxBatchSize = maxBatchSize;
return (
this.#throttleMessagePattern !== undefined && this.#throttleMessagePattern.test(message)
);
} }
setMaxBatchWait(maxBatchWait) {
setMaxBufferSize(maxBufferSize) { this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), maxBatchWait);
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
@ -305,33 +362,21 @@ export default function installWorker() {
* any new data is available. * any new data is available.
*/ */
readyForNextBatch() { readyForNextBatch() {
if (this.#hasData()) { if (this.#hasBatch) {
this.#throttledSendNextBatch(); this.#throttledSendNextBatch();
} else { } else {
this.#readyForNextBatch = true; this.#readyForNextBatch = true;
} }
} }
#sendNextBatch() { #sendNextBatch() {
const buffer = this.#buffer; const batch = this.#batch;
const dropped = this.#dropped;
const currentBufferLength = this.#currentBufferLength;
this.#resetBatch(); this.#resetBatch();
this.#worker.postMessage({ this.#worker.postMessage({
type: 'batch', type: 'batch',
dropped, batch
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');
} }
} }
@ -363,14 +408,15 @@ export default function installWorker() {
} }
const websocket = new ResilientWebSocket(self); const websocket = new ResilientWebSocket(self);
const messageBuffer = new MessageBuffer(self); const messageBatcher = new MessageBatcher(self);
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBuffer); const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
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) => {
messageBuffer.addMessageToBuffer(data); websocketBroker.routeMessageToHandler(data);
}); });
self.websocketInstance = websocket; self.websocketInstance = websocket;

View File

@ -257,9 +257,7 @@ export default {
return { return {
end, end,
start, start
size: 1,
strategy: 'latest'
}; };
}, },
loadComposition() { loadComposition() {
@ -332,11 +330,7 @@ export default {
this.domainObject.configuration.axes.xKey === undefined || this.domainObject.configuration.axes.xKey === undefined ||
this.domainObject.configuration.axes.yKey === undefined this.domainObject.configuration.axes.yKey === undefined
) { ) {
const { xKey, yKey } = this.identifyAxesKeys(axisMetadata); return;
this.openmct.objects.mutate(this.domainObject, 'configuration.axes', {
xKey,
yKey
});
} }
let xValues = []; let xValues = [];
@ -435,30 +429,6 @@ export default {
subscribeToAll() { subscribeToAll() {
const telemetryObjects = Object.values(this.telemetryObjects); const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.subscribeToObject); telemetryObjects.forEach(this.subscribeToObject);
},
identifyAxesKeys(metadata) {
const { xAxisMetadata, yAxisMetadata } = metadata;
let xKey;
let yKey;
// If xAxisMetadata contains array values, use the first one for xKey
const arrayValues = xAxisMetadata.filter((metaDatum) => metaDatum.isArrayValue);
const nonArrayValues = xAxisMetadata.filter((metaDatum) => !metaDatum.isArrayValue);
if (arrayValues.length > 0) {
xKey = arrayValues[0].key;
yKey = arrayValues.length > 1 ? arrayValues[1].key : yAxisMetadata.key;
} else if (nonArrayValues.length > 0) {
xKey = nonArrayValues[0].key;
yKey = 'none';
} else {
// Fallback if no valid xKey or yKey is found
xKey = 'none';
yKey = 'none';
}
return { xKey, yKey };
} }
} }
}; };

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.telemetryCollections = {}; this.subscriptions = {};
this.telemetryObjects = {}; this.telemetryObjects = {};
this.testData = { this.testData = {
conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData, conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData,
@ -48,46 +48,55 @@ export default class ConditionManager extends EventEmitter {
this.initialize(); this.initialize();
} }
subscribeToTelemetry(telemetryObject) { async requestLatestValue(endpoint) {
const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); const options = {
if (this.telemetryCollections[keyString]) {
return;
}
const requestOptions = {
size: 1, size: 1,
strategy: 'latest' strategy: 'latest'
}; };
const latestData = await this.openmct.telemetry.request(endpoint, options);
this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection( if (!latestData) {
telemetryObject, throw new Error('Telemetry request failed by returning a falsy response');
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 keyString = this.openmct.objects.makeKeyString(endpointIdentifier); const id = this.openmct.objects.makeKeyString(endpointIdentifier);
if (!this.telemetryCollections[keyString]) { if (!this.subscriptions[id]) {
console.log('no subscription to remove');
return; return;
} }
this.telemetryCollections[keyString].destroy(); this.subscriptions[id]();
this.telemetryCollections[keyString] = null; delete this.subscriptions[id];
this.telemetryObjects[keyString] = null; delete this.telemetryObjects[id];
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
@ -98,7 +107,7 @@ export default class ConditionManager extends EventEmitter {
this.timeSystems, this.timeSystems,
this.openmct.time.getTimeSystem() this.openmct.time.getTimeSystem()
); );
this.updateConditionResults({ id: keyString }); this.updateConditionResults({ id: id });
this.updateCurrentCondition(latestTimestamp); this.updateCurrentCondition(latestTimestamp);
if (Object.keys(this.telemetryObjects).length === 0) { if (Object.keys(this.telemetryObjects).length === 0) {
@ -401,13 +410,11 @@ export default class ConditionManager extends EventEmitter {
return this.openmct.time.getBounds().end >= currentTimestamp; return this.openmct.time.getBounds().end >= currentTimestamp;
} }
telemetryReceived(endpoint, data) { telemetryReceived(endpoint, datum) {
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 = {};
@ -500,9 +507,8 @@ 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.telemetryCollections).forEach((telemetryCollection) => Object.values(this.subscriptions).forEach((unsubscribe) => unsubscribe());
telemetryCollection.destroy() delete this.subscriptions;
);
this.conditions.forEach((condition) => { this.conditions.forEach((condition) => {
condition.destroy(); condition.destroy();

View File

@ -27,16 +27,14 @@
aria-label="Condition Set Condition Collection" aria-label="Condition Set Condition Collection"
> >
<div class="c-cs__header c-section__header"> <div class="c-cs__header c-section__header">
<button <span
class="c-disclosure-triangle c-tree__item__view-control is-enabled" class="c-disclosure-triangle c-tree__item__view-control is-enabled"
:class="{ 'c-disclosure-triangle--expanded': expanded }" :class="{ 'c-disclosure-triangle--expanded': expanded }"
:aria-expanded="expanded" @click="expanded = !expanded"
aria-controls="conditionContent" ></span>
@click="toggleExpanded"
></button>
<div class="c-cs__header-label c-section__label">Conditions</div> <div class="c-cs__header-label c-section__label">Conditions</div>
</div> </div>
<div v-if="expanded" id="conditionContent" class="c-cs__content"> <div v-if="expanded" class="c-cs__content">
<div <div
v-show="isEditing" v-show="isEditing"
class="hint" class="hint"
@ -56,10 +54,9 @@
v-show="isEditing" v-show="isEditing"
id="addCondition" id="addCondition"
class="c-button c-button--major icon-plus labeled" class="c-button c-button--major icon-plus labeled"
aria-labelledby="addConditionButtonLabel"
@click="addCondition" @click="addCondition"
> >
<span id="addConditionButtonLabel" class="c-cs-button__label">Add Condition</span> <span class="c-cs-button__label">Add Condition</span>
</button> </button>
<div class="c-cs__conditions-h" :class="{ 'is-active-dragging': isDragging }"> <div class="c-cs__conditions-h" :class="{ 'is-active-dragging': isDragging }">

View File

@ -28,7 +28,11 @@
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible }, { 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 } { 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]" ]"
:style="[encodedImageUrl ? { backgroundImage: 'url(' + encodedImageUrl + ')' } : itemStyle]" :style="[
styleItem.style.imageUrl
? { backgroundImage: 'url(' + styleItem.style.imageUrl + ')' }
: itemStyle
]"
class="c-style-thumb" class="c-style-thumb"
> >
<span <span
@ -58,7 +62,7 @@
@change="updateStyleValue" @change="updateStyleValue"
/> />
<ToolbarButton <ToolbarButton
v-if="hasProperty(encodedImageUrl)" v-if="hasProperty(styleItem.style.imageUrl)"
class="c-style__toolbar-button--image-url" class="c-style__toolbar-button--image-url"
:options="imageUrlOption" :options="imageUrlOption"
@change="updateStyleValue" @change="updateStyleValue"
@ -89,8 +93,6 @@ import ToolbarButton from '@/ui/toolbar/components/ToolbarButton.vue';
import ToolbarColorPicker from '@/ui/toolbar/components/ToolbarColorPicker.vue'; import ToolbarColorPicker from '@/ui/toolbar/components/ToolbarColorPicker.vue';
import ToolbarToggleButton from '@/ui/toolbar/components/ToolbarToggleButton.vue'; import ToolbarToggleButton from '@/ui/toolbar/components/ToolbarToggleButton.vue';
import { encode_url } from '../../../../utils/encoding';
export default { export default {
name: 'StyleEditor', name: 'StyleEditor',
components: { components: {
@ -181,14 +183,11 @@ export default {
}, },
property: 'imageUrl', property: 'imageUrl',
formKeys: ['url'], formKeys: ['url'],
value: { url: this.encodedImageUrl }, value: { url: this.styleItem.style.imageUrl },
isEditing: this.isEditing, isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1 nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
}; };
}, },
encodedImageUrl() {
return encode_url(this.styleItem.style.imageUrl);
},
isStyleInvisibleOption() { isStyleInvisibleOption() {
return { return {
value: this.styleItem.style.isStyleInvisible, value: this.styleItem.style.isStyleInvisible,

View File

@ -720,69 +720,50 @@ describe('the plugin', function () {
}; };
}); });
it('should evaluate as old when telemetry is not received in the allotted time', async () => { it('should evaluate as old when telemetry is not received in the allotted time', (done) => {
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: testTelemetryObject.telemetry.values, valueMetadatas: []
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.request.and.returnValue(Promise.resolve([]));
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();
// Wait for the 'on' callback to be called setTimeout(() => {
await onAddCalledPromise; expect(mockListener).toHaveBeenCalledWith({
output: 'Any old telemetry',
// Simulate the passage of time and no data received id: {
await new Promise((resolve) => setTimeout(resolve, 400)); namespace: '',
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
expect(mockListener).toHaveBeenCalledWith({ },
output: 'Any old telemetry', conditionId: '39584410-cbf9-499e-96dc-76f27e69885d',
id: { utc: undefined
namespace: '', });
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' done();
}, }, 400);
conditionId: '39584410-cbf9-499e-96dc-76f27e69885d',
utc: undefined
});
}); });
it('should not evaluate as old when telemetry is received in the allotted time', async () => { it('should not evaluate as old when telemetry is received in the allotted time', (done) => {
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,
@ -790,49 +771,8 @@ 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'];
@ -842,25 +782,19 @@ describe('the plugin', function () {
'test-object': testTelemetryObject 'test-object': testTelemetryObject
}; };
conditionMgr.updateConditionTelemetryObjects(); conditionMgr.updateConditionTelemetryObjects();
conditionMgr.telemetryReceived(testTelemetryObject, testDatum);
// Wait for the 'on' callback to be called setTimeout(() => {
await onAddCalledPromise; expect(mockListener).toHaveBeenCalledWith({
output: 'Default',
// Simulate receiving telemetry data id: {
onAddCallback([testDatum]); namespace: '',
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
// Wait a bit for the condition manager to process the data },
await new Promise((resolve) => setTimeout(resolve, 100)); conditionId: '2532d90a-e0d6-4935-b546-3123522da2de',
utc: date
expect(mockListener).toHaveBeenCalledWith({ });
output: 'Default', done();
id: { }, 300);
namespace: '',
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
},
conditionId: '2532d90a-e0d6-4935-b546-3123522da2de',
utc: date
});
}); });
}); });
@ -968,25 +902,17 @@ 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: testTelemetryObject.telemetry.values, valueMetadatas: []
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,
'some-key': 2, utc: date
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();
}); });
@ -1076,37 +1002,26 @@ 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',
'requestCollection' 'request'
]); ]);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
openmct.telemetry.isTelemetryObject.and.returnValue(true); openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); openmct.telemetry.subscribe.and.returnValue(function () {});
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.requestCollection.and.returnValue({ openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
load: jasmine.createSpy('load'), openmct.telemetry.request.and.returnValue(Promise.resolve([]));
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

@ -29,13 +29,12 @@
@end-move="endMove" @end-move="endMove"
> >
<template #content> <template #content>
<div v-show="showImage" aria-label="Image View" class="c-image-view" :style="style"></div> <div class="c-image-view" :style="style"></div>
</template> </template>
</LayoutFrame> </LayoutFrame>
</template> </template>
<script> <script>
import { encode_url } from '../../../utils/encoding';
import conditionalStylesMixin from '../mixins/objectStyles-mixin.js'; import conditionalStylesMixin from '../mixins/objectStyles-mixin.js';
import LayoutFrame from './LayoutFrame.vue'; import LayoutFrame from './LayoutFrame.vue';
@ -77,16 +76,13 @@ export default {
}, },
emits: ['move', 'end-move'], emits: ['move', 'end-move'],
computed: { computed: {
showImage() {
return this.isEditing || !this.itemStyle?.isStyleInvisible;
},
style() { style() {
let backgroundImage = `url('${encode_url(this.item.url)}')`; let backgroundImage = 'url(' + this.item.url + ')';
let border = '1px solid ' + this.item.stroke; let border = '1px solid ' + this.item.stroke;
if (this.itemStyle) { if (this.itemStyle) {
if (this.itemStyle.imageUrl !== undefined) { if (this.itemStyle.imageUrl !== undefined) {
backgroundImage = `url('${encode_url(this.itemStyle.imageUrl)}')`; backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
} }
border = this.itemStyle.border; border = this.itemStyle.border;

View File

@ -31,10 +31,9 @@
<template #content> <template #content>
<div <div
v-if="domainObject" v-if="domainObject"
v-show="showTelemetry"
ref="telemetryViewWrapper" ref="telemetryViewWrapper"
class="c-telemetry-view u-style-receiver" class="c-telemetry-view u-style-receiver"
:class="classNames" :class="[itemClasses]"
:style="styleObject" :style="styleObject"
:data-font-size="item.fontSize" :data-font-size="item.fontSize"
:data-font="item.font" :data-font="item.font"
@ -152,10 +151,7 @@ export default {
}; };
}, },
computed: { computed: {
showTelemetry() { itemClasses() {
return this.isEditing || !this.itemStyle?.isStyleInvisible;
},
classNames() {
let classes = []; let classes = [];
if (this.status) { if (this.status) {

View File

@ -109,9 +109,8 @@ class DuplicateAction {
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier); let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier); let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier); let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
const isLocked = parentCandidate.locked === true;
if (isLocked || !this.openmct.objects.isPersistable(parentCandidate.identifier)) { if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
return false; return false;
} }
@ -140,9 +139,10 @@ class DuplicateAction {
const parentType = parent && this.openmct.types.get(parent.type); const parentType = parent && this.openmct.types.get(parent.type);
const child = objectPath[0]; const child = objectPath[0];
const childType = child && this.openmct.types.get(child.type); const childType = child && this.openmct.types.get(child.type);
const locked = child.locked ? child.locked : parent && parent.locked;
const isPersistable = this.openmct.objects.isPersistable(child.identifier); const isPersistable = this.openmct.objects.isPersistable(child.identifier);
if (!isPersistable) { if (locked || !isPersistable) {
return false; return false;
} }

View File

@ -21,12 +21,7 @@
--> -->
<template> <template>
<div <div class="c-fault-mgmt__list data-selectable" :class="classesFromState">
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"
@ -38,44 +33,29 @@
<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="severityAriaLabel" :aria-label="fault.severity"
: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" aria-label="Fault namespace"> <div class="c-fault-mgmt__list-path">{{ fault.namespace }}</div>
{{ fault.namespace }} <div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div>
</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 <div class="c-fault-mgmt-item__value" :class="tripValueClassname" title="Trip Value">
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 <div class="c-fault-mgmt-item__value" :class="liveValueClassname" title="Live Value">
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 <div class="c-fault-mgmt-item__value" title="Last Trigger Time">
class="c-fault-mgmt-item__value"
title="Last Trigger Time"
aria-label="Last Trigger Time"
>
{{ fault.triggerTime }} {{ fault.triggerTime }}
</div> </div>
</div> </div>
@ -118,7 +98,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 || 'Unknown'} in ${this.fault.namespace || 'Unknown'}`; return `Select fault: ${this.fault.name}`;
}, },
classesFromState() { classesFromState() {
const exclusiveStates = [ const exclusiveStates = [
@ -185,12 +165,6 @@ 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,6 +68,7 @@
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';
@ -87,13 +88,6 @@ 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,
@ -117,18 +111,23 @@ export default {
}, },
filteredFaultsList() { filteredFaultsList() {
const filterName = FILTER_ITEMS[this.filterIndex]; const filterName = FILTER_ITEMS[this.filterIndex];
let list = this.faultsList.filter((fault) => let list = this.faultsList;
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 = filterFaultsByTerm(list, this.searchTerm); list = list.filter(this.filterUsingSearchTerm);
} }
list.sort(SORT_ITEMS[this.sortBy].sortFunction); list.sort(SORT_ITEMS[this.sortBy].sortFunction);
@ -139,9 +138,6 @@ 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);
}, },
@ -162,13 +158,14 @@ export default {
}); });
} }
}, },
async updateFaultList() { updateFaultList() {
const faultsData = await this.openmct.faults.request(this.domainObject); this.openmct.faults.request(this.domainObject).then((faultsData) => {
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) {
@ -226,29 +223,14 @@ export default {
); );
}, },
async toggleAcknowledgeSelected(faults = this.selectedFaults) { async toggleAcknowledgeSelected(faults = this.selectedFaults) {
const title = this.getAcknowledgeTitle(faults); let title = '';
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) { if (faults.length > 1) {
return `Acknowledge ${faults.length} selected faults`; title = `Acknowledge ${faults.length} selected faults`;
} else if (faults.length === 1) { } else if (faults.length === 1) {
return `Acknowledge fault: ${faults[0].name}`; title = `Acknowledge fault: ${faults[0].name}`;
} }
return '';
}, const formStructure = {
getAcknowledgeFormStructure(title) {
return {
title, title,
sections: [ sections: [
{ {
@ -271,11 +253,17 @@ export default {
} }
} }
}; };
},
acknowledgeFaults(faults, data) { try {
faults.forEach((fault) => { const data = await this.openmct.forms.showForm(formStructure);
this.openmct.faults.acknowledgeFault(fault, data); faults.forEach((fault) => {
}); 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) => {
@ -285,7 +273,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) {
const title = let 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}`;
@ -307,10 +295,10 @@ export default {
key: 'shelveDuration', key: 'shelveDuration',
control: 'select', control: 'select',
name: 'Shelve duration', name: 'Shelve duration',
options: this.shelveDurations, options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
required: false, required: false,
cssClass: 'l-input-lg', cssClass: 'l-input-lg',
value: this.shelveDurations[0].value value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
} }
] ]
} }
@ -331,16 +319,18 @@ export default {
shelveData.comment = data.comment || ''; shelveData.comment = data.comment || '';
shelveData.shelveDuration = shelveData.shelveDuration =
data.shelveDuration === undefined ? this.shelveDurations[0].value : data.shelveDuration; data.shelveDuration !== undefined
? data.shelveDuration
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
} else { } else {
shelveData = { shelveData = {
shelved: false shelved: false
}; };
} }
await Promise.all( Object.values(faults).forEach((selectedFault) => {
faults.map((selectedFault) => this.openmct.faults.shelveFault(selectedFault, shelveData)) this.openmct.faults.shelveFault(selectedFault, shelveData);
); });
this.selectedFaultMap = {}; this.selectedFaultMap = {};
}, },

View File

@ -42,6 +42,24 @@ export const FAULT_MANAGEMENT_TYPE = 'faultManagement';
export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector'; export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector';
export const FAULT_MANAGEMENT_ALARMS = 'alarms'; export const FAULT_MANAGEMENT_ALARMS = 'alarms';
export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status'; export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status';
export const FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS = [
{
name: '5 Minutes',
value: 300000
},
{
name: '10 Minutes',
value: 600000
},
{
name: '15 Minutes',
value: 900000
},
{
name: 'Indefinite',
value: 0
}
];
export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view'; export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view';
export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy'; export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy';
export const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved']; export const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved'];

View File

@ -45,7 +45,7 @@ class EditPropertiesAction extends PropertiesAction {
const definition = this._getTypeDefinition(object.type); const definition = this._getTypeDefinition(object.type);
const persistable = this.openmct.objects.isPersistable(object.identifier); const persistable = this.openmct.objects.isPersistable(object.identifier);
return persistable && definition && definition.creatable && !object.locked; return persistable && definition && definition.creatable;
} }
invoke(objectPath) { invoke(objectPath) {

View File

@ -38,7 +38,7 @@
<img <img
ref="img" ref="img"
class="c-thumb__image" class="c-thumb__image"
:src="imageSrc" :src="`${image.thumbnailUrl || image.url}`"
fetchpriority="low" fetchpriority="low"
@load="imageLoadCompleted" @load="imageLoadCompleted"
/> />
@ -54,8 +54,6 @@
</template> </template>
<script> <script>
import { encode_url } from '../../../utils/encoding';
const THUMB_PADDING = 4; const THUMB_PADDING = 4;
const BORDER_WIDTH = 2; const BORDER_WIDTH = 2;
@ -98,9 +96,6 @@ export default {
}; };
}, },
computed: { computed: {
imageSrc() {
return `${encode_url(this.image.thumbnailUrl) || encode_url(this.image.url)}`;
},
ariaLabel() { ariaLabel() {
return `Image thumbnail from ${this.image.formattedTime}${this.showAnnotationIndicator ? ', has annotations' : ''}`; return `Image thumbnail from ${this.image.formattedTime}${this.showAnnotationIndicator ? ', has annotations' : ''}`;
}, },

View File

@ -370,7 +370,6 @@ 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

@ -222,7 +222,6 @@ import { TIME_CONTEXT_EVENTS } from '@/api/time/constants.js';
import imageryData from '@/plugins/imagery/mixins/imageryData.js'; import imageryData from '@/plugins/imagery/mixins/imageryData.js';
import { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js'; import { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js';
import { encode_url } from '../../../utils/encoding';
import eventHelpers from '../lib/eventHelpers.js'; import eventHelpers from '../lib/eventHelpers.js';
import AnnotationsCanvas from './AnnotationsCanvas.vue'; import AnnotationsCanvas from './AnnotationsCanvas.vue';
import Compass from './Compass/CompassComponent.vue'; import Compass from './Compass/CompassComponent.vue';
@ -365,7 +364,7 @@ export default {
filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`, filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
backgroundImage: `${ backgroundImage: `${
this.imageUrl this.imageUrl
? `url(${encode_url(this.imageUrl)}), ? `url(${this.imageUrl}),
repeating-linear-gradient( repeating-linear-gradient(
45deg, 45deg,
transparent, transparent,
@ -621,7 +620,7 @@ export default {
if (matchIndex > -1) { if (matchIndex > -1) {
this.setFocusedImage(matchIndex); this.setFocusedImage(matchIndex);
} else { } else {
this.paused(false); this.paused();
} }
} }
@ -790,7 +789,7 @@ export default {
}, },
getVisibleLayerStyles(layer) { getVisibleLayerStyles(layer) {
return { return {
backgroundImage: `url(${encode_url(layer.source)})`, backgroundImage: `url(${layer.source})`,
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${ transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${
this.imageTranslateY / 2 this.imageTranslateY / 2
}px)`, }px)`,
@ -1083,7 +1082,7 @@ export default {
paused(state) { paused(state) {
this.isPaused = Boolean(state); this.isPaused = Boolean(state);
if (!this.isPaused) { if (!state) {
this.previousFocusedImage = null; this.previousFocusedImage = null;
this.setFocusedImage(this.nextImageIndex); this.setFocusedImage(this.nextImageIndex);
this.autoScroll = true; this.autoScroll = true;

View File

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

View File

@ -96,8 +96,6 @@ export default {
const createdTimestamp = this.domainObject.created; const createdTimestamp = this.domainObject.created;
const createdBy = this.domainObject.createdBy ? this.domainObject.createdBy : UNKNOWN_USER; const createdBy = this.domainObject.createdBy ? this.domainObject.createdBy : UNKNOWN_USER;
const modifiedBy = this.domainObject.modifiedBy ? this.domainObject.modifiedBy : UNKNOWN_USER; const modifiedBy = this.domainObject.modifiedBy ? this.domainObject.modifiedBy : UNKNOWN_USER;
const locked = this.domainObject.locked;
const lockedBy = this.domainObject.lockedBy ?? UNKNOWN_USER;
const modifiedTimestamp = this.domainObject.modified const modifiedTimestamp = this.domainObject.modified
? this.domainObject.modified ? this.domainObject.modified
: this.domainObject.created; : this.domainObject.created;
@ -150,13 +148,6 @@ export default {
}); });
} }
if (locked === true) {
details.push({
name: 'Locked By',
value: lockedBy
});
}
if (version) { if (version) {
details.push({ details.push({
name: 'Version', name: 'Version',

View File

@ -19,23 +19,14 @@
* 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();
openmct.performance = {
measurements: new Map()
};
const indicator = openmct.indicators.simpleIndicator(); const indicator = openmct.indicators.simpleIndicator();
indicator.key = 'performance-indicator';
indicator.text('~ fps');
indicator.description('Performance Indicator');
indicator.statusClass('s-status-info');
indicator.on('click', showOverlay);
indicator.text('~ fps');
indicator.statusClass('s-status-info');
openmct.indicators.add(indicator); openmct.indicators.add(indicator);
let rafHandle = requestAnimationFrame(incrementFrames); let rafHandle = requestAnimationFrame(incrementFrames);
@ -67,58 +58,5 @@ 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 }
);
}
}; };
} }

View File

@ -1,191 +0,0 @@
import http from 'http';
import nano from 'nano';
import { parseArgs } from 'util';
const COUCH_URL = process.env.OPENMCT_COUCH_URL || 'http://127.0.0.1:5984';
const COUCH_DB_NAME = process.env.OPENMCT_DATABASE_NAME || 'openmct';
const {
values: { couchUrl, database, lock, unlock, startObjectKeystring, user, pass }
} = parseArgs({
options: {
couchUrl: {
type: 'string',
default: COUCH_URL
},
database: {
type: 'string',
short: 'd',
default: COUCH_DB_NAME
},
lock: {
type: 'boolean',
short: 'l'
},
unlock: {
type: 'boolean',
short: 'u'
},
startObjectKeystring: {
type: 'string',
short: 'o',
default: 'mine'
},
user: {
type: 'string'
},
pass: {
type: 'string'
}
}
});
const BATCH_SIZE = 100;
const SOCKET_POOL_SIZE = 100;
const locked = lock === true;
console.info(`Connecting to ${couchUrl}/${database}`);
console.info(`${locked ? 'Locking' : 'Unlocking'} all children of ${startObjectKeystring}`);
const poolingAgent = new http.Agent({
keepAlive: true,
maxSockets: SOCKET_POOL_SIZE
});
const db = nano({
url: couchUrl,
requestDefaults: {
agent: poolingAgent
}
}).use(database);
db.auth(user, pass);
if (!unlock && !lock) {
throw new Error('Either -l or -u option is required');
}
const startObjectIdentifier = keystringToIdentifier(startObjectKeystring);
const documentBatch = [];
const alreadySeen = new Set();
let updatedDocumentCount = 0;
await processObjectTreeFrom(startObjectIdentifier);
//Persist final batch
await persistBatch();
console.log(`Processed ${updatedDocumentCount} documents`);
function processObjectTreeFrom(parentObjectIdentifier) {
//1. Fetch document for identifier;
return fetchDocument(parentObjectIdentifier)
.then(async (document) => {
if (document !== undefined) {
if (!alreadySeen.has(document._id)) {
alreadySeen.add(document._id);
//2. Lock or unlock object
document.model.locked = locked;
document.model.disallowUnlock = locked;
if (locked) {
document.model.lockedBy = 'script';
} else {
delete document.model.lockedBy;
}
//3. Push document to a batch
documentBatch.push(document);
//4. Persist batch if necessary, reporting failures
await persistBatchIfNeeded();
//5. Repeat for each composee
const composition = document.model.composition || [];
return Promise.all(
composition.map((composee) => {
return processObjectTreeFrom(composee);
})
);
}
}
})
.catch((error) => {
console.log(`Error ${error}`);
});
}
async function fetchDocument(identifierOrKeystring) {
let keystring;
if (typeof identifierOrKeystring === 'object') {
keystring = identifierToKeystring(identifierOrKeystring);
} else {
keystring = identifierOrKeystring;
}
try {
const document = await db.get(keystring);
return document;
} catch (error) {
return undefined;
}
}
function persistBatchIfNeeded() {
if (documentBatch.length >= BATCH_SIZE) {
return persistBatch();
} else {
//Noop - batch is not big enough yet
return;
}
}
async function persistBatch() {
try {
const localBatch = [].concat(documentBatch);
//Immediately clear the shared batch array. This asynchronous process is non-blocking, and
//we don't want to try and persist the same batch multiple times while we are waiting for
//the subsequent bulk operation to complete.
updatedDocumentCount += documentBatch.length;
documentBatch.splice(0, documentBatch.length);
const response = await db.bulk({ docs: localBatch });
if (response instanceof Array) {
response.forEach((r) => {
console.info(JSON.stringify(r));
});
} else {
console.info(JSON.stringify(response));
}
} catch (error) {
if (error instanceof Array) {
error.forEach((e) => console.error(JSON.stringify(e)));
} else {
console.error(`${error.statusCode} - ${error.reason}`);
}
}
}
function keystringToIdentifier(keystring) {
const tokens = keystring.split(':');
if (tokens.length === 2) {
return {
namespace: tokens[0],
key: tokens[1]
};
} else {
return {
namespace: '',
key: tokens[0]
};
}
}
function identifierToKeystring(identifier) {
if (typeof identifier === 'string') {
return identifier;
} else if (typeof identifier === 'object') {
if (identifier.namespace) {
return `${identifier.namespace}:${identifier.key}`;
} else {
return identifier.key;
}
}
}

View File

@ -160,24 +160,6 @@ add_index_and_views() {
echo "Unable to create annotation_keystring_index" echo "Unable to create annotation_keystring_index"
echo $response echo $response
fi fi
# Add auth database for locked objects
response=$(curl --silent --user "${CURL_USERPASS_ARG}" --request PUT "$COUCH_BASE_LOCAL"/"$OPENMCT_DATABASE_NAME"/_design/auth \
--header 'Content-Type: application/json' \
--data '{
"_id": "_design/auth",
"language": "javascript",
"validate_doc_update": "function (newDoc, oldDoc, userCtx) { if (userCtx.roles.indexOf('\''_admin'\'') !== -1) { return; } else if (oldDoc === null) { return; } else if (oldDoc.model.type === '\''timer'\'' || oldDoc.model.type === '\''notebook'\'' || oldDoc.model.type === '\''restricted-notebook'\'') { if (oldDoc.model.name !== newDoc.model.name) { throw ({ forbidden: '\''Read-only object'\'' }); } else { return; } } else if (oldDoc.model.locked === true && oldDoc.model.disallowUnlock === true) { throw ({ forbidden: '\''Read-only object'\'' }); } else { return; }}"
}')
if [[ $response =~ "\"ok\":true" ]]; then
echo "Successfully created _design/auth design document for locked objects"
elif [[ $response =~ "\"error\":\"conflict\"" ]]; then
echo "_design/auth already exists, skipping creation"
else
echo "Unable to create _design/auth"
echo $response
fi
} }
# Main script execution # Main script execution

View File

@ -69,6 +69,7 @@ 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;
@ -142,15 +143,13 @@ 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() {
this.resizeObserver.disconnect(); clearInterval(this.resizeTimer);
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();
@ -243,12 +242,11 @@ export default {
if (this.planObject) { if (this.planObject) {
this.showReplacePlanDialog(domainObject); this.showReplacePlanDialog(domainObject);
} else { } else {
this.setupPlan(domainObject);
this.swimlaneVisibility = this.configuration.swimlaneVisibility; this.swimlaneVisibility = this.configuration.swimlaneVisibility;
this.setupPlan(domainObject);
} }
}, },
handleConfigurationChange(newConfiguration) { handleConfigurationChange(newConfiguration) {
this.configuration = this.planViewConfiguration.getConfiguration();
Object.keys(newConfiguration).forEach((key) => { Object.keys(newConfiguration).forEach((key) => {
this[key] = newConfiguration[key]; this[key] = newConfiguration[key];
}); });
@ -424,10 +422,7 @@ export default {
return currentRow || SWIMLANE_PADDING; return currentRow || SWIMLANE_PADDING;
}, },
generateActivities() { generateActivities() {
if (!this.planObject) { const groupNames = getValidatedGroups(this.domainObject, this.planData);
return;
}
const groupNames = getValidatedGroups(this.planObject, this.planData);
if (!groupNames.length) { if (!groupNames.length) {
return; return;

View File

@ -1,5 +1,5 @@
<!-- <!--
Open MCT, Copyright (c) 2014-2024, United States Government Open MCT, Copyright (c) 2014-2023, 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-2024, United States Government Open MCT, Copyright (c) 2014-2023, 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,22 +128,35 @@ export default {
} }
}, },
updateStyle(styleObj) { updateStyle(styleObj) {
const elemToStyle = this.getStyleReceiver(); let elemToStyle = this.getStyleReceiver();
if (!styleObj || !elemToStyle) { if (!styleObj || elemToStyle === undefined) {
return; return;
} }
// handle visibility separately
if (styleObj.isStyleInvisible !== undefined) {
elemToStyle.classList.toggle(STYLE_CONSTANTS.isStyleInvisible, styleObj.isStyleInvisible);
styleObj.isStyleInvisible = null;
}
Object.entries(styleObj).forEach(([key, value]) => { let keys = Object.keys(styleObj);
if (typeof value !== 'string' || !value.includes('__no_value')) {
elemToStyle.style[key] = value; keys.forEach((key) => {
} else { if (elemToStyle) {
elemToStyle.style[key] = ''; // remove the property if (typeof styleObj[key] === 'string' && styleObj[key].indexOf('__no_value') > -1) {
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-2024, United States Government * Open MCT, Copyright (c) 2014-2023, 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-2024, United States Government * Open MCT, Copyright (c) 2014-2023, 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,18 +152,15 @@ export default class RemoteClock extends DefaultClock {
*/ */
#waitForReady() { #waitForReady() {
const waitForInitialTick = (resolve) => { const waitForInitialTick = (resolve) => {
const tickListener = () => { if (this.lastTick > 0) {
if (this.lastTick > 0) { const offsets = this.openmct.time.getClockOffsets();
const offsets = this.openmct.time.getClockOffsets(); resolve({
this.openmct.time.off('tick', tickListener); // Unregister the tick listener start: this.lastTick + offsets.start,
resolve({ end: this.lastTick + offsets.end
start: this.lastTick + offsets.start, });
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,43 +20,16 @@
* 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

@ -38,11 +38,11 @@
v-for="(tab, index) in tabsList" v-for="(tab, index) in tabsList"
:ref="tab.keyString" :ref="tab.keyString"
:key="tab.keyString" :key="tab.keyString"
:aria-label="`${tab.domainObject.name} tab${tab.keyString === currentTab.keyString ? ' - selected' : ''}`" :aria-label="`${tab.domainObject.name} tab`"
class="c-tab c-tabs-view__tab js-tab" class="c-tab c-tabs-view__tab js-tab"
role="tab" role="tab"
:class="{ :class="{
'is-current': tab.keyString === currentTab.keyString 'is-current': isCurrent(tab)
}" }"
@click="showTab(tab, index)" @click="showTab(tab, index)"
@mouseover.ctrl="showToolTip(tab)" @mouseover.ctrl="showToolTip(tab)"
@ -74,7 +74,7 @@
:key="tab.keyString" :key="tab.keyString"
:style="getTabStyles(tab)" :style="getTabStyles(tab)"
class="c-tabs-view__object-holder" class="c-tabs-view__object-holder"
:class="{ 'c-tabs-view__object-holder--hidden': tab.keyString !== currentTab.keyString }" :class="{ 'c-tabs-view__object-holder--hidden': !isCurrent(tab) }"
> >
<ObjectView <ObjectView
v-if="shouldLoadTab(tab)" v-if="shouldLoadTab(tab)"
@ -353,10 +353,7 @@ export default {
this.internalDomainObject = domainObject; this.internalDomainObject = domainObject;
}, },
persistCurrentTabIndex(index) { persistCurrentTabIndex(index) {
//only persist if the domain object is not locked. The object mutate API will deal with whether the object is persistable or not. this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index);
if (!this.internalDomainObject.locked) {
this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index);
}
}, },
storeCurrentTabIndexInURL(index) { storeCurrentTabIndexInURL(index) {
let currentTabIndexInURL = this.openmct.router.getSearchParam(this.searchTabKey); let currentTabIndexInURL = this.openmct.router.getSearchParam(this.searchTabKey);

View File

@ -25,7 +25,6 @@ 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 } 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';
@ -120,7 +119,7 @@ export default class TelemetryTable extends EventEmitter {
this.rowLimit = rowLimit; this.rowLimit = rowLimit;
} }
if (this.telemetryMode === MODE.PERFORMANCE) { if (this.telemetryMode === 'performance') {
this.tableRows.setLimit(this.rowLimit); this.tableRows.setLimit(this.rowLimit);
} else { } else {
this.tableRows.removeLimit(); this.tableRows.removeLimit();
@ -130,7 +129,14 @@ export default class TelemetryTable extends EventEmitter {
createTableRowCollections() { createTableRowCollections() {
this.tableRows = new TableRowCollection(); this.tableRows = new TableRowCollection();
const sortOptions = this.configuration.getSortOptions(); //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: 'desc'
};
this.updateRowLimit(); this.updateRowLimit();
@ -165,10 +171,11 @@ export default class TelemetryTable extends EventEmitter {
this.removeTelemetryCollection(keyString); this.removeTelemetryCollection(keyString);
let sortOptions = this.configuration.getSortOptions(); let sortOptions = this.configuration.getConfiguration().sortOptions;
requestOptions.order = sortOptions.direction; requestOptions.order =
sortOptions?.direction ?? (this.telemetryMode === 'performance' ? 'desc' : 'asc');
if (this.telemetryMode === MODE.PERFORMANCE) { if (this.telemetryMode === 'performance') {
requestOptions.size = this.rowLimit; requestOptions.size = this.rowLimit;
requestOptions.enforceSize = true; requestOptions.enforceSize = true;
} }
@ -435,13 +442,12 @@ export default class TelemetryTable extends EventEmitter {
} }
sortBy(sortOptions) { sortBy(sortOptions) {
this.configuration.setSortOptions(sortOptions); this.tableRows.sortBy(sortOptions);
if (this.telemetryMode === MODE.PERFORMANCE) { if (this.openmct.editor.isEditing()) {
this.tableRows.setSortOptions(sortOptions); let configuration = this.configuration.getConfiguration();
this.clearAndResubscribe(); configuration.sortOptions = sortOptions;
} else { this.configuration.updateConfiguration(configuration);
this.tableRows.sortBy(sortOptions);
} }
} }

View File

@ -23,11 +23,7 @@
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();
@ -48,26 +44,6 @@ export default class TelemetryTableConfiguration extends EventEmitter {
this.notPersistable = !this.openmct.objects.isPersistable(this.domainObject.identifier); this.notPersistable = !this.openmct.objects.isPersistable(this.domainObject.identifier);
} }
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);
}
}
getConfiguration() { getConfiguration() {
let configuration = this.domainObject.configuration || {}; let configuration = this.domainObject.configuration || {};
configuration.hiddenColumns = configuration.hiddenColumns || {}; configuration.hiddenColumns = configuration.hiddenColumns || {};

View File

@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { MODE } from './constants.js';
export default function getTelemetryTableType(options) { export default function getTelemetryTableType(options) {
let { telemetryMode, persistModeChange, rowLimit } = options; let { telemetryMode, persistModeChange, rowLimit } = options;
@ -38,11 +36,11 @@ export default function getTelemetryTableType(options) {
control: 'select', control: 'select',
options: [ options: [
{ {
value: MODE.PERFORMANCE, value: 'performance',
name: 'Limited (Performance) Mode' name: 'Limited (Performance) Mode'
}, },
{ {
value: MODE.UNLIMITED, value: 'unlimited',
name: 'Unlimited Mode' name: 'Unlimited Mode'
} }
], ],

View File

@ -22,7 +22,6 @@
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import _ from 'lodash'; import _ from 'lodash';
import { ORDER } from '../constants.js';
/** /**
* @constructor * @constructor
*/ */
@ -150,13 +149,13 @@ export default class TableRowCollection extends EventEmitter {
} }
insertOrUpdateRows(rowsToAdd, addToBeginning) { insertOrUpdateRows(rowsToAdd, addToBeginning) {
rowsToAdd.forEach((row, addRowsIndex) => { rowsToAdd.forEach((row) => {
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.splice(addRowsIndex, 0, row); this.rows.unshift(row);
} else { } else {
this.rows.push(row); this.rows.push(row);
} }
@ -209,7 +208,7 @@ export default class TableRowCollection extends EventEmitter {
const val1 = this.getValueForSortColumn(row1); const val1 = this.getValueForSortColumn(row1);
const val2 = this.getValueForSortColumn(row2); const val2 = this.getValueForSortColumn(row2);
if (this.sortOptions.direction === ORDER.ASCENDING) { if (this.sortOptions.direction === 'asc') {
return val1 <= val2 ? row1 : row2; return val1 <= val2 ? row1 : row2;
} else { } else {
return val1 >= val2 ? row1 : row2; return val1 >= val2 ? row1 : row2;
@ -273,7 +272,7 @@ export default class TableRowCollection extends EventEmitter {
*/ */
sortBy(sortOptions) { sortBy(sortOptions) {
if (arguments.length > 0) { if (arguments.length > 0) {
this.setSortOptions(sortOptions); this.sortOptions = sortOptions;
this.rows = _.orderBy( this.rows = _.orderBy(
this.rows, this.rows,
(row) => row.getParsedValue(sortOptions.key), (row) => row.getParsedValue(sortOptions.key),
@ -286,10 +285,6 @@ 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;
@ -378,7 +373,7 @@ export default class TableRowCollection extends EventEmitter {
getRows() { getRows() {
if (this.rowLimit && this.rows.length > this.rowLimit) { if (this.rowLimit && this.rows.length > this.rowLimit) {
if (this.sortOptions.direction === ORDER.DESCENDING) { if (this.sortOptions.direction === 'desc') {
return this.rows.slice(0, this.rowLimit); return this.rows.slice(0, this.rowLimit);
} else { } else {
return this.rows.slice(-this.rowLimit); return this.rows.slice(-this.rowLimit);

View File

@ -296,7 +296,6 @@ import ProgressBar from '../../../ui/components/ProgressBar.vue';
import Search from '../../../ui/components/SearchComponent.vue'; import Search from '../../../ui/components/SearchComponent.vue';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue'; import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import { useResizeObserver } from '../../../ui/composables/resize.js'; import { useResizeObserver } from '../../../ui/composables/resize.js';
import { MODE, ORDER } from '../constants.js';
import SizingRow from './SizingRow.vue'; import SizingRow from './SizingRow.vue';
import TableColumnHeader from './TableColumnHeader.vue'; import TableColumnHeader from './TableColumnHeader.vue';
import TableFooterIndicator from './TableFooterIndicator.vue'; import TableFooterIndicator from './TableFooterIndicator.vue';
@ -714,7 +713,7 @@ export default {
sortBy(columnKey) { sortBy(columnKey) {
let timeSystemKey = this.openmct.time.getTimeSystem().key; let timeSystemKey = this.openmct.time.getTimeSystem().key;
if (this.telemetryMode === MODE.PERFORMANCE && columnKey !== timeSystemKey) { if (this.telemetryMode === 'performance' && columnKey !== timeSystemKey) {
this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Sort', () => { this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Sort', () => {
this.initiateSort(columnKey); this.initiateSort(columnKey);
}); });
@ -725,15 +724,15 @@ export default {
initiateSort(columnKey) { initiateSort(columnKey) {
// If sorting by the same column, flip the sort direction. // If sorting by the same column, flip the sort direction.
if (this.sortOptions.key === columnKey) { if (this.sortOptions.key === columnKey) {
if (this.sortOptions.direction === ORDER.ASCENDING) { if (this.sortOptions.direction === 'asc') {
this.sortOptions.direction = ORDER.DESCENDING; this.sortOptions.direction = 'desc';
} else { } else {
this.sortOptions.direction = ORDER.ASCENDING; this.sortOptions.direction = 'asc';
} }
} else { } else {
this.sortOptions = { this.sortOptions = {
key: columnKey, key: columnKey,
direction: ORDER.DESCENDING direction: 'desc'
}; };
} }
@ -752,7 +751,7 @@ export default {
} }
}, },
shouldAutoScroll() { shouldAutoScroll() {
if (this.sortOptions.direction === ORDER.DESCENDING) { if (this.sortOptions.direction === 'desc') {
return false; return false;
} }
@ -845,7 +844,7 @@ export default {
return justTheData; return justTheData;
}, },
exportAllDataAsCSV() { exportAllDataAsCSV() {
if (this.telemetryMode === MODE.PERFORMANCE) { if (this.telemetryMode === 'performance') {
this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Export', () => { this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Export', () => {
const data = this.getTableRowData(); const data = this.getTableRowData();
@ -1227,8 +1226,7 @@ export default {
}); });
}, },
updateTelemetryMode() { updateTelemetryMode() {
this.telemetryMode = this.telemetryMode = this.telemetryMode === 'unlimited' ? 'performance' : 'unlimited';
this.telemetryMode === MODE.UNLIMITED ? MODE.PERFORMANCE : MODE.UNLIMITED;
if (this.persistModeChange) { if (this.persistModeChange) {
this.table.configuration.setTelemetryMode(this.telemetryMode); this.table.configuration.setTelemetryMode(this.telemetryMode);
@ -1238,7 +1236,7 @@ export default {
const timeSystemKey = this.openmct.time.getTimeSystem().key; const timeSystemKey = this.openmct.time.getTimeSystem().key;
if (this.telemetryMode === MODE.PERFORMANCE && this.sortOptions.key !== timeSystemKey) { if (this.telemetryMode === 'performance' && this.sortOptions.key !== timeSystemKey) {
this.openmct.notifications.info( this.openmct.notifications.info(
'Switched to Performance Mode: Table now sorted by time for optimized efficiency.' 'Switched to Performance Mode: Table now sorted by time for optimized efficiency.'
); );

View File

@ -62,8 +62,6 @@
<script> <script>
import _ from 'lodash'; import _ from 'lodash';
import { MODE } from '../constants.js';
const FILTER_INDICATOR_LABEL = 'Filters:'; const FILTER_INDICATOR_LABEL = 'Filters:';
const FILTER_INDICATOR_LABEL_MIXED = 'Mixed Filters:'; const FILTER_INDICATOR_LABEL_MIXED = 'Mixed Filters:';
const FILTER_INDICATOR_TITLE = 'Data filters are being applied to this view.'; const FILTER_INDICATOR_TITLE = 'Data filters are being applied to this view.';
@ -83,7 +81,7 @@ export default {
}, },
telemetryMode: { telemetryMode: {
type: String, type: String,
default: MODE.PERFORMANCE default: 'performance'
} }
}, },
emits: ['telemetry-mode-change'], emits: ['telemetry-mode-change'],
@ -105,7 +103,7 @@ export default {
}); });
}, },
isUnlimitedMode() { isUnlimitedMode() {
return this.telemetryMode === MODE.UNLIMITED; return this.telemetryMode === 'unlimited';
}, },
label() { label() {
if (this.hasMixedFilters) { if (this.hasMixedFilters) {

View File

@ -1,11 +0,0 @@
const ORDER = {
ASCENDING: 'asc',
DESCENDING: 'desc'
};
const MODE = {
PERFORMANCE: 'performance',
UNLIMITED: 'unlimited'
};
export { MODE, ORDER };

View File

@ -20,14 +20,13 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { MODE } from './constants.js';
import TableConfigurationViewProvider from './TableConfigurationViewProvider.js'; import TableConfigurationViewProvider from './TableConfigurationViewProvider.js';
import getTelemetryTableType from './TelemetryTableType.js'; import getTelemetryTableType from './TelemetryTableType.js';
import TelemetryTableViewProvider from './TelemetryTableViewProvider.js'; import TelemetryTableViewProvider from './TelemetryTableViewProvider.js';
import TelemetryTableViewActions from './ViewActions.js'; import TelemetryTableViewActions from './ViewActions.js';
export default function plugin( export default function plugin(
options = { telemetryMode: MODE.PERFORMANCE, persistModeChange: true, rowLimit: 50 } options = { telemetryMode: 'performance', persistModeChange: true, rowLimit: 50 }
) { ) {
return function install(openmct) { return function install(openmct) {
openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options)); openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options));

View File

@ -28,7 +28,6 @@ import {
} from 'utils/testing'; } from 'utils/testing';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { MODE } from './constants.js';
import TablePlugin from './plugin.js'; import TablePlugin from './plugin.js';
class MockDataTransfer { class MockDataTransfer {
@ -199,7 +198,7 @@ describe('the plugin', () => {
}, },
persistModeChange: true, persistModeChange: true,
rowLimit: 50, rowLimit: 50,
telemetryMode: MODE.PERFORMANCE telemetryMode: 'performance'
} }
}; };
const testTelemetry = [ const testTelemetry = [

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: true trailing: false
}); });
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,8 +181,6 @@ 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

@ -11,19 +11,18 @@
> >
<input <input
ref="startDate" ref="startDate"
v-model="formattedBounds.startDate" v-model="formattedBounds.start"
class="c-input--datetime" class="c-input--datetime"
type="text" type="text"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
aria-label="Start date" aria-label="Start date"
@input="validateInput('startDate')" @input="validateAllBounds('startDate')"
@change="reportValidity('startDate')"
/> />
<DatePicker <DatePicker
v-if="isUTCBased" v-if="isUTCBased"
class="c-ctrl-wrapper--menus-right" class="c-ctrl-wrapper--menus-right"
:default-date-time="formattedBounds.startDate" :default-date-time="formattedBounds.start"
:formatter="timeFormatter" :formatter="timeFormatter"
@date-selected="startDateSelected" @date-selected="startDateSelected"
/> />
@ -38,8 +37,7 @@
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
aria-label="Start time" aria-label="Start time"
@input="validateInput('startTime')" @input="validateAllBounds('startDate')"
@change="reportValidity('startTime')"
/> />
</div> </div>
@ -50,19 +48,18 @@
> >
<input <input
ref="endDate" ref="endDate"
v-model="formattedBounds.endDate" v-model="formattedBounds.end"
class="c-input--datetime" class="c-input--datetime"
type="text" type="text"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
aria-label="End date" aria-label="End date"
@input="validateInput('endDate')" @input="validateAllBounds('endDate')"
@change="reportValidity('endDate')"
/> />
<DatePicker <DatePicker
v-if="isUTCBased" v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left" class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.endDate" :default-date-time="formattedBounds.end"
:formatter="timeFormatter" :formatter="timeFormatter"
@date-selected="endDateSelected" @date-selected="endDateSelected"
/> />
@ -77,15 +74,14 @@
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
aria-label="End time" aria-label="End time"
@input="validateInput('endTime')" @input="validateAllBounds('endDate')"
@change="reportValidity('endTime')"
/> />
</div> </div>
<div class="pr-time-input pr-time-input--buttons"> <div class="pr-time-input pr-time-input--buttons">
<button <button
class="c-button c-button--major icon-check" class="c-button c-button--major icon-check"
:disabled="hasInputValidityError" :disabled="isDisabled"
aria-label="Submit time bounds" aria-label="Submit time bounds"
@click.prevent="handleFormSubmission(true)" @click.prevent="handleFormSubmission(true)"
></button> ></button>
@ -129,7 +125,6 @@ export default {
return { return {
timeFormatter: this.getFormatter(timeSystem.timeFormat), timeFormatter: this.getFormatter(timeSystem.timeFormat),
durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER), durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER),
timeSystemKey: timeSystem.key,
bounds: { bounds: {
start: bounds.start, start: bounds.start,
end: bounds.end end: bounds.end
@ -141,29 +136,9 @@ export default {
endTime: '' endTime: ''
}, },
isUTCBased: timeSystem.isUTCBased, isUTCBased: timeSystem.isUTCBased,
inputValidityMap: { isDisabled: false
startDate: { valid: true },
startTime: { valid: true },
endDate: { valid: true },
endTime: { valid: true }
},
logicalValidityMap: {
limit: { valid: true },
bounds: { valid: true }
}
}; };
}, },
computed: {
hasInputValidityError() {
return Object.values(this.inputValidityMap).some((isValid) => !isValid.valid);
},
hasLogicalValidationErrors() {
return Object.values(this.logicalValidityMap).some((isValid) => !isValid.valid);
},
isValid() {
return !this.hasInputValidityError && !this.hasLogicalValidationErrors;
}
},
watch: { watch: {
inputBounds: { inputBounds: {
handler(newBounds) { handler(newBounds) {
@ -193,17 +168,25 @@ export default {
this.setBounds(bounds); this.setBounds(bounds);
this.setViewFromBounds(bounds); this.setViewFromBounds(bounds);
}, },
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
if (input) {
input.setCustomValidity('');
input.title = '';
}
},
setBounds(bounds) { setBounds(bounds) {
this.bounds = bounds; this.bounds = bounds;
}, },
setViewFromBounds(bounds) { setViewFromBounds(bounds) {
this.formattedBounds.startDate = this.timeFormatter.format(bounds.start).split(' ')[0]; this.formattedBounds.start = this.timeFormatter.format(bounds.start).split(' ')[0];
this.formattedBounds.endDate = this.timeFormatter.format(bounds.end).split(' ')[0]; this.formattedBounds.end = this.timeFormatter.format(bounds.end).split(' ')[0];
this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start)); this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start));
this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end)); this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end));
}, },
setTimeSystem(timeSystem) { setTimeSystem(timeSystem) {
this.timeSystemKey = timeSystem.key;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat); this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter( this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
@ -218,10 +201,10 @@ export default {
setBoundsFromView(dismiss) { setBoundsFromView(dismiss) {
if (this.$refs.fixedDeltaInput.checkValidity()) { if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse( let start = this.timeFormatter.parse(
`${this.formattedBounds.startDate} ${this.formattedBounds.startTime}` `${this.formattedBounds.start} ${this.formattedBounds.startTime}`
); );
let end = this.timeFormatter.parse( let end = this.timeFormatter.parse(
`${this.formattedBounds.endDate} ${this.formattedBounds.endTime}` `${this.formattedBounds.end} ${this.formattedBounds.endTime}`
); );
this.$emit('update', { start, end }); this.$emit('update', { start, end });
@ -232,93 +215,96 @@ export default {
return false; return false;
} }
}, },
clearAllValidation() {
Object.keys(this.inputValidityMap).forEach(this.clearValidation);
},
clearValidation(refName) {
const input = this.getInput(refName);
input.setCustomValidity('');
input.title = '';
},
handleFormSubmission(shouldDismiss) { handleFormSubmission(shouldDismiss) {
this.validateLimit(); this.validateAllBounds('startDate');
this.reportValidity('limit'); this.validateAllBounds('endDate');
this.validateBounds();
this.reportValidity('bounds');
if (this.isValid) { if (!this.isDisabled) {
this.setBoundsFromView(shouldDismiss); this.setBoundsFromView(shouldDismiss);
} }
}, },
validateInput(refName) { validateAllBounds(ref) {
this.clearAllValidation(); this.isDisabled = false;
const inputType = refName.includes('Date') ? 'Date' : 'Time'; if (!this.areBoundsFormatsValid()) {
const formatter = inputType === 'Date' ? this.timeFormatter : this.durationFormatter; this.isDisabled = true;
const validationResult = formatter.validate(this.formattedBounds[refName]) return false;
? { valid: true }
: { valid: false, message: `Invalid ${inputType}` };
this.inputValidityMap[refName] = validationResult;
},
validateBounds() {
const bounds = {
start: this.timeFormatter.parse(
`${this.formattedBounds.startDate} ${this.formattedBounds.startTime}`
),
end: this.timeFormatter.parse(
`${this.formattedBounds.endDate} ${this.formattedBounds.endTime}`
)
};
this.logicalValidityMap.bounds = this.openmct.time.validateBounds(bounds);
},
validateLimit(bounds) {
const limit = this.configuration?.menuOptions
?.filter((option) => option.timeSystem === this.timeSystemKey)
?.find((option) => option.limit)?.limit;
if (this.isUTCBased && limit && bounds.end - bounds.start > limit) {
this.logicalValidityMap.limit = {
valid: false,
message: 'Start and end difference exceeds allowable limit'
};
} else {
this.logicalValidityMap.limit = { valid: true };
} }
},
reportValidity(refName) {
const input = this.getInput(refName);
const validationResult = this.inputValidityMap[refName] ?? this.logicalValidityMap[refName];
let validationResult = { valid: true };
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(
`${this.formattedBounds.start} ${this.formattedBounds.startTime}`
),
end: this.timeFormatter.parse(
`${this.formattedBounds.end} ${this.formattedBounds.endTime}`
)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: 'Start and end difference exceeds allowable limit'
};
}
} else if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
return this.handleValidationResults(input, validationResult);
});
},
areBoundsFormatsValid() {
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate =
input === this.$refs.startDate
? `${this.formattedBounds.start} ${this.formattedBounds.startTime}`
: `${this.formattedBounds.end} ${this.formattedBounds.endTime}`;
const validationResult = this.timeFormatter.validate(formattedDate)
? { valid: true }
: { valid: false, message: 'Invalid date' };
return this.handleValidationResults(input, validationResult);
});
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter((option) => option.timeSystem === this.timeSystem.key)
.find((option) => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) { if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message); input.setCustomValidity(validationResult.message);
input.title = validationResult.message; input.title = validationResult.message;
this.hasLogicalValidationErrors = true; this.isDisabled = true;
} else { } else {
input.setCustomValidity(''); input.setCustomValidity('');
input.title = ''; input.title = '';
} }
this.$refs.fixedDeltaInput.reportValidity(); this.$refs.fixedDeltaInput.reportValidity();
},
getInput(refName) {
if (Object.keys(this.inputValidityMap).includes(refName)) {
return this.$refs[refName];
}
return this.$refs.startDate; return validationResult.valid;
}, },
startDateSelected(date) { startDateSelected(date) {
this.formattedBounds.startDate = this.timeFormatter.format(date).split(' ')[0]; this.formattedBounds.start = this.timeFormatter.format(date).split(' ')[0];
this.validateInput('startDate'); this.validateAllBounds('startDate');
this.reportValidity('startDate');
}, },
endDateSelected(date) { endDateSelected(date) {
this.formattedBounds.endDate = this.timeFormatter.format(date).split(' ')[0]; this.formattedBounds.end = this.timeFormatter.format(date).split(' ')[0];
this.validateInput('endDate'); this.validateAllBounds('endDate');
this.reportValidity('endDate');
}, },
hide($event) { hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) { if ($event.target.className.indexOf('c-button icon-x') > -1) {

View File

@ -99,7 +99,6 @@ 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: $colorGaugeValue; // Color of needle in a needle gauge. $colorGaugeNeedle: rgba(#fff, 0);
$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: $colorGaugeValue; // Color of needle in a needle gauge. $colorGaugeNeedle: rgba(#fff, 0);
$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: $colorGaugeValue; // Color of needle in a needle gauge. $colorGaugeNeedle: rgba(#fff, 0);
$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

@ -79,7 +79,6 @@ export default {
const svgWidth = ref(0); const svgWidth = ref(0);
const svgHeight = ref(0); const svgHeight = ref(0);
const axisTransform = ref('translate(0,20)'); const axisTransform = ref('translate(0,20)');
const alignmentOffset = ref(0);
const nowMarkerStyle = reactive({ const nowMarkerStyle = reactive({
height: '0px', height: '0px',
left: '0px' left: '0px'
@ -101,7 +100,6 @@ export default {
svgWidth, svgWidth,
svgHeight, svgHeight,
axisTransform, axisTransform,
alignmentOffset,
nowMarkerStyle, nowMarkerStyle,
openmct openmct
}; };

View File

@ -1,13 +1,16 @@
/******************************************************** PROGRESS BAR */ /******************************************************** PROGRESS BAR */
@keyframes progressIndeterminate { @keyframes progressIndeterminate {
0% { 0% {
transform:scaleX(0); left: 0;
width: 0;
} }
90% { 70% {
transform:scaleX(1); left: 0;
width: 100%;
opacity: 1; opacity: 1;
} }
100% { 100% {
left: 100%;
opacity: 0; opacity: 0;
} }
} }
@ -21,10 +24,11 @@
&__bar { &__bar {
background: $colorProgressBar; background: $colorProgressBar;
transform-origin: left; height: 100%;
min-height: $progressBarMinH;
&.--indeterminate { &.--indeterminate {
@include abs(); position: absolute;
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-2024, United States Government as represented by the Open MCT, Copyright &copy; 2014-2023, 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>

View File

@ -32,7 +32,6 @@
<script> <script>
import mount from 'utils/mount'; import mount from 'utils/mount';
import { encode_url } from '../../utils/encoding';
import AboutDialog from './AboutDialog.vue'; import AboutDialog from './AboutDialog.vue';
export default { export default {
@ -40,7 +39,7 @@ export default {
mounted() { mounted() {
const branding = this.openmct.branding(); const branding = this.openmct.branding();
if (branding.smallLogoImage) { if (branding.smallLogoImage) {
this.$refs.aboutLogo.style.backgroundImage = `url('${encode_url(branding.smallLogoImage)}')`; this.$refs.aboutLogo.style.backgroundImage = `url('${branding.smallLogoImage}')`;
} }
}, },
methods: { methods: {

View File

@ -24,7 +24,6 @@
<div class="l-browse-bar__start"> <div class="l-browse-bar__start">
<button <button
v-if="hasParent" v-if="hasParent"
aria-label="Navigate up to parent"
class="l-browse-bar__nav-to-parent-button c-icon-button c-icon-button--major icon-arrow-nav-to-parent" class="l-browse-bar__nav-to-parent-button c-icon-button c-icon-button--major icon-arrow-nav-to-parent"
title="Navigate up to parent" title="Navigate up to parent"
@click="goToParent" @click="goToParent"
@ -37,7 +36,7 @@
ref="objectName" ref="objectName"
class="l-browse-bar__object-name c-object-label__name" class="l-browse-bar__object-name c-object-label__name"
:class="{ 'c-input-inline': isPersistable }" :class="{ 'c-input-inline': isPersistable }"
:contenteditable="isNameEditable" :contenteditable="isPersistable"
@blur="updateName" @blur="updateName"
@keydown.enter.prevent @keydown.enter.prevent
@keyup.enter.prevent="updateNameOnEnterKeyPress" @keyup.enter.prevent="updateNameOnEnterKeyPress"
@ -79,7 +78,7 @@
></button> ></button>
<button <button
v-if="shouldShowLock" v-if="isViewEditable & !isEditing"
:aria-label="lockedOrUnlockedTitle" :aria-label="lockedOrUnlockedTitle"
:title="lockedOrUnlockedTitle" :title="lockedOrUnlockedTitle"
:class="{ :class="{
@ -89,13 +88,6 @@
@click="toggleLock(!domainObject.locked)" @click="toggleLock(!domainObject.locked)"
></button> ></button>
<span
v-else-if="domainObject?.locked"
class="icon-lock"
aria-label="Locked for editing, cannot be unlocked."
title="Locked for editing, cannot be unlocked."
></span>
<button <button
v-if="isViewEditable && !isEditing && !domainObject.locked" v-if="isViewEditable && !isEditing && !domainObject.locked"
class="l-browse-bar__actions__edit c-button c-button--major icon-pencil" class="l-browse-bar__actions__edit c-button c-button--major icon-pencil"
@ -188,18 +180,6 @@ export default {
}; };
}, },
computed: { computed: {
isNameEditable() {
return this.isPersistable && !this.domainObject.locked;
},
shouldShowLock() {
if (this.domainObject === undefined) {
return false;
}
if (this.domainObject.disallowUnlock) {
return false;
}
return this.domainObject.locked || (this.isViewEditable && !this.isEditing);
},
statusClass() { statusClass() {
return this.status ? `is-status--${this.status}` : ''; return this.status ? `is-status--${this.status}` : '';
}, },
@ -273,19 +253,11 @@ export default {
return false; return false;
}, },
lockedOrUnlockedTitle() { lockedOrUnlockedTitle() {
let title;
if (this.domainObject.locked) { if (this.domainObject.locked) {
if (this.domainObject.lockedBy !== undefined) { return 'Locked for editing - click to unlock.';
title = `Locked for editing by ${this.domainObject.lockedBy}. `;
} else {
title = 'Locked for editing. ';
}
title += 'Click to unlock.';
} else { } else {
title = 'Unlocked for editing, click to lock.'; return 'Unlocked for editing - click to lock.';
} }
return title;
}, },
domainObjectName() { domainObjectName() {
return this.domainObject?.name ?? ''; return this.domainObject?.name ?? '';
@ -316,6 +288,7 @@ export default {
document.addEventListener('click', this.closeViewAndSaveMenu); document.addEventListener('click', this.closeViewAndSaveMenu);
this.promptUserbeforeNavigatingAway = this.promptUserbeforeNavigatingAway.bind(this); this.promptUserbeforeNavigatingAway = this.promptUserbeforeNavigatingAway.bind(this);
window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway); window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway);
this.openmct.editor.on('isEditing', (isEditing) => { this.openmct.editor.on('isEditing', (isEditing) => {
this.isEditing = isEditing; this.isEditing = isEditing;
}); });
@ -448,27 +421,8 @@ export default {
this.actionCollection.off('update', this.updateActionItems); this.actionCollection.off('update', this.updateActionItems);
delete this.actionCollection; delete this.actionCollection;
}, },
async toggleLock(flag) { toggleLock(flag) {
if (!this.domainObject.disallowUnlock) { this.openmct.objects.mutate(this.domainObject, 'locked', flag);
const wasTransactionActive = this.openmct.objects.isTransactionActive();
let transaction;
if (!wasTransactionActive) {
transaction = this.openmct.objects.startTransaction();
}
this.openmct.objects.mutate(this.domainObject, 'locked', flag);
const user = await this.openmct.user.getCurrentUser();
if (user !== undefined) {
this.openmct.objects.mutate(this.domainObject, 'lockedBy', user.id);
}
if (!wasTransactionActive) {
await transaction.commit();
this.openmct.objects.endTransaction();
}
}
}, },
setStatus(status) { setStatus(status) {
this.status = status; this.status = status;

Some files were not shown because too many files have changed in this diff Show More