mirror of
https://github.com/nasa/openmct.git
synced 2025-06-28 11:51:10 +00:00
Compare commits
2 Commits
eval-sourc
...
v4.0.0
Author | SHA1 | Date | |
---|---|---|---|
e664afd468 | |||
d786452abe |
@ -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:
|
||||||
|
15
.github/workflows/e2e-couchdb.yml
vendored
15
.github/workflows/e2e-couchdb.yml
vendored
@ -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()
|
||||||
|
2
.github/workflows/e2e-flakefinder.yml
vendored
2
.github/workflows/e2e-flakefinder.yml
vendored
@ -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)
|
||||||
|
2
.github/workflows/e2e-perf.yml
vendored
2
.github/workflows/e2e-perf.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/e2e-pr.yml
vendored
2
.github/workflows/e2e-pr.yml
vendored
@ -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
|
||||||
|
@ -15,5 +15,5 @@ export default merge(common, {
|
|||||||
__OPENMCT_ROOT_RELATIVE__: '""'
|
__OPENMCT_ROOT_RELATIVE__: '""'
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
devtool: 'eval-source-map'
|
devtool: 'source-map'
|
||||||
});
|
});
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
};
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 |
@ -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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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,17 +918,14 @@ 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'
|
||||||
});
|
});
|
||||||
|
if (!(await lockButton.isVisible())) {
|
||||||
await lockButton.isVisible();
|
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||||
// if (!(await lockButton.isVisible())) {
|
}
|
||||||
// await page.getByLabel('Focused Image Element').hover({ trial: true });
|
|
||||||
// }
|
|
||||||
await lockButton.click();
|
await lockButton.click();
|
||||||
|
|
||||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||||
@ -931,7 +974,6 @@ async function buttonZoomOnImageAndAssert(page) {
|
|||||||
|
|
||||||
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
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
|
||||||
*/
|
*/
|
||||||
|
@ -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);
|
|
||||||
}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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', () => {
|
||||||
|
@ -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 });
|
||||||
|
|
||||||
|
@ -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 page.getByLabel('Start date').fill(invalidStartDate);
|
||||||
await expect(submitButtonLocator).toBeDisabled();
|
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||||
await page.getByLabel('Start date').fill(initialStartDate);
|
await page.getByLabel('Start date').fill(startDate);
|
||||||
await expect(submitButtonLocator).toBeEnabled();
|
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('invalid start time disables submit button', async () => {
|
// Test invalid end date
|
||||||
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
const invalidEndDate = `${year - 1}-12-31`;
|
||||||
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
|
||||||
|
|
||||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
|
||||||
await expect(submitButtonLocator).toBeDisabled();
|
|
||||||
await page.getByLabel('Start time').fill(initialStartTime);
|
|
||||||
await expect(submitButtonLocator).toBeEnabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('disable/enable submit button also works with multiple invalid inputs', async () => {
|
|
||||||
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 page.getByLabel('End date').fill(invalidEndDate);
|
||||||
await expect(submitButtonLocator).toBeDisabled();
|
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||||
await page.getByLabel('End date').fill(initialEndDate);
|
await page.getByLabel('End date').fill(endDate);
|
||||||
await expect(submitButtonLocator).toBeDisabled();
|
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||||
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)}`;
|
|
||||||
|
|
||||||
|
// Test invalid start time
|
||||||
|
const invalidStartTime = '42:00:00';
|
||||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||||
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
|
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||||
await page.getByLabel('Start time').press('Tab');
|
await page.getByLabel('Start time').fill(startTime);
|
||||||
await expect(page.getByLabel('Start time')).toHaveAttribute('title', 'Invalid Time');
|
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||||
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)}`;
|
|
||||||
|
|
||||||
|
// Test invalid end time
|
||||||
|
const invalidEndTime = '43:00:00';
|
||||||
await page.getByLabel('End time').fill(invalidEndTime);
|
await page.getByLabel('End time').fill(invalidEndTime);
|
||||||
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
|
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||||
await page.getByLabel('End time').press('Tab');
|
await page.getByLabel('End time').fill(endTime);
|
||||||
await expect(page.getByLabel('End time')).toHaveAttribute('title', 'Invalid Time');
|
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||||
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 }) => {
|
// Submit valid time bounds
|
||||||
// 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.
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
@ -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')
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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
291
package-lock.json
generated
@ -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",
|
||||||
|
11
package.json
11
package.json
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
|
||||||
this.#throttleRate = throttleRate;
|
|
||||||
this.#sendThrottleRateToWorker(this.#throttleRate);
|
|
||||||
}
|
}
|
||||||
setThrottleMessagePattern(throttleMessagePattern) {
|
setMaxBatchWait(wait) {
|
||||||
|
this.#maxBatchWait = wait;
|
||||||
|
this.#sendBatchWaitToWorker(this.#maxBatchWait);
|
||||||
|
}
|
||||||
|
#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 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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')) {
|
||||||
|
const bounds = options.timeContext?.getBounds();
|
||||||
|
if (bounds?.start) {
|
||||||
options.start = options.timeContext.getBounds().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')) {
|
||||||
|
const bounds = options.timeContext?.getBounds();
|
||||||
|
if (bounds?.end) {
|
||||||
options.end = options.timeContext.getBounds().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;
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
const messageToConsider = this.#buffer[i];
|
|
||||||
if (this.#shouldThrottle(messageToConsider)) {
|
|
||||||
this.#buffer.splice(i, 1);
|
|
||||||
this.#currentBufferLength -= messageToConsider.length;
|
|
||||||
this.#dropped = true;
|
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Applies the `shouldBatchMessage` function from the supplied batching strategy
|
||||||
|
* 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.
|
||||||
|
* @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
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
@ -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 };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
|
@ -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 }">
|
||||||
|
@ -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,
|
||||||
|
@ -720,57 +720,31 @@ 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;
|
|
||||||
|
|
||||||
// Simulate the passage of time and no data received
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
||||||
|
|
||||||
expect(mockListener).toHaveBeenCalledWith({
|
expect(mockListener).toHaveBeenCalledWith({
|
||||||
output: 'Any old telemetry',
|
output: 'Any old telemetry',
|
||||||
id: {
|
id: {
|
||||||
@ -780,9 +754,16 @@ describe('the plugin', function () {
|
|||||||
conditionId: '39584410-cbf9-499e-96dc-76f27e69885d',
|
conditionId: '39584410-cbf9-499e-96dc-76f27e69885d',
|
||||||
utc: undefined
|
utc: undefined
|
||||||
});
|
});
|
||||||
|
done();
|
||||||
|
}, 400);
|
||||||
});
|
});
|
||||||
|
|
||||||
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,16 +782,8 @@ 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;
|
|
||||||
|
|
||||||
// Simulate receiving telemetry data
|
|
||||||
onAddCallback([testDatum]);
|
|
||||||
|
|
||||||
// Wait a bit for the condition manager to process the data
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
expect(mockListener).toHaveBeenCalledWith({
|
expect(mockListener).toHaveBeenCalledWith({
|
||||||
output: 'Default',
|
output: 'Default',
|
||||||
id: {
|
id: {
|
||||||
@ -861,6 +793,8 @@ describe('the plugin', function () {
|
|||||||
conditionId: '2532d90a-e0d6-4935-b546-3123522da2de',
|
conditionId: '2532d90a-e0d6-4935-b546-3123522da2de',
|
||||||
utc: date
|
utc: date
|
||||||
});
|
});
|
||||||
|
done();
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -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');
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: {
|
||||||
|
@ -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 {
|
||||||
|
const data = await this.openmct.forms.showForm(formStructure);
|
||||||
faults.forEach((fault) => {
|
faults.forEach((fault) => {
|
||||||
this.openmct.faults.acknowledgeFault(fault, data);
|
this.openmct.faults.acknowledgeFault(fault, data);
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
this.resetSelectedFaultMap();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resetSelectedFaultMap() {
|
resetSelectedFaultMap() {
|
||||||
Object.keys(this.selectedFaultMap).forEach((key) => {
|
Object.keys(this.selectedFaultMap).forEach((key) => {
|
||||||
@ -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 = {};
|
||||||
},
|
},
|
||||||
|
@ -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'];
|
||||||
|
@ -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) {
|
||||||
|
@ -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' : ''}`;
|
||||||
},
|
},
|
||||||
|
@ -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, {
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
return existingDatumTimestamp !== datumToRemoveTimestamp;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.imageHistory = this.imageHistory.filter((image) => {
|
return shouldKeep;
|
||||||
const imageTimestamp = this.parseTime(image);
|
|
||||||
|
|
||||||
return !removedTimestamps[imageTimestamp];
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setDataTimeContext() {
|
setDataTimeContext() {
|
||||||
|
@ -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',
|
||||||
|
@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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) {
|
let keys = Object.keys(styleObj);
|
||||||
elemToStyle.classList.toggle(STYLE_CONSTANTS.isStyleInvisible, styleObj.isStyleInvisible);
|
|
||||||
styleObj.isStyleInvisible = null;
|
keys.forEach((key) => {
|
||||||
|
if (elemToStyle) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(styleObj).forEach(([key, value]) => {
|
elemToStyle.style[key] = styleObj[key];
|
||||||
if (typeof value !== 'string' || !value.includes('__no_value')) {
|
}
|
||||||
elemToStyle.style[key] = value;
|
|
||||||
} else {
|
|
||||||
elemToStyle.style[key] = ''; // remove the property
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -152,20 +152,17 @@ export default class RemoteClock extends DefaultClock {
|
|||||||
*/
|
*/
|
||||||
#waitForReady() {
|
#waitForReady() {
|
||||||
const waitForInitialTick = (resolve) => {
|
const waitForInitialTick = (resolve) => {
|
||||||
const tickListener = () => {
|
|
||||||
if (this.lastTick > 0) {
|
if (this.lastTick > 0) {
|
||||||
const offsets = this.openmct.time.getClockOffsets();
|
const offsets = this.openmct.time.getClockOffsets();
|
||||||
this.openmct.time.off('tick', tickListener); // Unregister the tick listener
|
|
||||||
resolve({
|
resolve({
|
||||||
start: this.lastTick + offsets.start,
|
start: this.lastTick + offsets.start,
|
||||||
end: this.lastTick + offsets.end
|
end: this.lastTick + offsets.end
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
setTimeout(() => waitForInitialTick(resolve), 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.openmct.time.on('tick', tickListener);
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise(waitForInitialTick);
|
return new Promise(waitForInitialTick);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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.
|
|
||||||
if (!this.internalDomainObject.locked) {
|
|
||||||
this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index);
|
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);
|
||||||
|
@ -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);
|
|
||||||
|
|
||||||
if (this.telemetryMode === MODE.PERFORMANCE) {
|
|
||||||
this.tableRows.setSortOptions(sortOptions);
|
|
||||||
this.clearAndResubscribe();
|
|
||||||
} else {
|
|
||||||
this.tableRows.sortBy(sortOptions);
|
this.tableRows.sortBy(sortOptions);
|
||||||
|
|
||||||
|
if (this.openmct.editor.isEditing()) {
|
||||||
|
let configuration = this.configuration.getConfiguration();
|
||||||
|
configuration.sortOptions = sortOptions;
|
||||||
|
this.configuration.updateConfiguration(configuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 || {};
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -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);
|
||||||
|
@ -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.'
|
||||||
);
|
);
|
||||||
|
@ -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) {
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
const ORDER = {
|
|
||||||
ASCENDING: 'asc',
|
|
||||||
DESCENDING: 'desc'
|
|
||||||
};
|
|
||||||
|
|
||||||
const MODE = {
|
|
||||||
PERFORMANCE: 'performance',
|
|
||||||
UNLIMITED: 'unlimited'
|
|
||||||
};
|
|
||||||
|
|
||||||
export { MODE, ORDER };
|
|
@ -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));
|
||||||
|
@ -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 = [
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
let validationResult = { valid: true };
|
||||||
},
|
const currentInput = this.$refs[ref];
|
||||||
validateBounds() {
|
|
||||||
const bounds = {
|
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
|
||||||
|
let boundsValues = {
|
||||||
start: this.timeFormatter.parse(
|
start: this.timeFormatter.parse(
|
||||||
`${this.formattedBounds.startDate} ${this.formattedBounds.startTime}`
|
`${this.formattedBounds.start} ${this.formattedBounds.startTime}`
|
||||||
),
|
),
|
||||||
end: this.timeFormatter.parse(
|
end: this.timeFormatter.parse(
|
||||||
`${this.formattedBounds.endDate} ${this.formattedBounds.endTime}`
|
`${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;
|
||||||
|
|
||||||
this.logicalValidityMap.bounds = this.openmct.time.validateBounds(bounds);
|
if (this.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) {
|
||||||
},
|
if (input === currentInput) {
|
||||||
validateLimit(bounds) {
|
validationResult = {
|
||||||
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,
|
valid: false,
|
||||||
message: 'Start and end difference exceeds allowable limit'
|
message: 'Start and end difference exceeds allowable limit'
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
this.logicalValidityMap.limit = { valid: true };
|
|
||||||
}
|
}
|
||||||
},
|
} else if (input === currentInput) {
|
||||||
reportValidity(refName) {
|
validationResult = this.openmct.time.validateBounds(boundsValues);
|
||||||
const input = this.getInput(refName);
|
}
|
||||||
const validationResult = this.inputValidityMap[refName] ?? this.logicalValidityMap[refName];
|
|
||||||
|
|
||||||
|
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) {
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 © 2014-2024, United States Government as represented by the
|
Open MCT, Copyright © 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>
|
||||||
|
@ -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: {
|
||||||
|
@ -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 {
|
} else {
|
||||||
title = 'Locked for editing. ';
|
return 'Unlocked for editing - click to lock.';
|
||||||
}
|
}
|
||||||
title += 'Click to unlock.';
|
|
||||||
} else {
|
|
||||||
title = '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) {
|
|
||||||
const wasTransactionActive = this.openmct.objects.isTransactionActive();
|
|
||||||
let transaction;
|
|
||||||
|
|
||||||
if (!wasTransactionActive) {
|
|
||||||
transaction = this.openmct.objects.startTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.openmct.objects.mutate(this.domainObject, 'locked', flag);
|
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
Reference in New Issue
Block a user