mirror of
https://github.com/nasa/openmct.git
synced 2025-06-24 18:25:19 +00:00
Compare commits
46 Commits
telemetry-
...
master
Author | SHA1 | Date | |
---|---|---|---|
f4637b8ac7 | |||
826c7134b5 | |||
fa1a45b6cd | |||
10bc8eb55d | |||
2e7fb94dd5 | |||
573bbb041e | |||
6a450a0e89 | |||
e5631c9f6c | |||
15b5d1405d | |||
0377788533 | |||
28dcff7e89 | |||
b191eb9d64 | |||
f8e4aba922 | |||
28f6987dd7 | |||
f3047093d6 | |||
34e57ef300 | |||
88e6557782 | |||
dbd4abebae | |||
50ca27e54f | |||
2955092c86 | |||
28b5d7c41c | |||
ecd120387c | |||
a6517bb33e | |||
1fde0d9e38 | |||
5be103ea72 | |||
d74e1b19b6 | |||
5bb6a18cd4 | |||
14b947c101 | |||
61b982ab99 | |||
ba4d8a428b | |||
ea9947cab5 | |||
2010f2e377 | |||
3241e9ba57 | |||
057a5f997c | |||
078cd341a5 | |||
518b55cf0f | |||
3e23dceb64 | |||
7f8b5e09e5 | |||
7c2bb16bfd | |||
890ddcac4e | |||
d8c5095ebb | |||
ccf7ed91af | |||
2b8673941a | |||
703186adf1 | |||
c43ef64733 | |||
f4cf9c756b |
@ -5,7 +5,7 @@ orbs:
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.47.2-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.48.1-focal
|
||||
environment:
|
||||
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
|
||||
@ -198,7 +198,7 @@ jobs:
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
- run: npx playwright@1.47.2 install #Necessary for bare ubuntu machine
|
||||
- run: npx playwright@1.48.1 install #Necessary for bare ubuntu machine
|
||||
- run: |
|
||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||
@ -286,8 +286,8 @@ workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
- lint:
|
||||
name: node20-lint
|
||||
node-version: lts/iron
|
||||
name: node22-lint
|
||||
node-version: '22'
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: lts/hydrogen
|
||||
@ -304,8 +304,8 @@ workflows:
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
name: node20-chrome-nightly
|
||||
node-version: lts/iron
|
||||
name: node22-chrome-nightly
|
||||
node-version: '22'
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: lts/hydrogen
|
||||
|
@ -484,7 +484,8 @@
|
||||
"darkmatter",
|
||||
"Undeletes",
|
||||
"SSSZ",
|
||||
"LOCF"
|
||||
"pageerror",
|
||||
"annotatable"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
|
||||
"ignorePaths": [
|
||||
|
@ -14,8 +14,7 @@ const config = {
|
||||
__OPENMCT_VERSION__: 'readonly',
|
||||
__OPENMCT_BUILD_DATE__: 'readonly',
|
||||
__OPENMCT_REVISION__: 'readonly',
|
||||
__OPENMCT_BUILD_BRANCH__: 'readonly',
|
||||
__OPENMCT_ROOT_RELATIVE__: 'readonly'
|
||||
__OPENMCT_BUILD_BRANCH__: 'readonly'
|
||||
},
|
||||
plugins: ['prettier', 'unicorn', 'simple-import-sort'],
|
||||
extends: [
|
||||
|
14
.github/workflows/e2e-couchdb.yml
vendored
14
.github/workflows/e2e-couchdb.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- run: npx playwright@1.47.2 install
|
||||
- run: npx playwright@1.48.1 install
|
||||
|
||||
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
||||
run: |
|
||||
@ -51,12 +51,12 @@ jobs:
|
||||
env:
|
||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
|
||||
run: npm run test:e2e:couchdb
|
||||
|
||||
|
||||
- name: Generate Code Coverage Report
|
||||
run: npm run cov:e2e:report
|
||||
|
||||
- name: Publish Results to Codecov.io
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage/e2e/lcov.info
|
||||
@ -66,15 +66,19 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-couchdb-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Archive html test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-couchdb-html-test-results
|
||||
path: html-test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:couchdb label (if present)
|
||||
if: always()
|
||||
|
6
.github/workflows/e2e-flakefinder.yml
vendored
6
.github/workflows/e2e-flakefinder.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.47.2 install
|
||||
- run: npx playwright@1.48.1 install
|
||||
- run: npm ci --no-audit --progress=false
|
||||
|
||||
- name: Run E2E Tests (Repeated 10 Times)
|
||||
@ -38,9 +38,11 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-flakefinder-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:flakefinder label (if present)
|
||||
if: always()
|
||||
|
6
.github/workflows/e2e-perf.yml
vendored
6
.github/workflows/e2e-perf.yml
vendored
@ -28,16 +28,18 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.47.2 install
|
||||
- run: npx playwright@1.48.1 install
|
||||
- run: npm ci --no-audit --progress=false
|
||||
- run: npm run test:perf:localhost
|
||||
- run: npm run test:perf:contract
|
||||
- run: npm run test:perf:memory
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-perf-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:perf label (if present)
|
||||
if: always()
|
||||
|
4
.github/workflows/e2e-pr.yml
vendored
4
.github/workflows/e2e-pr.yml
vendored
@ -45,9 +45,11 @@ jobs:
|
||||
npm run cov:e2e:full:publish
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-pr-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e label (if present)
|
||||
if: always()
|
||||
|
116
.github/workflows/release.yml
vendored
116
.github/workflows/release.yml
vendored
@ -1,116 +0,0 @@
|
||||
# GitHub Actions Workflow for Automated Releases
|
||||
|
||||
name: Automated Release Workflow
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Nightly builds at 6 PM PST every day
|
||||
- cron: '0 2 * * *'
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
- published
|
||||
|
||||
jobs:
|
||||
nightly-build:
|
||||
if: github.event_name == 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
name: Nightly Build and Release
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/iron' # Specify your Node.js version
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Bump Version for Nightly
|
||||
id: bump_version
|
||||
run: |
|
||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
||||
DATE=$(date +%Y%m%d)
|
||||
NIGHTLY_VERSION=$(echo $PACKAGE_VERSION | awk -F. -v OFS=. '{$NF+=1; print}')-nightly-$DATE
|
||||
echo "NIGHTLY_VERSION=${NIGHTLY_VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Update package.json
|
||||
run: |
|
||||
npm version $NIGHTLY_VERSION --no-git-tag-version
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add package.json
|
||||
git commit -m "chore: bump version to $NIGHTLY_VERSION for nightly build"
|
||||
|
||||
- name: Push Changes
|
||||
uses: ad-m/github-push-action@v0.6.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ github.ref }}
|
||||
|
||||
- name: Build Project
|
||||
run: npm run build:prod
|
||||
|
||||
- name: Publish Nightly to NPM
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
npm publish --access public --tag nightly
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
prerelease-build:
|
||||
if: github.event.release.prerelease == true
|
||||
runs-on: ubuntu-latest
|
||||
name: Pre-release (Beta) Build and Publish
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16' # Specify your Node.js version
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build Project
|
||||
run: npm run build:prod
|
||||
|
||||
- name: Publish Beta to NPM
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
npm publish --access public --tag beta
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
stable-release-build:
|
||||
if: github.event.release.prerelease == false
|
||||
runs-on: ubuntu-latest
|
||||
name: Stable Release Build and Publish
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16' # Specify your Node.js version
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build Project
|
||||
run: npm run build:prod
|
||||
|
||||
- name: Publish to NPM
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
@ -48,7 +48,6 @@ const config = {
|
||||
generatorWorker: './example/generator/generatorWorker.js',
|
||||
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
|
||||
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
|
||||
compsMathWorker: './src/plugins/comps/CompsMathWorker.js',
|
||||
espressoTheme: './src/plugins/themes/espresso-theme.scss',
|
||||
snowTheme: './src/plugins/themes/snow-theme.scss',
|
||||
darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss'
|
||||
@ -90,8 +89,7 @@ const config = {
|
||||
__OPENMCT_REVISION__: `'${gitRevision}'`,
|
||||
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,
|
||||
__VUE_OPTIONS_API__: true, // enable/disable Options API support, default: true
|
||||
__VUE_PROD_DEVTOOLS__: false, // enable/disable devtools support in production, default: false
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // enable/disable hydration mismatch details in production, default: false
|
||||
__VUE_PROD_DEVTOOLS__: false // enable/disable devtools support in production, default: false
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
|
12
API.md
12
API.md
@ -31,6 +31,10 @@
|
||||
- [`latest` request strategy](#latest-request-strategy)
|
||||
- [`minmax` request strategy](#minmax-request-strategy)
|
||||
- [Telemetry Formats](#telemetry-formats)
|
||||
- [Built-in Formats](#built-in-formats)
|
||||
- [**Number Format (default):**](#number-format-default)
|
||||
- [**String Format**](#string-format)
|
||||
- [**Enum Format**](#enum-format)
|
||||
- [Registering Formats](#registering-formats)
|
||||
- [Telemetry Data](#telemetry-data)
|
||||
- [Telemetry Datums](#telemetry-datums)
|
||||
@ -59,6 +63,12 @@
|
||||
- [Custom Indicators](#custom-indicators)
|
||||
- [Priority API](#priority-api)
|
||||
- [Priority Types](#priority-types)
|
||||
- [User API](#user-api)
|
||||
- [Example](#example)
|
||||
- [Visibility-Based Rendering in View Providers](#visibility-based-rendering-in-view-providers)
|
||||
- [Overview](#overview)
|
||||
- [Implementing Visibility-Based Rendering](#implementing-visibility-based-rendering)
|
||||
- [Example](#example-1)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
@ -1301,9 +1311,11 @@ Open MCT provides some built-in priority values that can be used in the applicat
|
||||
|
||||
Currently, the Open MCT Priority API provides (type: numeric value):
|
||||
|
||||
- HIGHEST: Infinity
|
||||
- HIGH: 1000
|
||||
- Default: 0
|
||||
- LOW: -1000
|
||||
- LOWEST: -Infinity
|
||||
|
||||
View provider Example:
|
||||
|
||||
|
@ -1,6 +1,10 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass
|
||||
|
||||
# Disabling annotations for now. They are incorrectly labelling lines as lacking coverage when they are in fact covered by tests.
|
||||
github_checks:
|
||||
annotations: false
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
|
@ -2,7 +2,6 @@
|
||||
module.exports = {
|
||||
extends: ['plugin:playwright/recommended'],
|
||||
rules: {
|
||||
'playwright/max-nested-describe': ['error', { max: 1 }],
|
||||
'playwright/expect-expect': 'off'
|
||||
},
|
||||
overrides: [
|
||||
|
@ -68,7 +68,11 @@ import { v4 as genUuid } from 'uuid';
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [options.parent='mine'] - The Identifier or uuid of the parent object. Defaults to 'mine' folder
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||
*/
|
||||
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
|
||||
async function createDomainObjectWithDefaults(
|
||||
page,
|
||||
{ type, name, parent = 'mine' },
|
||||
additionalOptions = {}
|
||||
) {
|
||||
if (!name) {
|
||||
name = `${type}:${genUuid()}`;
|
||||
}
|
||||
@ -89,6 +93,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
||||
await page.getByLabel('Title', { exact: true }).fill('');
|
||||
await page.getByLabel('Title', { exact: true }).fill(name);
|
||||
|
||||
if (additionalOptions) {
|
||||
for (const [key, value] of Object.entries(additionalOptions)) {
|
||||
// eslint-disable-next-line playwright/no-raw-locators
|
||||
await page.locator(`#form-${key}`).fill(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (page.testNotes) {
|
||||
// Fill the "Notes" section with information about the
|
||||
// currently running test and its project.
|
||||
@ -105,7 +116,7 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
||||
|
||||
if (await _isInEditMode(page, uuid)) {
|
||||
// Save (exit edit mode)
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
}
|
||||
|
||||
@ -510,6 +521,10 @@ async function setTimeConductorBounds(page, { submitChanges = true, ...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);
|
||||
|
||||
if (startDate) {
|
||||
await page.getByLabel('Start date').fill(startDate);
|
||||
}
|
||||
@ -678,6 +693,21 @@ async function linkParameterToObject(page, parameterName, objectName) {
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the currently viewed `domainObject` from the browse bar.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function renameCurrentObjectFromBrowseBar(page, newName) {
|
||||
const nameInput = page.getByLabel('Browse bar object name');
|
||||
await nameInput.click();
|
||||
await nameInput.fill('');
|
||||
await nameInput.fill(newName);
|
||||
// Click the browse bar container to save changes
|
||||
await page.getByLabel('Browse bar', { exact: true }).click();
|
||||
}
|
||||
|
||||
export {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
@ -689,6 +719,7 @@ export {
|
||||
linkParameterToObject,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
navigateToObjectWithRealTime,
|
||||
renameCurrentObjectFromBrowseBar,
|
||||
setEndOffset,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
|
@ -103,25 +103,40 @@ const extendedTest = test.extend({
|
||||
* Default: `true`
|
||||
*/
|
||||
failOnConsoleError: [true, { option: true }],
|
||||
ignore404s: [[], { option: true }],
|
||||
/**
|
||||
* Extends the base page class to enable console log error detection.
|
||||
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
|
||||
*/
|
||||
page: async ({ page, failOnConsoleError }, use) => {
|
||||
page: async ({ page, failOnConsoleError, ignore404s }, use) => {
|
||||
// Capture any console errors during test execution
|
||||
const messages = [];
|
||||
let messages = [];
|
||||
page.on('console', (msg) => messages.push(msg));
|
||||
|
||||
await use(page);
|
||||
|
||||
if (ignore404s.length > 0) {
|
||||
messages = messages.filter((msg) => {
|
||||
let keep = true;
|
||||
|
||||
if (msg.text().match(/404 \((Object )?Not Found\)/) !== null) {
|
||||
keep = ignore404s.every((ignoreRule) => {
|
||||
return msg.location().url.match(ignoreRule) === null;
|
||||
});
|
||||
}
|
||||
|
||||
return keep;
|
||||
});
|
||||
}
|
||||
|
||||
// Assert against console errors during teardown
|
||||
if (failOnConsoleError) {
|
||||
messages.forEach((msg) =>
|
||||
messages.forEach((msg) => {
|
||||
// eslint-disable-next-line playwright/no-standalone-expect
|
||||
expect
|
||||
.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)
|
||||
.not.toEqual('error')
|
||||
);
|
||||
.not.toEqual('error');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -129,6 +129,7 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl
|
||||
*/
|
||||
export function getEarliestStartTime(planJson) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
|
||||
return Math.min(...activities.map((activity) => activity.start));
|
||||
}
|
||||
|
||||
@ -139,6 +140,7 @@ export function getEarliestStartTime(planJson) {
|
||||
*/
|
||||
export function getLatestEndTime(planJson) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
|
||||
return Math.max(...activities.map((activity) => activity.end));
|
||||
}
|
||||
|
||||
@ -151,6 +153,7 @@ export function getFirstActivity(planJson) {
|
||||
const groups = Object.keys(planJson);
|
||||
const firstGroupKey = groups[0];
|
||||
const firstGroupItems = planJson[firstGroupKey];
|
||||
|
||||
return firstGroupItems[0];
|
||||
}
|
||||
|
||||
@ -221,7 +224,7 @@ export async function createTimelistWithPlanAndSetActivityInProgress(page, planJ
|
||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||
|
||||
// Find the display properties section in the inspector
|
||||
await page.getByRole('tab', { name: 'View Properties' }).click();
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
// Switch to expanded view and save the setting
|
||||
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
|
||||
|
||||
|
@ -76,6 +76,7 @@ export async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
|
||||
export async function testTelemetryItem(page, telemetryItem) {
|
||||
// Check that telemetry item also received the tag
|
||||
await page.goto(telemetryItem.url);
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
|
||||
@ -93,6 +94,7 @@ export async function testTelemetryItem(page, telemetryItem) {
|
||||
y: 100
|
||||
}
|
||||
});
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
await expect(page.getByText('Science')).toBeVisible();
|
||||
await expect(page.getByText('Driving')).toBeHidden();
|
||||
@ -107,6 +109,8 @@ export async function basicTagsTests(page) {
|
||||
// Search for Driving
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
// Clicking elsewhere should cause annotation selection to be cleared
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
//
|
||||
@ -119,6 +123,8 @@ export async function basicTagsTests(page) {
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
// Delete Driving Tag
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Remove tag Driving"]').click();
|
||||
@ -155,6 +161,8 @@ export async function basicTagsTests(page) {
|
||||
}
|
||||
});
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
//Expect Science to be visible but Driving to be hidden
|
||||
await expect(page.getByText('Science')).toBeVisible();
|
||||
await expect(page.getByText('Driving')).toBeHidden();
|
||||
@ -170,7 +178,7 @@ export async function basicTagsTests(page) {
|
||||
});
|
||||
|
||||
// Add Driving Tag again
|
||||
await page.getByText('Annotations').click();
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
await page.getByRole('button', { name: /Add Tag/ }).click();
|
||||
await page.getByPlaceholder('Type to select tag').click();
|
||||
await page.getByText('Driving').click();
|
||||
|
@ -16,7 +16,7 @@
|
||||
"devDependencies": {
|
||||
"@percy/cli": "1.27.4",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.47.2",
|
||||
"@playwright/test": "1.48.1",
|
||||
"@axe-core/playwright": "4.8.5"
|
||||
},
|
||||
"author": {
|
||||
|
BIN
e2e/test-data/rick space roll.jpg
Normal file
BIN
e2e/test-data/rick space roll.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,67 @@
|
||||
/*****************************************************************************
|
||||
* 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 { createDomainObjectWithDefaults, setRealTimeMode } from '../../../appActions.js';
|
||||
import { MISSION_TIME } from '../../../constants.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const TELEMETRY_RATE = 2500;
|
||||
|
||||
test.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Event Message Generator with Acknowledge'
|
||||
});
|
||||
});
|
||||
|
||||
test('Rows are updatable in place', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7938'
|
||||
});
|
||||
|
||||
await test.step('First telemetry datum gets added as new row', async () => {
|
||||
await page.clock.fastForward(TELEMETRY_RATE);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK');
|
||||
});
|
||||
|
||||
await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => {
|
||||
await page.clock.fastForward(TELEMETRY_RATE * 2);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(acknowledgeCell).toHaveAttribute('title', 'OK');
|
||||
});
|
||||
});
|
||||
});
|
@ -132,7 +132,7 @@ test("View a timelist in expanded view, verify all the activities are displayed
|
||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||
|
||||
// Find the display properties section in the inspector
|
||||
await page.getByRole('tab', { name: 'View Properties' }).click();
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
// Switch to expanded view and save the setting
|
||||
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
|
||||
|
||||
|
@ -54,8 +54,7 @@ const examplePlanSmall1 = JSON.parse(
|
||||
const TIME_TO_FROM_COLUMN = 2;
|
||||
const HEADER_ROW = 0;
|
||||
const NUM_COLUMNS = 5;
|
||||
const FULL_CIRCLE_PATH =
|
||||
'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';
|
||||
const FULL_CIRCLE_PATH = 'M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z';
|
||||
|
||||
/**
|
||||
* The regular expression used to parse the countdown string.
|
||||
|
@ -24,7 +24,9 @@ import {
|
||||
createDomainObjectWithDefaults,
|
||||
createPlanFromJSON,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
setFixedIndependentTimeConductorBounds
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
setTimeConductorBounds
|
||||
} from '../../../appActions.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
@ -74,21 +76,14 @@ const testPlan = {
|
||||
};
|
||||
|
||||
test.describe('Time Strip', () => {
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5627'
|
||||
});
|
||||
|
||||
// Constant locators
|
||||
const activityBounds = page.locator('.activity-bounds');
|
||||
let timestrip;
|
||||
let plan;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Goto baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const timestrip = await test.step('Create a Time Strip', async () => {
|
||||
timestrip = await test.step('Create a Time Strip', async () => {
|
||||
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
||||
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||
expect(objectName).toBe(createdTimeStrip.name);
|
||||
@ -96,7 +91,7 @@ test.describe('Time Strip', () => {
|
||||
return createdTimeStrip;
|
||||
});
|
||||
|
||||
const plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||
plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||
const createdPlan = await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan
|
||||
@ -110,6 +105,22 @@ test.describe('Time Strip', () => {
|
||||
.dragTo(page.getByLabel('Object View'));
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
return createdPlan;
|
||||
});
|
||||
});
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5627'
|
||||
});
|
||||
|
||||
// Constant locators
|
||||
const activityBounds = page.locator('.activity-bounds');
|
||||
|
||||
await test.step('Set time strip to fixed timespan mode and verify activities', async () => {
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
|
||||
@ -119,8 +130,6 @@ test.describe('Time Strip', () => {
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.locator('.activity-bounds').count();
|
||||
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
||||
|
||||
return createdPlan;
|
||||
});
|
||||
|
||||
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
|
||||
@ -177,4 +186,48 @@ test.describe('Time Strip', () => {
|
||||
expect(await activityBounds.count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Time strip now line', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7817'
|
||||
});
|
||||
|
||||
await test.step('Is displayed in realtime mode', async () => {
|
||||
await expect(page.getByLabel('Now Marker')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Is hidden when out of bounds of the time axis', async () => {
|
||||
// Switch to fixed timespan mode
|
||||
await setFixedTimeMode(page);
|
||||
// Get the end bounds
|
||||
const endBounds = await page.getByLabel('End bounds').textContent();
|
||||
|
||||
// Add 2 minutes to end bound datetime and use it as the new end time
|
||||
let endTimeStamp = new Date(endBounds);
|
||||
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() + 2);
|
||||
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||
const milliseconds = endTimeStamp.getMilliseconds();
|
||||
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||
|
||||
// Subtract 1 minute from the end bound and use it as the new start time
|
||||
let startTimeStamp = new Date(endBounds);
|
||||
startTimeStamp.setUTCMinutes(startTimeStamp.getUTCMinutes() + 1);
|
||||
const startDate = startTimeStamp.toISOString().split('T')[0];
|
||||
const startMilliseconds = startTimeStamp.getMilliseconds();
|
||||
const startTime = startTimeStamp
|
||||
.toISOString()
|
||||
.split('T')[1]
|
||||
.replace(`.${startMilliseconds}Z`, '');
|
||||
// Set fixed timespan mode to the future so that "now" is out of bounds.
|
||||
await setTimeConductorBounds(page, {
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime
|
||||
});
|
||||
|
||||
await expect(page.getByLabel('Now Marker')).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,110 +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 {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
setRealTimeMode
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Comps', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Basic Functionality Works', async ({ page, openmctConfig }) => {
|
||||
const folder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
});
|
||||
|
||||
// Create the comps with defaults
|
||||
const comp = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Derived Telemetry',
|
||||
parent: folder.uuid
|
||||
});
|
||||
|
||||
const telemetryObject = await createExampleTelemetryObject(page, comp.uuid);
|
||||
|
||||
// Check that expressions can be edited
|
||||
await page.goto(comp.url);
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByPlaceholder('Enter an expression').fill('a*2');
|
||||
await page.getByText('Current Output').click();
|
||||
await expect(page.getByText('Expression valid')).toBeVisible();
|
||||
|
||||
// Check that expressions are marked invalid
|
||||
await page.getByLabel('Reference Name Input for a').fill('b');
|
||||
await page.getByText('Current Output').click();
|
||||
await expect(page.getByText('Invalid: Undefined symbol a')).toBeVisible();
|
||||
|
||||
// Check that test data works
|
||||
await page.getByPlaceholder('Enter an expression').fill('b*2');
|
||||
await page.getByLabel('Reference Test Value for b').fill('5');
|
||||
await page.getByLabel('Apply Test Data').click();
|
||||
let testValue = await page.getByLabel('Current Output Value').textContent();
|
||||
expect(testValue).toBe('10');
|
||||
|
||||
// Check that real data works
|
||||
await page.getByLabel('Apply Test Data').click();
|
||||
await setRealTimeMode(page);
|
||||
testValue = await page.getByLabel('Current Output Value').textContent();
|
||||
expect(testValue).not.toBe('10');
|
||||
// should be a number
|
||||
expect(parseFloat(testValue)).not.toBeNaN();
|
||||
|
||||
// Check that object path is correct
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
let objectPath = await page.getByLabel(`${telemetryObject.name} Object Path`).textContent();
|
||||
const expectedObjectPath = `/${myItemsFolderName}/${folder.name}/${comp.name}/${telemetryObject.name}`;
|
||||
expect(objectPath).toBe(expectedObjectPath);
|
||||
|
||||
// Check that the comps are saved
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
const expression = await page.getByLabel('Expression', { exact: true }).textContent();
|
||||
expect(expression).toBe('b*2');
|
||||
|
||||
// Check that object path is still correct after save
|
||||
objectPath = await page.getByLabel(`${telemetryObject.name} Object Path`).textContent();
|
||||
expect(objectPath).toBe(expectedObjectPath);
|
||||
|
||||
// Check that comps work after being saved
|
||||
testValue = await page.getByLabel('Current Output Value').textContent();
|
||||
expect(testValue).not.toBe('10');
|
||||
// should be a number
|
||||
expect(parseFloat(testValue)).not.toBeNaN();
|
||||
|
||||
// Check that output format can be changed
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page.getByLabel('Output Format').click();
|
||||
await page.getByLabel('Output Format').fill('%d');
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
// Ensure we only have one digit
|
||||
await expect(page.getByLabel('Current Output Value')).toHaveText(/^-1$|^0$|^1$/);
|
||||
// And that it persists post save
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await expect(page.getByLabel('Current Output Value')).toHaveText(/^-1$|^0$|^1$/);
|
||||
});
|
||||
});
|
@ -27,7 +27,8 @@ demonstrate some playwright for test developers. This pattern should not be re-u
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject
|
||||
createExampleTelemetryObject,
|
||||
setRealTimeMode
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
@ -116,7 +117,7 @@ test.describe('Basic Condition Set Use', () => {
|
||||
await page.getByLabel('Conditions View').click();
|
||||
await expect(page.getByText('Current Output')).toBeVisible();
|
||||
});
|
||||
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
|
||||
test('ConditionSet produces an output when telemetry is available, and does not when it is not', async ({
|
||||
page
|
||||
}) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
@ -281,12 +282,142 @@ test.describe('Basic Condition Set Use', () => {
|
||||
await page.goto(exampleTelemetry.url);
|
||||
});
|
||||
|
||||
test('Short circuit evaluation does not cause incorrect evaluation https://github.com/nasa/openmct/issues/7992', async ({
|
||||
page
|
||||
}) => {
|
||||
await setRealTimeMode(page);
|
||||
await page.getByLabel('Create', { exact: true }).click();
|
||||
await page.getByLabel('State Generator').click();
|
||||
await page.getByLabel('Title', { exact: true }).fill('P1');
|
||||
await page.getByLabel('State Duration (seconds)').fill('1');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByLabel('Create', { exact: true }).click();
|
||||
await page.getByLabel('State Generator').click();
|
||||
await page.getByLabel('Title', { exact: true }).fill('P2');
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('1');
|
||||
await page.getByRole('treeitem', { name: 'Test Condition Set' }).click();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
await page.getByRole('treeitem', { name: 'Test Condition Set' }).click();
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByLabel('Add Condition').click();
|
||||
await page.getByLabel('Condition Name Input').first().fill('P1 IS ON AND P2 IS ON');
|
||||
await page.getByLabel('Criterion Telemetry Selection').selectOption({ label: 'P1' });
|
||||
await page.getByLabel('Criterion Metadata Selection').selectOption('value');
|
||||
await page.getByLabel('Criterion Comparison Selection').selectOption('equalTo');
|
||||
await page.getByLabel('Criterion Input').fill('1');
|
||||
await page.getByLabel('Add Criteria - Enabled').click();
|
||||
await page.getByLabel('Criterion Telemetry Selection').nth(1).selectOption({ label: 'P2' });
|
||||
await page.getByLabel('Criterion Metadata Selection').nth(1).selectOption('value');
|
||||
await page.getByLabel('Criterion Comparison Selection').nth(1).selectOption('equalTo');
|
||||
await page.getByLabel('Criterion Input').nth(1).fill('1');
|
||||
await page.getByLabel('Add Condition').click();
|
||||
await page.getByLabel('Condition Name Input').first().fill('P1 IS OFF OR P2 IS OFF');
|
||||
await page.getByLabel('Condition Trigger').first().selectOption('any');
|
||||
await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ label: 'P1' });
|
||||
await page.getByLabel('Criterion Metadata Selection').first().selectOption('value');
|
||||
await page.getByLabel('Criterion Comparison Selection').first().selectOption('equalTo');
|
||||
await page.getByLabel('Criterion Input').first().fill('0');
|
||||
await page.getByLabel('Add Criteria - Enabled').first().click();
|
||||
await page.getByLabel('Criterion Telemetry Selection').nth(1).selectOption({ label: 'P2' });
|
||||
await page.getByLabel('Criterion Metadata Selection').nth(1).selectOption('value');
|
||||
await page.getByLabel('Criterion Comparison Selection').nth(1).selectOption('equalTo');
|
||||
await page.getByLabel('Criterion Input').nth(1).fill('0');
|
||||
await page.getByLabel('Condition Name Input').first().dblclick();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
/**
|
||||
* Create default conditions for test. Start with invalid values to put condition set into
|
||||
* "default" state
|
||||
*/
|
||||
await page.getByLabel('Test Data Telemetry Selection').selectOption({ label: 'P1' });
|
||||
await page.getByLabel('Test Data Metadata Selection').selectOption({ label: 'Value' });
|
||||
await page.getByLabel('Test Data Input').fill('3');
|
||||
await page.getByLabel('Add Test Datum').click();
|
||||
await page.getByLabel('Test Data Telemetry Selection').nth(1).selectOption({ label: 'P2' });
|
||||
await page.getByLabel('Test Data Metadata Selection').nth(1).selectOption({ label: 'Value' });
|
||||
await page.getByLabel('Test Data Input').nth(1).fill('3');
|
||||
await page.getByLabel('Apply Test Data').nth(1).click();
|
||||
|
||||
let activeCondition = page.getByLabel('Active Condition Set Condition');
|
||||
let activeConditionName = activeCondition.getByLabel('Condition Name Label');
|
||||
|
||||
await expect(activeConditionName).toHaveText('Default');
|
||||
|
||||
/**
|
||||
* Set P1 to 0
|
||||
*/
|
||||
await page.getByLabel('Test Data Input').nth(0).fill('0');
|
||||
|
||||
activeCondition = page.getByLabel('Active Condition Set Condition');
|
||||
activeConditionName = activeCondition.getByLabel('Condition Name Label');
|
||||
|
||||
await expect(activeConditionName).toHaveText('P1 IS OFF OR P2 IS OFF');
|
||||
|
||||
/**
|
||||
* Set P2 to 1
|
||||
*/
|
||||
await page.getByLabel('Test Data Input').nth(1).fill('1');
|
||||
|
||||
activeCondition = page.getByLabel('Active Condition Set Condition');
|
||||
activeConditionName = activeCondition.getByLabel('Condition Name Label');
|
||||
|
||||
await expect(activeConditionName).toHaveText('P1 IS OFF OR P2 IS OFF');
|
||||
|
||||
/**
|
||||
* Set P1 to 1
|
||||
*/
|
||||
await page.getByLabel('Test Data Input').nth(0).fill('1');
|
||||
|
||||
activeCondition = page.getByLabel('Active Condition Set Condition');
|
||||
activeConditionName = activeCondition.getByLabel('Condition Name Label');
|
||||
|
||||
await expect(activeConditionName).toHaveText('P1 IS ON AND P2 IS ON');
|
||||
});
|
||||
|
||||
test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7484'
|
||||
});
|
||||
});
|
||||
|
||||
test('ConditionSet has add criteria button enabled/disabled when composition is and is not available', async ({
|
||||
page
|
||||
}) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.goto(conditionSet.url);
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Create a condition
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
|
||||
|
||||
// Validate that the add criteria button is disabled
|
||||
await expect(page.getByLabel('Add Criteria - Disabled')).toHaveAttribute('disabled');
|
||||
|
||||
// Add Telemetry to ConditionSet
|
||||
const sineWaveGeneratorTreeItem = page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: exampleTelemetry.name
|
||||
});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
// Validate that the add criteria button is enabled and adds a new criterion
|
||||
await expect(page.getByLabel('Add Criteria - Enabled')).not.toHaveAttribute('disabled');
|
||||
await page.getByLabel('Add Criteria - Enabled').click();
|
||||
const numOfUnnamedCriteria = await page.getByLabel('Criterion Telemetry Selection').count();
|
||||
expect(numOfUnnamedCriteria).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Condition Set Composition', () => {
|
||||
|
@ -236,7 +236,7 @@ test.describe('Display Layout', () => {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
@ -278,7 +278,7 @@ test.describe('Display Layout', () => {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
@ -317,7 +317,7 @@ test.describe('Display Layout', () => {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
||||
@ -358,7 +358,7 @@ test.describe('Display Layout', () => {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
||||
@ -413,7 +413,7 @@ test.describe('Display Layout', () => {
|
||||
await page.locator('div[title="Resize object width"] > input').click();
|
||||
await page.locator('div[title="Resize object width"] > input').fill('70');
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
const startDate = '2021-12-30 01:01:00.000Z';
|
||||
@ -473,7 +473,7 @@ test.describe('Display Layout', () => {
|
||||
await page.getByText('View type').click();
|
||||
await page.getByText('Overlay Plot').click();
|
||||
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Time to inspect some network traffic
|
||||
@ -507,8 +507,140 @@ test.describe('Display Layout', () => {
|
||||
// In real time mode, we don't fetch annotations at all
|
||||
await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Same objects with different request options have unique subscriptions', async ({
|
||||
page
|
||||
}) => {
|
||||
// Expand My Items
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display'
|
||||
});
|
||||
|
||||
// Create a State Generator, set to higher frequency updates
|
||||
const stateGenerator = await createDomainObjectWithDefaults(page, {
|
||||
type: 'State Generator',
|
||||
name: 'State Generator'
|
||||
});
|
||||
const stateGeneratorTreeItem = page.getByRole('treeitem', {
|
||||
name: stateGenerator.name
|
||||
});
|
||||
await stateGeneratorTreeItem.click({ button: 'right' });
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
|
||||
// Create a Table for filtering ON values
|
||||
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Table Filter On Value'
|
||||
});
|
||||
const tableFilterOnTreeItem = page.getByRole('treeitem', {
|
||||
name: tableFilterOnValue.name
|
||||
});
|
||||
|
||||
// Create a Table for filtering OFF values
|
||||
const tableFilterOffValue = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Table Filter Off Value'
|
||||
});
|
||||
const tableFilterOffTreeItem = page.getByRole('treeitem', {
|
||||
name: tableFilterOffValue.name
|
||||
});
|
||||
|
||||
// Navigate to ON filtering table and add state generator and setup filters
|
||||
await page.goto(tableFilterOnValue.url);
|
||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||
await selectFilterOption(page, '1');
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Navigate to OFF filtering table and add state generator and setup filters
|
||||
await page.goto(tableFilterOffValue.url);
|
||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||
await selectFilterOption(page, '0');
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Navigate to the display layout and edit it
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
// Add the tables to the display layout
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await tableFilterOffTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 10, y: 300 }
|
||||
});
|
||||
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 400, y: 500 },
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await tableFilterOnTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 10, y: 100 }
|
||||
});
|
||||
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 400, y: 300 },
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Get the tables so we can verify filtering is working as expected
|
||||
const tableFilterOn = page.getByLabel(`${tableFilterOnValue.name} Frame`, {
|
||||
exact: true
|
||||
});
|
||||
const tableFilterOff = page.getByLabel(`${tableFilterOffValue.name} Frame`, {
|
||||
exact: true
|
||||
});
|
||||
|
||||
// Verify filtering is working correctly
|
||||
|
||||
// Check that no filtered values appear for at least 2 seconds
|
||||
const VERIFICATION_TIME = 2000; // 2 seconds
|
||||
const CHECK_INTERVAL = 100; // Check every 100ms
|
||||
|
||||
// Create a promise that will check for filtered values periodically
|
||||
const checkForCorrectValues = new Promise((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
const offCount = await tableFilterOn.locator('td[title="OFF"]').count();
|
||||
const onCount = await tableFilterOff.locator('td[title="ON"]').count();
|
||||
if (offCount > 0 || onCount > 0) {
|
||||
clearInterval(interval);
|
||||
reject(
|
||||
new Error(
|
||||
`Found ${offCount} OFF and ${onCount} ON values when expecting 0 OFF and 0 ON`
|
||||
)
|
||||
);
|
||||
}
|
||||
}, CHECK_INTERVAL);
|
||||
|
||||
// After VERIFICATION_TIME, if no filtered values were found, resolve successfully
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}, VERIFICATION_TIME);
|
||||
});
|
||||
|
||||
await expect(checkForCorrectValues).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
async function selectFilterOption(page, filterOption) {
|
||||
await page.getByRole('tab', { name: 'Filters' }).click();
|
||||
await page
|
||||
.getByLabel('Inspector Views')
|
||||
.locator('li')
|
||||
.filter({ hasText: 'State Generator' })
|
||||
.locator('span')
|
||||
.click();
|
||||
await page.getByRole('switch').click();
|
||||
await page.selectOption('select[name="setSelectionThreshold"]', filterOption);
|
||||
}
|
||||
|
||||
async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
|
||||
await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);
|
||||
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);
|
||||
|
@ -28,7 +28,9 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject
|
||||
createExampleTelemetryObject,
|
||||
setRealTimeMode,
|
||||
setStartOffset
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
@ -166,6 +168,57 @@ test.describe('Gauge', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Gauge does not break when an object is missing', async ({ page }) => {
|
||||
// Set up error listeners
|
||||
const pageErrors = [];
|
||||
|
||||
// Listen for uncaught exceptions
|
||||
page.on('pageerror', (err) => {
|
||||
pageErrors.push(err.message);
|
||||
});
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Create a Gauge
|
||||
const gauge = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gauge',
|
||||
name: 'Gauge with missing object'
|
||||
});
|
||||
|
||||
// Create a Sine Wave Generator in the Gauge with a loading delay
|
||||
const missingSWG = await createExampleTelemetryObject(page, gauge.uuid);
|
||||
|
||||
// Remove the object from local storage
|
||||
await page.evaluate(
|
||||
([missingObject]) => {
|
||||
const mct = localStorage.getItem('mct');
|
||||
const mctObjects = JSON.parse(mct);
|
||||
delete mctObjects[missingObject.uuid];
|
||||
localStorage.setItem('mct', JSON.stringify(mctObjects));
|
||||
},
|
||||
[missingSWG]
|
||||
);
|
||||
|
||||
// Verify start bounds
|
||||
await expect(page.getByLabel('Start offset: 00:30:00')).toBeVisible();
|
||||
|
||||
// Nav to the Gauge
|
||||
await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// adjust time bounds and ensure they are updated
|
||||
await setStartOffset(page, {
|
||||
startHours: '00',
|
||||
startMins: '45',
|
||||
startSecs: '00'
|
||||
});
|
||||
|
||||
// Verify start bounds changed
|
||||
await expect(page.getByLabel('Start offset: 00:45:00')).toBeVisible();
|
||||
|
||||
// // Verify no errors were thrown
|
||||
expect(pageErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Gauge enforces composition policy', async ({ page }) => {
|
||||
// Create a Gauge
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
|
@ -96,9 +96,6 @@ test.describe('Example Imagery Object', () => {
|
||||
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 ({
|
||||
page,
|
||||
browserName
|
||||
|
@ -0,0 +1,93 @@
|
||||
/*****************************************************************************
|
||||
* 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();
|
||||
});
|
||||
});
|
@ -54,8 +54,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
|
||||
await page.goto(exampleDataVisualizationSource.url);
|
||||
|
||||
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
||||
await page.getByRole('cell', { name: /First Sine Wave Generator/ }).click();
|
||||
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
||||
await expect(page.getByText('Numeric Data')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('span.plot-series-name', { hasText: 'First Sine Wave Generator Hz' })
|
||||
@ -63,6 +63,7 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
|
||||
|
||||
await page.getByRole('cell', { name: /Second Sine Wave Generator/ }).click();
|
||||
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
||||
await expect(page.getByText('Numeric Data')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('span.plot-series-name', { hasText: 'Second Sine Wave Generator Hz' })
|
||||
@ -77,6 +78,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
// ensure our new tab's title is correct
|
||||
const newPage = await pagePromise;
|
||||
await newPage.waitForLoadState();
|
||||
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
||||
|
||||
// expect new tab title to contain 'Second Sine Wave Generator'
|
||||
await expect(newPage).toHaveTitle('Second Sine Wave Generator');
|
||||
|
||||
|
@ -53,7 +53,6 @@ test.describe('Testing LAD table configuration', () => {
|
||||
test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => {
|
||||
// Edit LAD table
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
||||
|
||||
// make sure headers are visible initially
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
@ -114,7 +113,6 @@ test.describe('Testing LAD table configuration', () => {
|
||||
|
||||
// Edit LAD table
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
||||
|
||||
// show timestamp column
|
||||
await page.getByLabel('Timestamp', { exact: true }).check();
|
||||
@ -142,7 +140,6 @@ test.describe('Testing LAD table configuration', () => {
|
||||
|
||||
// Edit LAD table
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
||||
|
||||
// show units, type, and WATCH columns
|
||||
await page.getByLabel('Units').check();
|
||||
@ -182,7 +179,6 @@ test.describe('Testing LAD table configuration', () => {
|
||||
|
||||
// Edit LAD table
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
||||
|
||||
// make sure Sine Wave headers are visible initially too
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
|
@ -26,7 +26,10 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
renameCurrentObjectFromBrowseBar
|
||||
} from '../../../../appActions.js';
|
||||
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
|
||||
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
||||
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
|
||||
@ -596,4 +599,61 @@ test.describe('Notebook entry tests', () => {
|
||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
|
||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('When changing the name of a notebook in the browse bar, new notebook changes are not lost', async ({
|
||||
page
|
||||
}) => {
|
||||
const TEST_TEXT = 'Do not lose me!';
|
||||
const FIRST_NEW_NAME = 'New Name';
|
||||
const SECOND_NEW_NAME = 'Second New Name';
|
||||
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
|
||||
await renameCurrentObjectFromBrowseBar(page, FIRST_NEW_NAME);
|
||||
|
||||
// verify the name change in tree and browse bar
|
||||
await verifyNameChange(page, FIRST_NEW_NAME);
|
||||
|
||||
// enter one entry
|
||||
await enterAndCommitTextEntry(page, TEST_TEXT);
|
||||
|
||||
// verify the entry is present
|
||||
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||
|
||||
// change the name
|
||||
await renameCurrentObjectFromBrowseBar(page, SECOND_NEW_NAME);
|
||||
|
||||
// verify the name change in tree and browse bar
|
||||
await verifyNameChange(page, SECOND_NEW_NAME);
|
||||
|
||||
// verify the entry is still present
|
||||
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Enter text into the last notebook entry and commit it.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} text
|
||||
*/
|
||||
async function enterAndCommitTextEntry(page, text) {
|
||||
await nbUtils.addNotebookEntry(page);
|
||||
await nbUtils.enterTextInLastEntry(page, text);
|
||||
await nbUtils.commitEntry(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the name change in the tree and browse bar.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function verifyNameChange(page, newName) {
|
||||
await expect(
|
||||
page.getByRole('treeitem').locator('.is-navigated-object .c-tree__item__name')
|
||||
).toHaveText(newName);
|
||||
await expect(page.getByLabel('Browse bar object name')).toHaveText(newName);
|
||||
}
|
||||
|
@ -65,9 +65,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
});
|
||||
test('Can add tags with blank entry', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
await enterTextEntry(page, '');
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
|
||||
|
@ -47,8 +47,6 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
|
||||
});
|
||||
|
||||
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
|
||||
//Ensure we're on the annotations Tab in the inspector
|
||||
await page.getByText('Annotations').click();
|
||||
// Expand sidebar
|
||||
await page.locator('.c-notebook__toggle-nav-button').click();
|
||||
|
||||
@ -86,6 +84,9 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
|
||||
|
||||
//Ensure we're on the annotations Tab in the inspector
|
||||
await page.getByText('Annotations').click();
|
||||
|
||||
// Add some tags
|
||||
// Network Requests are for each tag creation are:
|
||||
// 1) Getting the original path of the parent object
|
||||
@ -180,8 +181,8 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
|
||||
});
|
||||
await page.getByText('Annotations').click();
|
||||
await nbUtils.enterTextEntry(page, 'First Entry');
|
||||
await page.getByText('Annotations').click();
|
||||
|
||||
// Add three tags
|
||||
await addTagAndAwaitNetwork(page, 'Science');
|
||||
|
@ -100,6 +100,9 @@ test.describe('Overlay Plot', () => {
|
||||
await page.getByLabel('Expand By Default').check();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Assert that the legend is now open
|
||||
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
|
||||
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
|
||||
@ -111,6 +114,9 @@ test.describe('Overlay Plot', () => {
|
||||
|
||||
// Assert that the legend is expanded on page load
|
||||
await page.reload();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
|
||||
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
|
||||
|
@ -108,4 +108,42 @@ test.describe('Plot Controls', () => {
|
||||
// Expect before and after plot points to match
|
||||
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
|
||||
});
|
||||
|
||||
/*
|
||||
Test to verify that switching a plot's time context from global to
|
||||
its own independent time context and then back to global context works correctly.
|
||||
|
||||
After switching from fixed time mode (ITC) to real time mode (global context),
|
||||
the pause control for the plot should be available, indicating that it is following the right context.
|
||||
*/
|
||||
test('Plots follow the right time context', async ({ page }) => {
|
||||
// Set global time conductor to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is visible since global time conductor is in Real time mode.
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||
|
||||
// Toggle independent time conductor ON
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Bring up the independent time conductor popup and switch to fixed time mode
|
||||
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||
await page.getByLabel('Independent Time Conductor Mode Menu').click();
|
||||
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is no longer visible since the plot is following the independent time context
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeHidden();
|
||||
|
||||
// Toggle independent time conductor OFF - Note that the global time conductor is still in Real time mode
|
||||
await page.getByLabel('Disable Independent Time Conductor').click();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is visible since the global time conductor is in real time mode
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,58 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2025, 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 the rendering and interaction of plots.
|
||||
*
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Plot Controls in compact mode', () => {
|
||||
let timeStrip;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timeStrip = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time Strip'
|
||||
});
|
||||
|
||||
// Create an overlay plot with a sine wave generator
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: timeStrip.uuid
|
||||
});
|
||||
await page.goto(`${timeStrip.url}`);
|
||||
});
|
||||
|
||||
test('Plots show cursor guides', async ({ page }) => {
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// click on cursor guides control
|
||||
await page.getByTitle('Toggle cursor guides').click();
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
await expect(page.getByLabel('Vertical cursor guide')).toBeVisible();
|
||||
await expect(page.getByLabel('Horizontal cursor guide')).toBeVisible();
|
||||
});
|
||||
});
|
244
e2e/tests/functional/plugins/plot/plotViewActions.e2e.spec.js
Normal file
244
e2e/tests/functional/plugins/plot/plotViewActions.e2e.spec.js
Normal file
@ -0,0 +1,244 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality when objects are missing
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
const SWG_NAME = 'Sine Wave Generator';
|
||||
const OVERLAY_PLOT_NAME = 'Overlay Plot';
|
||||
const STACKED_PLOT_NAME = 'Stacked Plot';
|
||||
|
||||
test.describe('For a default Plot View, Plot View Action:', () => {
|
||||
let download;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const plot = await createDomainObjectWithDefaults(page, {
|
||||
type: SWG_NAME,
|
||||
name: SWG_NAME
|
||||
});
|
||||
|
||||
await page.goto(plot.url);
|
||||
|
||||
// Set up dialog handler before clicking the export button
|
||||
await page.getByLabel('More actions').click();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
if (download) {
|
||||
await download.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as PNG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);
|
||||
});
|
||||
|
||||
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as JPG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('For an Overlay Plot View, Plot View Action:', () => {
|
||||
let download;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: OVERLAY_PLOT_NAME,
|
||||
name: OVERLAY_PLOT_NAME
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: SWG_NAME,
|
||||
name: SWG_NAME,
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
// Set up dialog handler before clicking the export button
|
||||
await page.getByLabel('More actions').click();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
if (download) {
|
||||
await download.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as PNG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.png`);
|
||||
});
|
||||
|
||||
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as JPG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.jpeg`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('For a Stacked Plot View, Plot View Action:', () => {
|
||||
let download;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const stackedPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: STACKED_PLOT_NAME,
|
||||
name: STACKED_PLOT_NAME
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: SWG_NAME,
|
||||
name: SWG_NAME,
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
// Set up dialog handler before clicking the export button
|
||||
await page.getByLabel('More actions').click();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
if (download) {
|
||||
await download.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as PNG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.png`);
|
||||
});
|
||||
|
||||
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as JPG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.jpeg`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Plot View Action:', () => {
|
||||
let download;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const plot = await createDomainObjectWithDefaults(page, {
|
||||
type: SWG_NAME,
|
||||
name: `!@#${SWG_NAME}!@#><`
|
||||
});
|
||||
|
||||
await page.goto(plot.url);
|
||||
|
||||
// Set up dialog handler before clicking the export button
|
||||
await page.getByLabel('More actions').click();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
if (download) {
|
||||
await download.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
test('Export as PNG saved filenames will not include invalid characters', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as PNG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);
|
||||
});
|
||||
|
||||
test('Export as JPG saved filenames will not include invalid characters', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as JPG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);
|
||||
});
|
||||
});
|
@ -50,7 +50,7 @@ test.describe('Plots work in Previews', () => {
|
||||
});
|
||||
const layoutGridHolder = page.getByLabel('Test Display Layout Layout Grid');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// right click on the plot and select view large
|
||||
@ -67,7 +67,7 @@ test.describe('Plots work in Previews', () => {
|
||||
await page.getByLabel('Move Sub-object Frame').click();
|
||||
await page.getByText('View type').click();
|
||||
await page.getByText('Overlay Plot').click();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await expect(
|
||||
page.getByLabel('Test Display Layout Layout', { exact: true }).getByLabel('Plot Canvas')
|
||||
|
@ -152,14 +152,14 @@ test.describe('Stacked Plot', () => {
|
||||
}) => {
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Click on the 1st plot
|
||||
await page
|
||||
.getByLabel('Stacked Plot Item Sine Wave Generator A')
|
||||
.getByLabel('Plot Canvas')
|
||||
.click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Assert that the inspector shows the Y Axis properties for swgA
|
||||
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
@ -172,6 +172,9 @@ test.describe('Stacked Plot', () => {
|
||||
.getByLabel('Stacked Plot Item Sine Wave Generator B')
|
||||
.getByLabel('Plot Canvas')
|
||||
.click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Assert that the inspector shows the Y Axis properties for swgB
|
||||
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
@ -184,6 +187,9 @@ test.describe('Stacked Plot', () => {
|
||||
.getByLabel('Stacked Plot Item Sine Wave Generator C')
|
||||
.getByLabel('Plot Canvas')
|
||||
.click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Assert that the inspector shows the Y Axis properties for swgB
|
||||
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
@ -194,7 +200,7 @@ test.describe('Stacked Plot', () => {
|
||||
// Go into edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
// await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Click on the 1st plot
|
||||
await page.getByLabel('Stacked Plot Item Sine Wave Generator A').click();
|
||||
@ -233,11 +239,11 @@ test.describe('Stacked Plot', () => {
|
||||
// Go into edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Click on canvas for the 1st plot
|
||||
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Expand config for the series
|
||||
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();
|
||||
|
||||
@ -255,6 +261,8 @@ test.describe('Stacked Plot', () => {
|
||||
// Click on canvas for the 1st plot
|
||||
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Expand config for the series
|
||||
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();
|
||||
|
||||
|
@ -45,6 +45,8 @@ const setFontFamily = '"Andale Mono", sans-serif';
|
||||
|
||||
test.describe('Stacked Plot styling', () => {
|
||||
let stackedPlot;
|
||||
let overlayPlot1;
|
||||
let overlayPlot2;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
@ -54,17 +56,30 @@ test.describe('Stacked Plot styling', () => {
|
||||
name: 'StackedPlot1'
|
||||
});
|
||||
|
||||
// create two overlay plots
|
||||
overlayPlot1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Overlay Plot 1',
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
overlayPlot2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Overlay Plot 2',
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
// Create two SWGs and attach them to the Stacked Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Sine Wave Generator 1',
|
||||
parent: stackedPlot.uuid
|
||||
parent: overlayPlot1.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Sine Wave Generator 2',
|
||||
parent: stackedPlot.uuid
|
||||
parent: overlayPlot2.uuid
|
||||
});
|
||||
});
|
||||
|
||||
@ -138,21 +153,21 @@ test.describe('Stacked Plot styling', () => {
|
||||
NO_STYLE_RGBA,
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||
);
|
||||
|
||||
await checkStyles(
|
||||
NO_STYLE_RGBA,
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 2')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 2')
|
||||
);
|
||||
|
||||
await checkFontStyles(
|
||||
setFontSize,
|
||||
setFontWeight,
|
||||
setFontFamily,
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||
);
|
||||
});
|
||||
|
||||
@ -169,19 +184,19 @@ test.describe('Stacked Plot styling', () => {
|
||||
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
|
||||
//Check default styles for SWG1 and SWG2
|
||||
//Check default styles for overlayPlot1 and overlayPlot2
|
||||
await checkStyles(
|
||||
NO_STYLE_RGBA,
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||
);
|
||||
|
||||
await checkStyles(
|
||||
NO_STYLE_RGBA,
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 2')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 2')
|
||||
);
|
||||
|
||||
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
|
||||
@ -190,11 +205,11 @@ test.describe('Stacked Plot styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||
);
|
||||
|
||||
//Set Font Styles on SWG1 but not SWG2
|
||||
await page.getByLabel('Stacked Plot Item Sine Wave Generator 1').click();
|
||||
await page.getByLabel('Stacked Plot Item Overlay Plot 1').click();
|
||||
//Set Font Size to 72
|
||||
await page.getByLabel('Set Font Size').click();
|
||||
await page.getByRole('menuitem', { name: '72px' }).click();
|
||||
|
@ -57,7 +57,7 @@ test.describe('Tabs View', () => {
|
||||
await page.goto(tabsView.url);
|
||||
|
||||
// select first tab
|
||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
||||
await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
@ -92,6 +92,38 @@ test.describe('Tabs View', () => {
|
||||
// no canvas (i.e., sine wave generator) in the document should be visible
|
||||
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', () => {
|
||||
|
@ -117,7 +117,8 @@ test.describe('Telemetry Table', () => {
|
||||
|
||||
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);
|
||||
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||
const endTime = endTimeStamp.toISOString().split('T')[1];
|
||||
const milliseconds = endTimeStamp.getMilliseconds();
|
||||
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||
|
||||
await setTimeConductorBounds(page, { endDate, endTime });
|
||||
|
||||
|
@ -24,65 +24,210 @@ import {
|
||||
setEndOffset,
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode,
|
||||
setStartOffset,
|
||||
setTimeConductorBounds
|
||||
setStartOffset
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Time conductor operations', () => {
|
||||
test('validate start time does not exceed end time', async ({ page }) => {
|
||||
const DAY = '2024-01-01';
|
||||
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
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
const year = new Date().getFullYear();
|
||||
});
|
||||
|
||||
// Set initial valid 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 });
|
||||
test('validate date and time inputs are validated on input event', async ({ page }) => {
|
||||
const submitButtonLocator = page.getByLabel('Submit time bounds');
|
||||
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
// Test invalid start date
|
||||
const invalidStartDate = `${year}-01-02`;
|
||||
await page.getByLabel('Start date').fill(invalidStartDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('Start date').fill(startDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
await test.step('invalid start date disables submit button', async () => {
|
||||
const initialStartDate = await page.getByLabel('Start date').inputValue();
|
||||
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
|
||||
|
||||
// Test invalid end date
|
||||
const invalidEndDate = `${year - 1}-12-31`;
|
||||
await page.getByLabel('End date').fill(invalidEndDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('End date').fill(endDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
await page.getByLabel('Start date').fill(invalidStartDate);
|
||||
await expect(submitButtonLocator).toBeDisabled();
|
||||
await page.getByLabel('Start date').fill(initialStartDate);
|
||||
await expect(submitButtonLocator).toBeEnabled();
|
||||
});
|
||||
|
||||
// Test invalid start time
|
||||
const invalidStartTime = '42:00:00';
|
||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('Start time').fill(startTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
await test.step('invalid start time disables submit button', async () => {
|
||||
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||
|
||||
// Test invalid end time
|
||||
const invalidEndTime = '43:00:00';
|
||||
await page.getByLabel('End time').fill(invalidEndTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('End time').fill(endTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||
await expect(submitButtonLocator).toBeDisabled();
|
||||
await page.getByLabel('Start time').fill(initialStartTime);
|
||||
await expect(submitButtonLocator).toBeEnabled();
|
||||
});
|
||||
|
||||
// Submit valid time bounds
|
||||
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 expect(submitButtonLocator).toBeDisabled();
|
||||
await page.getByLabel('End date').fill(initialEndDate);
|
||||
await expect(submitButtonLocator).toBeDisabled();
|
||||
await page.getByLabel('Start time').fill(initialStartTime);
|
||||
await expect(submitButtonLocator).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('validate date and time inputs validation is reported on change event', async ({ page }) => {
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
await test.step('invalid start date is reported on change event, not on input event', async () => {
|
||||
const initialStartDate = await page.getByLabel('Start date').inputValue();
|
||||
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
|
||||
|
||||
await page.getByLabel('Start date').fill(invalidStartDate);
|
||||
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||
await page.getByLabel('Start date').press('Tab');
|
||||
await expect(page.getByLabel('Start date')).toHaveAttribute('title', 'Invalid Date');
|
||||
await page.getByLabel('Start date').fill(initialStartDate);
|
||||
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||
});
|
||||
|
||||
await test.step('invalid start time is reported on change event, not on input event', async () => {
|
||||
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||
|
||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||
await page.getByLabel('Start time').press('Tab');
|
||||
await expect(page.getByLabel('Start time')).toHaveAttribute('title', 'Invalid Time');
|
||||
await page.getByLabel('Start time').fill(initialStartTime);
|
||||
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||
});
|
||||
|
||||
await test.step('invalid end date is reported on change event, not on input event', async () => {
|
||||
const initialEndDate = await page.getByLabel('End date').inputValue();
|
||||
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
|
||||
|
||||
await page.getByLabel('End date').fill(invalidEndDate);
|
||||
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||
await page.getByLabel('End date').press('Tab');
|
||||
await expect(page.getByLabel('End date')).toHaveAttribute('title', 'Invalid Date');
|
||||
await page.getByLabel('End date').fill(initialEndDate);
|
||||
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||
});
|
||||
|
||||
await test.step('invalid end time is reported on change event, not on input event', async () => {
|
||||
const initialEndTime = await page.getByLabel('End time').inputValue();
|
||||
const invalidEndTime = `${initialEndTime.substring(0, 5)}${initialEndTime.substring(6)}`;
|
||||
|
||||
await page.getByLabel('End time').fill(invalidEndTime);
|
||||
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||
await page.getByLabel('End time').press('Tab');
|
||||
await expect(page.getByLabel('End time')).toHaveAttribute('title', 'Invalid Time');
|
||||
await page.getByLabel('End time').fill(initialEndTime);
|
||||
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||
});
|
||||
});
|
||||
|
||||
test('validate start time does not exceed end time on submit', async ({ page }) => {
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByLabel('Start date').fill(DAY);
|
||||
await page.getByLabel('Start time').fill(TWO_O_CLOCK);
|
||||
await page.getByLabel('End date').fill(DAY);
|
||||
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('Submit time bounds').click();
|
||||
|
||||
// Verify the submitted time bounds
|
||||
await expect(page.getByLabel('Start bounds')).toHaveText(
|
||||
new RegExp(`${startDate} ${startTime}.000Z`)
|
||||
await expect(page.getByLabel('Start date')).toHaveAttribute(
|
||||
'title',
|
||||
'Specified start date exceeds end bound'
|
||||
);
|
||||
await expect(page.getByLabel('End bounds')).toHaveText(
|
||||
new RegExp(`${endDate} ${endTime}.000Z`)
|
||||
await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${TWO_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);
|
||||
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`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -131,77 +276,6 @@ test.describe('Global Time Conductor', () => {
|
||||
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
|
||||
* between fixed timespan and real-time mode.
|
||||
|
@ -31,6 +31,8 @@ import { expect, test } from '../../pluginFixtures.js';
|
||||
test.describe('Grand Search', () => {
|
||||
let grandSearchInput;
|
||||
|
||||
test.use({ ignore404s: [/_design\/object_names\/_view\/object_names$/] });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
grandSearchInput = page
|
||||
.getByLabel('OpenMCT Search')
|
||||
@ -191,7 +193,88 @@ test.describe('Grand Search', () => {
|
||||
await expect(searchResults).toContainText(folderName);
|
||||
});
|
||||
|
||||
test.describe('Search will test for the presence of the object_names index, and', () => {
|
||||
test('use index if available @couchdb @network', async ({ page }) => {
|
||||
await createObjectsForSearch(page);
|
||||
|
||||
let isObjectNamesViewAvailable = false;
|
||||
let isObjectNamesUsedForSearch = false;
|
||||
|
||||
page.on('request', async (request) => {
|
||||
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
|
||||
const isHeadRequest = request.method().toLowerCase() === 'head';
|
||||
|
||||
if (isObjectNamesRequest && isHeadRequest) {
|
||||
const response = await request.response();
|
||||
isObjectNamesViewAvailable = response.status() === 200;
|
||||
}
|
||||
});
|
||||
|
||||
page.on('request', (request) => {
|
||||
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
|
||||
const isPostRequest = request.method().toLowerCase() === 'post';
|
||||
|
||||
if (isObjectNamesRequest && isPostRequest) {
|
||||
isObjectNamesUsedForSearch = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Full search for object
|
||||
await grandSearchInput.pressSequentially('Clock', { delay: 100 });
|
||||
|
||||
// Wait for search to finish
|
||||
await waitForSearchCompletion(page);
|
||||
|
||||
expect(isObjectNamesViewAvailable).toBe(true);
|
||||
expect(isObjectNamesUsedForSearch).toBe(true);
|
||||
});
|
||||
|
||||
test('fall-back on base index if index not available @couchdb @network', async ({ page }) => {
|
||||
await page.route('**/_view/object_names', (route) => {
|
||||
route.fulfill({
|
||||
status: 404
|
||||
});
|
||||
});
|
||||
await createObjectsForSearch(page);
|
||||
|
||||
let isObjectNamesViewAvailable = false;
|
||||
let isFindUsedForSearch = false;
|
||||
|
||||
page.on('request', async (request) => {
|
||||
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
|
||||
const isHeadRequest = request.method().toLowerCase() === 'head';
|
||||
|
||||
if (isObjectNamesRequest && isHeadRequest) {
|
||||
const response = await request.response();
|
||||
isObjectNamesViewAvailable = response.status() === 200;
|
||||
}
|
||||
});
|
||||
|
||||
page.on('request', (request) => {
|
||||
const isFindRequest = request.url().endsWith('_find');
|
||||
const isPostRequest = request.method().toLowerCase() === 'post';
|
||||
|
||||
if (isFindRequest && isPostRequest) {
|
||||
isFindUsedForSearch = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Full search for object
|
||||
await grandSearchInput.pressSequentially('Clock', { delay: 100 });
|
||||
|
||||
// Wait for search to finish
|
||||
await waitForSearchCompletion(page);
|
||||
console.info(
|
||||
`isObjectNamesViewAvailable: ${isObjectNamesViewAvailable} | isFindUsedForSearch: ${isFindUsedForSearch}`
|
||||
);
|
||||
expect(isObjectNamesViewAvailable).toBe(false);
|
||||
expect(isFindUsedForSearch).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('Search results are debounced @couchdb @network', async ({ page }) => {
|
||||
// Unfortunately 404s are always logged to the JavaScript console and can't be suppressed
|
||||
// A 404 is now thrown when we test for the presence of the object names view used by search.
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6179'
|
||||
@ -199,11 +282,17 @@ test.describe('Grand Search', () => {
|
||||
await createObjectsForSearch(page);
|
||||
|
||||
let networkRequests = [];
|
||||
|
||||
page.on('request', (request) => {
|
||||
const searchRequest =
|
||||
request.url().endsWith('_find') || request.url().includes('by_keystring');
|
||||
const fetchRequest = request.resourceType() === 'fetch';
|
||||
if (searchRequest && fetchRequest) {
|
||||
const isSearchRequest =
|
||||
request.url().endsWith('object_names') ||
|
||||
request.url().endsWith('_find') ||
|
||||
request.url().includes('by_keystring');
|
||||
const isFetchRequest = request.resourceType() === 'fetch';
|
||||
// CouchDB search results in a one-time head request to test for the presence of an index.
|
||||
const isHeadRequest = request.method().toLowerCase() === 'head';
|
||||
|
||||
if (isSearchRequest && isFetchRequest && !isHeadRequest) {
|
||||
networkRequests.push(request);
|
||||
}
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* eslint-disable playwright/no-conditional-in-test */
|
||||
/* eslint-disable playwright/no-conditional-expect */
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
@ -31,6 +33,104 @@ Enim nec dui nunc mattis. Cursus turpis massa tincidunt dui ut. Donec adipiscing
|
||||
Proin libero nunc consequat interdum varius sit amet mattis vulputate. Metus dictum at tempor commodo ullamcorper a lacus vestibulum sed. Quisque non tellus orci ac auctor augue mauris. Id ornare arcu odio ut. Rhoncus est pellentesque elit ullamcorper dignissim. Senectus et netus et malesuada fames ac turpis egestas. Volutpat ac tincidunt vitae semper quis lectus nulla. Adipiscing elit duis tristique sollicitudin. Ipsum faucibus vitae aliquet nec ullamcorper sit. Gravida neque convallis a cras semper auctor neque vitae tempus. Porttitor leo a diam sollicitudin tempor id. Dictum non consectetur a erat nam at lectus. At volutpat diam ut venenatis tellus in. Morbi enim nunc faucibus a pellentesque sit amet. Cursus in hac habitasse platea. Sed augue lacus viverra vitae.
|
||||
`;
|
||||
|
||||
const viewsTabsMatrix = {
|
||||
Clock: {
|
||||
Browse: ['Properties']
|
||||
},
|
||||
'Condition Set': {
|
||||
Browse: ['Properties', 'Elements', 'Annotations'],
|
||||
Edit: ['Elements', 'Properties']
|
||||
},
|
||||
'Condition Widget': {
|
||||
Browse: ['Properties', 'Styles'],
|
||||
Edit: ['Styles', 'Properties']
|
||||
},
|
||||
'Display Layout': {
|
||||
Browse: ['Properties', 'Elements', 'Styles'],
|
||||
Edit: ['Elements', 'Styles', 'Properties']
|
||||
},
|
||||
'Event Message Generator': {
|
||||
Browse: ['Properties']
|
||||
},
|
||||
'Event Message Generator with Acknowledge': {
|
||||
Browse: ['Properties']
|
||||
},
|
||||
'Example Imagery': {
|
||||
Browse: ['Properties', 'Annotations']
|
||||
},
|
||||
'Flexible Layout': {
|
||||
Browse: ['Properties', 'Elements', 'Styles'],
|
||||
Edit: ['Elements', 'Styles', 'Properties']
|
||||
},
|
||||
Folder: {
|
||||
Browse: ['Properties']
|
||||
},
|
||||
'Gantt Chart': {
|
||||
Browse: ['Properties', 'Config', 'Elements'],
|
||||
Edit: ['Config', 'Elements', 'Properties']
|
||||
},
|
||||
Gauge: {
|
||||
Browse: ['Properties', 'Elements', 'Styles'],
|
||||
Edit: ['Elements', 'Styles', 'Properties']
|
||||
},
|
||||
Graph: {
|
||||
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
|
||||
Edit: ['Config', 'Elements', 'Styles', 'Properties']
|
||||
},
|
||||
Hyperlink: {
|
||||
Browse: ['Properties'],
|
||||
required: {
|
||||
url: 'https://www.google.com',
|
||||
displayText: 'Google'
|
||||
}
|
||||
},
|
||||
'LAD Table': {
|
||||
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
|
||||
Edit: ['Config', 'Elements', 'Styles', 'Properties']
|
||||
},
|
||||
'LAD Table Set': {
|
||||
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
|
||||
Edit: ['Config', 'Elements', 'Styles', 'Properties']
|
||||
},
|
||||
Notebook: {
|
||||
Browse: ['Properties']
|
||||
},
|
||||
'Overlay Plot': {
|
||||
Browse: ['Properties', 'Config', 'Annotations', 'Styles'],
|
||||
Edit: ['Config', 'Elements', 'Styles', 'Filters', 'Properties']
|
||||
},
|
||||
'Scatter Plot': {
|
||||
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
|
||||
Edit: ['Config', 'Elements', 'Styles', 'Properties']
|
||||
},
|
||||
'Sine Wave Generator': {
|
||||
Browse: ['Properties', 'Annotations']
|
||||
},
|
||||
'Stacked Plot': {
|
||||
Browse: ['Properties', 'Config', 'Annotations', 'Elements', 'Styles'],
|
||||
Edit: ['Config', 'Elements', 'Styles', 'Properties']
|
||||
},
|
||||
'Tabs View': {
|
||||
Browse: ['Properties', 'Elements', 'Styles'],
|
||||
Edit: ['Elements', 'Styles', 'Properties']
|
||||
},
|
||||
'Telemetry Table': {
|
||||
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
|
||||
Edit: ['Config', 'Elements', 'Styles', 'Filters', 'Properties']
|
||||
},
|
||||
'Time List': {
|
||||
Browse: ['Properties', 'Config', 'Elements'],
|
||||
Edit: ['Config', 'Elements', 'Properties']
|
||||
},
|
||||
'Time Strip': {
|
||||
Browse: ['Properties', 'Elements'],
|
||||
Edit: ['Elements', 'Properties']
|
||||
},
|
||||
Timer: {
|
||||
Browse: ['Properties']
|
||||
}
|
||||
};
|
||||
|
||||
test.describe('Inspector tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
@ -72,4 +172,49 @@ test.describe('Inspector tests', () => {
|
||||
|
||||
await expect(lastInspectorPropertyValue).toBeInViewport();
|
||||
});
|
||||
|
||||
test(`Inspector tabs show the correct tabs per view and mode`, async ({ page }) => {
|
||||
// loop through each view type
|
||||
for (const view of Object.keys(viewsTabsMatrix)) {
|
||||
const viewConfig = viewsTabsMatrix[view];
|
||||
const createOptions = {
|
||||
type: view,
|
||||
name: view
|
||||
};
|
||||
|
||||
// create and navigate to view;
|
||||
const objectInfo = await createDomainObjectWithDefaults(
|
||||
page,
|
||||
createOptions,
|
||||
viewConfig.required ? viewConfig.required : {}
|
||||
);
|
||||
await page.goto(objectInfo.url);
|
||||
|
||||
// verify correct number of tabs for browse mode
|
||||
expect(await page.getByRole('tab').count()).toBe(Object.keys(viewConfig.Browse).length);
|
||||
|
||||
// verify correct order of tabs for browse mode
|
||||
for (const [index, value] of Object.entries(viewConfig.Browse)) {
|
||||
const tab = page.getByRole('tab').nth(index);
|
||||
await expect(tab).toHaveText(value);
|
||||
}
|
||||
|
||||
// enter Edit if necessary
|
||||
if (viewConfig.Edit) {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// verify correct number of tabs for edit mode
|
||||
expect(await page.getByRole('tab').count()).toBe(Object.keys(viewConfig.Edit).length);
|
||||
|
||||
// verify correct order of tabs for edit mode
|
||||
for (const [index, value] of Object.entries(viewConfig.Edit)) {
|
||||
const tab = page.getByRole('tab').nth(index);
|
||||
await expect(tab).toHaveText(value);
|
||||
}
|
||||
|
||||
await page.getByLabel('Save').first().click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -213,7 +213,6 @@ test.describe('Navigation memory leak is not detected in', () => {
|
||||
page,
|
||||
'example-imagery-memory-leak-test'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@ -317,6 +316,12 @@ test.describe('Navigation memory leak is not detected in', () => {
|
||||
|
||||
// Manually invoke the garbage collector once all references are removed.
|
||||
window.gc();
|
||||
window.gc();
|
||||
window.gc();
|
||||
|
||||
setTimeout(() => {
|
||||
window.gc();
|
||||
}, 1000);
|
||||
|
||||
return gcPromise;
|
||||
});
|
||||
|
@ -64,7 +64,7 @@ test.describe('Tabs View', () => {
|
||||
page.goto(tabsView.url);
|
||||
|
||||
// select first tab
|
||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
||||
await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
|
@ -40,6 +40,9 @@ test.describe('Visual - Inspector @ally @clock', () => {
|
||||
});
|
||||
|
||||
test('Inspector from overlay_plot_with_delay_storage @localStorage', async ({ page, theme }) => {
|
||||
// navigate to the plot
|
||||
await page.getByRole('gridcell', { name: 'Overlay Plot with 5s Delay' }).click();
|
||||
|
||||
//Expand the Inspector Pane
|
||||
await page.getByRole('button', { name: 'Inspect' }).click();
|
||||
|
||||
|
@ -26,14 +26,25 @@ import fs from 'fs';
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
|
||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
import { VISUAL_FIXED_URL } from '../../constants.js';
|
||||
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
|
||||
import {
|
||||
getFirstActivity,
|
||||
setBoundsToSpanAllActivities,
|
||||
setDraftStatusForPlan
|
||||
} from '../../helper/planningUtils.js';
|
||||
|
||||
const examplePlanSmall2 = JSON.parse(
|
||||
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.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' });
|
||||
});
|
||||
test('Gantt Chart View', async ({ page, theme }) => {
|
||||
|
@ -27,14 +27,21 @@ import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appAct
|
||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
import { waitForAnimations } from '../../baseFixtures.js';
|
||||
import { VISUAL_FIXED_URL } from '../../constants.js';
|
||||
import { setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
|
||||
import { getFirstActivity, setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
|
||||
|
||||
const examplePlanSmall2 = JSON.parse(
|
||||
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.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' });
|
||||
});
|
||||
test('Time Strip View', async ({ page, theme }) => {
|
||||
|
@ -42,6 +42,7 @@ const examplePlanSmall2 = JSON.parse(
|
||||
);
|
||||
|
||||
const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);
|
||||
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
|
||||
|
||||
test.describe('Visual - Timelist progress bar @clock @a11y', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@ -59,6 +60,11 @@ test.describe('Visual - Timelist progress bar @clock @a11y', () => {
|
||||
|
||||
test.describe('Visual - Plan View @a11y', () => {
|
||||
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' });
|
||||
});
|
||||
|
||||
|
@ -83,7 +83,7 @@ test.describe('Grand Search @a11y', () => {
|
||||
);
|
||||
|
||||
// Save and finish editing the Display Layout
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Search for the object
|
||||
|
@ -100,9 +100,12 @@ test.describe('Flexible Layout styling @a11y', () => {
|
||||
);
|
||||
|
||||
// Save Flexible Layout
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Select styles tab
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
`Saved Styled Flex Layout with Styled StackedPlot (theme: '${theme}')`
|
||||
@ -124,17 +127,30 @@ test.describe('Stacked Plot styling @a11y', () => {
|
||||
name: 'StackedPlot1'
|
||||
});
|
||||
|
||||
// Create two SWGs and attach them to the Stacked Plot
|
||||
// Create an overlay plots to hold the SWGs
|
||||
const overlayPlot1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Overlay Plot 1',
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
const overlayPlot2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Overlay Plot 2',
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
// Create two SWGs and attach them to the overlay plots
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Sine Wave Generator 1',
|
||||
parent: stackedPlot.uuid
|
||||
parent: overlayPlot1.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Sine Wave Generator 2',
|
||||
parent: stackedPlot.uuid
|
||||
parent: overlayPlot2.uuid
|
||||
});
|
||||
});
|
||||
|
||||
@ -177,7 +193,7 @@ test.describe('Stacked Plot styling @a11y', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||
);
|
||||
|
||||
await percySnapshot(page, `Edit Mode StackedPlot with Styled SWG (theme: '${theme}')`);
|
||||
|
@ -46,6 +46,24 @@ class EventMetadataProvider {
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const inPlaceUpdateMetadataValue = {
|
||||
key: 'messageId',
|
||||
name: 'row identifier',
|
||||
format: 'string',
|
||||
useToUpdateInPlace: true
|
||||
};
|
||||
const eventAcknowledgeMetadataValue = {
|
||||
key: 'acknowledge',
|
||||
name: 'Acknowledge',
|
||||
format: 'string'
|
||||
};
|
||||
|
||||
const eventGeneratorWithAcknowledge = structuredClone(this.METADATA_BY_TYPE.eventGenerator);
|
||||
eventGeneratorWithAcknowledge.values.push(inPlaceUpdateMetadataValue);
|
||||
eventGeneratorWithAcknowledge.values.push(eventAcknowledgeMetadataValue);
|
||||
|
||||
this.METADATA_BY_TYPE.eventGeneratorWithAcknowledge = eventGeneratorWithAcknowledge;
|
||||
}
|
||||
|
||||
supportsMetadata(domainObject) {
|
||||
|
@ -0,0 +1,70 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
|
||||
*/
|
||||
|
||||
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
||||
|
||||
class EventWithAcknowledgeTelemetryProvider extends EventTelemetryProvider {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.unAcknowledgedData = undefined;
|
||||
}
|
||||
|
||||
generateData(firstObservedTime, count, startTime, duration, name) {
|
||||
if (this.unAcknowledgedData === undefined) {
|
||||
const unAcknowledgedData = super.generateData(
|
||||
firstObservedTime,
|
||||
count,
|
||||
startTime,
|
||||
duration,
|
||||
name
|
||||
);
|
||||
unAcknowledgedData.messageId = unAcknowledgedData.message;
|
||||
this.unAcknowledgedData = unAcknowledgedData;
|
||||
|
||||
return this.unAcknowledgedData;
|
||||
} else {
|
||||
const acknowledgedData = {
|
||||
...this.unAcknowledgedData,
|
||||
acknowledge: 'OK'
|
||||
};
|
||||
|
||||
this.unAcknowledgedData = undefined;
|
||||
|
||||
return acknowledgedData;
|
||||
}
|
||||
}
|
||||
|
||||
supportsRequest(domainObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
supportsSubscribe(domainObject) {
|
||||
return domainObject.type === 'eventGeneratorWithAcknowledge';
|
||||
}
|
||||
}
|
||||
|
||||
export default EventWithAcknowledgeTelemetryProvider;
|
@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import EventMetadataProvider from './EventMetadataProvider.js';
|
||||
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
||||
import EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js';
|
||||
|
||||
export default function EventGeneratorPlugin(options) {
|
||||
return function install(openmct) {
|
||||
@ -38,5 +39,20 @@ export default function EventGeneratorPlugin(options) {
|
||||
});
|
||||
openmct.telemetry.addProvider(new EventTelemetryProvider());
|
||||
openmct.telemetry.addProvider(new EventMetadataProvider());
|
||||
|
||||
openmct.types.addType('eventGeneratorWithAcknowledge', {
|
||||
name: 'Event Message Generator with Acknowledge',
|
||||
description:
|
||||
'For development use. Creates sample event message data stream and updates the event row with an acknowledgement.',
|
||||
cssClass: 'icon-generator-events',
|
||||
creatable: true,
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
duration: 2.5
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider());
|
||||
};
|
||||
}
|
||||
|
@ -108,6 +108,16 @@ const METADATA_BY_TYPE = {
|
||||
string: 'ON'
|
||||
}
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
singleSelectionThreshold: true,
|
||||
comparator: 'equals',
|
||||
possibleValues: [
|
||||
{ label: 'OFF', value: 0 },
|
||||
{ label: 'ON', value: 1 }
|
||||
]
|
||||
}
|
||||
],
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
|
@ -34,14 +34,16 @@ StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
|
||||
return domainObject.type === 'example.state-generator';
|
||||
};
|
||||
|
||||
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
|
||||
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback, options) {
|
||||
var duration = domainObject.telemetry.duration * 1000;
|
||||
|
||||
var interval = setInterval(function () {
|
||||
var interval = setInterval(() => {
|
||||
var now = Date.now();
|
||||
var datum = pointForTimestamp(now, duration, domainObject.name);
|
||||
datum.value = String(datum.value);
|
||||
callback(datum);
|
||||
if (!this.shouldBeFiltered(datum, options)) {
|
||||
datum.value = String(datum.value);
|
||||
callback(datum);
|
||||
}
|
||||
}, duration);
|
||||
|
||||
return function () {
|
||||
@ -63,9 +65,25 @@ StateGeneratorProvider.prototype.request = function (domainObject, options) {
|
||||
|
||||
var data = [];
|
||||
while (start <= end && data.length < 5000) {
|
||||
data.push(pointForTimestamp(start, duration, domainObject.name));
|
||||
const point = pointForTimestamp(start, duration, domainObject.name);
|
||||
|
||||
if (!this.shouldBeFiltered(point, options)) {
|
||||
data.push(point);
|
||||
}
|
||||
start += duration;
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
};
|
||||
|
||||
StateGeneratorProvider.prototype.shouldBeFiltered = function (point, options) {
|
||||
const valueToFilter = options?.filters?.state?.equals?.[0];
|
||||
|
||||
if (!valueToFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { value } = point;
|
||||
|
||||
return value !== Number(valueToFilter);
|
||||
};
|
||||
|
@ -20,6 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { seededRandom } from 'utils/random.js';
|
||||
|
||||
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-18732.jpg',
|
||||
@ -162,8 +164,8 @@ export default function () {
|
||||
};
|
||||
}
|
||||
|
||||
function getCompassValues(min, max) {
|
||||
return min + Math.random() * (max - min);
|
||||
function getCompassValues(min, max, timestamp) {
|
||||
return min + seededRandom(timestamp) * (max - min);
|
||||
}
|
||||
|
||||
function getImageSamples(configuration) {
|
||||
@ -283,9 +285,9 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
|
||||
utc: Math.floor(timestamp / delay) * delay,
|
||||
local: Math.floor(timestamp / delay) * delay,
|
||||
url,
|
||||
sunOrientation: getCompassValues(0, 360),
|
||||
cameraAzimuth: getCompassValues(0, 360),
|
||||
heading: getCompassValues(0, 360),
|
||||
sunOrientation: getCompassValues(0, 360, timestamp),
|
||||
cameraAzimuth: getCompassValues(0, 360, timestamp),
|
||||
heading: getCompassValues(0, 360, timestamp),
|
||||
transformations: navCamTransformations,
|
||||
imageDownloadName
|
||||
};
|
||||
|
1445
package-lock.json
generated
1445
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@ -16,25 +16,25 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.23.3",
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@types/d3-axis": "3.0.6",
|
||||
"@types/d3-scale": "4.0.8",
|
||||
"@types/d3-selection": "3.0.10",
|
||||
"@types/d3-shape": "3.0.0",
|
||||
"@types/d3-shape": "3.1.7",
|
||||
"@types/eventemitter3": "1.2.0",
|
||||
"@types/jasmine": "5.1.2",
|
||||
"@types/lodash": "4.17.0",
|
||||
"@vue/compiler-sfc": "3.4.3",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"babel-plugin-istanbul": "7.0.0",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"cspell": "7.3.8",
|
||||
"css-loader": "6.10.0",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "4.0.2",
|
||||
"d3-selection": "3.0.0",
|
||||
"d3-shape": "3.0.0",
|
||||
"d3-shape": "3.2.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-compat": "4.2.0",
|
||||
@ -51,7 +51,7 @@
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "5.0.0",
|
||||
"jasmine-core": "5.1.1",
|
||||
"jasmine-core": "5.6.0",
|
||||
"karma": "6.4.2",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-cli": "2.0.0",
|
||||
@ -64,14 +64,14 @@
|
||||
"karma-webpack": "5.0.1",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "12.0.0",
|
||||
"mathjs": "13.1.1",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"marked": "15.0.7",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"moment": "2.30.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.41",
|
||||
"npm-run-all2": "6.1.2",
|
||||
"nyc": "15.1.0",
|
||||
"nano": "10.1.4",
|
||||
"npm-run-all2": "7.0.2",
|
||||
"nyc": "17.1.0",
|
||||
"painterro": "1.2.87",
|
||||
"plotly.js-basic-dist-min": "2.29.1",
|
||||
"plotly.js-gl2d-dist-min": "2.20.0",
|
||||
@ -79,21 +79,21 @@
|
||||
"prettier-eslint": "16.3.0",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.12.1",
|
||||
"sanitize-html": "2.15.0",
|
||||
"sass": "1.71.1",
|
||||
"sass-loader": "14.1.1",
|
||||
"style-loader": "3.3.3",
|
||||
"style-loader": "4.0.0",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"tiny-emitter": "2.1.0",
|
||||
"typescript": "5.3.3",
|
||||
"uuid": "9.0.1",
|
||||
"uuid": "11.1.0",
|
||||
"vue": "3.4.24",
|
||||
"vue-eslint-parser": "9.4.2",
|
||||
"vue-loader": "16.8.3",
|
||||
"webpack": "5.90.3",
|
||||
"webpack": "5.98.0",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-dev-server": "5.0.2",
|
||||
"webpack-merge": "5.10.0"
|
||||
"webpack-merge": "6.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./e2e/test-results ./.nyc_output ./e2e/.nyc_output",
|
||||
@ -139,7 +139,7 @@
|
||||
"url": "git+https://github.com/nasa/openmct.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.2 <22"
|
||||
"node": ">=18.14.2 <23"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
@ -157,4 +157,4 @@
|
||||
"keywords": [
|
||||
"nasa"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -306,7 +306,6 @@ export class MCT extends EventEmitter {
|
||||
this.install(this.plugins.UserIndicator());
|
||||
this.install(this.plugins.Gauge());
|
||||
this.install(this.plugins.InspectorViews());
|
||||
this.install(this.plugins.Comps());
|
||||
}
|
||||
/**
|
||||
* Set path to where assets are hosted. This should be the path to main.js.
|
||||
|
@ -582,4 +582,15 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
_.isEqual(targets, otherTargets)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given type is annotatable
|
||||
* @param {string} type The type to check
|
||||
* @returns {boolean} Returns true if the type is annotatable
|
||||
*/
|
||||
isAnnotatableType(type) {
|
||||
const types = this.openmct.types.getAllTypes();
|
||||
|
||||
return types[type]?.definition?.annotatable;
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { isIdentifier } from '../objects/object-utils';
|
||||
|
||||
/**
|
||||
* @typedef {import('openmct').DomainObject} DomainObject
|
||||
*/
|
||||
@ -209,9 +211,15 @@ export default class CompositionCollection {
|
||||
this.#cleanUpMutables();
|
||||
const children = await this.#provider.load(this.domainObject);
|
||||
const childObjects = await Promise.all(
|
||||
children.map((c) => this.#publicAPI.objects.get(c, abortSignal))
|
||||
children.map((child) => {
|
||||
if (isIdentifier(child)) {
|
||||
return this.#publicAPI.objects.get(child, abortSignal);
|
||||
} else {
|
||||
return Promise.resolve(child);
|
||||
}
|
||||
})
|
||||
);
|
||||
childObjects.forEach((c) => this.add(c, true));
|
||||
childObjects.forEach((child) => this.add(child, true));
|
||||
this.#emit('load');
|
||||
|
||||
return childObjects;
|
||||
|
@ -96,8 +96,9 @@ export default class CompositionProvider {
|
||||
* object.
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* for which to load composition
|
||||
* @returns {Promise<Identifier[]>} a promise for
|
||||
* the Identifiers in this composition
|
||||
* @returns {Promise<Identifier[] | DomainObject[]>} a promise for
|
||||
* the Identifiers or Domain Objects in this composition. If Identifiers are returned,
|
||||
* they will be automatically resolved to domain objects by the API.
|
||||
*/
|
||||
load(domainObject) {
|
||||
throw new Error('This method must be implemented by a subclass.');
|
||||
|
@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
import { makeKeyString } from '../objects/object-utils.js';
|
||||
import { makeKeyString, parseKeyString } from '../objects/object-utils.js';
|
||||
import CompositionProvider from './CompositionProvider.js';
|
||||
|
||||
/**
|
||||
@ -75,7 +75,11 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
* the Identifiers in this composition
|
||||
*/
|
||||
load(domainObject) {
|
||||
return Promise.all(domainObject.composition);
|
||||
const identifiers = domainObject.composition
|
||||
.filter((idOrKeystring) => idOrKeystring !== null && idOrKeystring !== undefined)
|
||||
.map((idOrKeystring) => parseKeyString(idOrKeystring));
|
||||
|
||||
return Promise.all(identifiers);
|
||||
}
|
||||
/**
|
||||
* Attach listeners for changes to the composition of a given domain object.
|
||||
|
@ -35,7 +35,7 @@ export const DEFAULT_SHELVE_DURATIONS = [
|
||||
value: 900000
|
||||
},
|
||||
{
|
||||
name: 'Indefinite',
|
||||
name: 'Unlimited',
|
||||
value: null
|
||||
}
|
||||
];
|
||||
@ -136,17 +136,21 @@ export default class FaultManagementAPI {
|
||||
/**
|
||||
* Retrieves the available shelve durations from the provider, or the default durations if the
|
||||
* provider does not provide any.
|
||||
* @returns {ShelveDuration[]}
|
||||
* @returns {ShelveDuration[] | undefined}
|
||||
*/
|
||||
getShelveDurations() {
|
||||
return this.provider?.getShelveDurations() ?? DEFAULT_SHELVE_DURATIONS;
|
||||
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 indefinite
|
||||
* @property {number|null} value - The value of the shelve duration in milliseconds, or null for unlimited
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -27,6 +27,7 @@ import ConflictError from './ConflictError.js';
|
||||
import InMemorySearchProvider from './InMemorySearchProvider.js';
|
||||
import InterceptorRegistry from './InterceptorRegistry.js';
|
||||
import MutableDomainObject from './MutableDomainObject.js';
|
||||
import { isIdentifier, isKeyString } from './object-utils.js';
|
||||
import RootObjectProvider from './RootObjectProvider.js';
|
||||
import RootRegistry from './RootRegistry.js';
|
||||
import Transaction from './Transaction.js';
|
||||
@ -742,11 +743,19 @@ export default class ObjectAPI {
|
||||
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
|
||||
* @returns {Promise<Array<DomainObject>>} a promise containing an array of domain objects
|
||||
*/
|
||||
async getOriginalPath(identifier, path = [], abortSignal = null) {
|
||||
const domainObject = await this.get(identifier, abortSignal);
|
||||
async getOriginalPath(identifierOrObject, path = [], abortSignal = null) {
|
||||
let domainObject;
|
||||
|
||||
if (isKeyString(identifierOrObject) || isIdentifier(identifierOrObject)) {
|
||||
domainObject = await this.get(identifierOrObject, abortSignal);
|
||||
} else {
|
||||
domainObject = identifierOrObject;
|
||||
}
|
||||
|
||||
if (!domainObject) {
|
||||
return [];
|
||||
}
|
||||
|
||||
path.push(domainObject);
|
||||
const { location } = domainObject;
|
||||
if (location && !this.#pathContainsDomainObject(location, path)) {
|
||||
|
@ -21,8 +21,11 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const PRIORITIES = Object.freeze({
|
||||
HIGHEST: Infinity,
|
||||
HIGH: 1000,
|
||||
DEFAULT: 0,
|
||||
LOW: -1000
|
||||
LOW: -1000,
|
||||
LOWEST: -Infinity
|
||||
});
|
||||
|
||||
export default PRIORITIES;
|
||||
|
@ -250,6 +250,117 @@ export default class TelemetryAPI {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes objects for consistent serialization by:
|
||||
* 1. Removing non-plain objects (class instances) and functions
|
||||
* 2. Sorting object keys alphabetically to ensure consistent ordering
|
||||
*/
|
||||
sanitizeForSerialization(key, value) {
|
||||
// Handle null and primitives directly
|
||||
if (value === null || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Remove functions and non-plain objects (except arrays)
|
||||
if (
|
||||
typeof value === 'function' ||
|
||||
(Object.getPrototypeOf(value) !== Object.prototype && !Array.isArray(value))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// For plain objects, just sort the keys
|
||||
if (!Array.isArray(value)) {
|
||||
const sortedObject = {};
|
||||
const sortedKeys = Object.keys(value).sort();
|
||||
|
||||
sortedKeys.forEach((objectKey) => {
|
||||
sortedObject[objectKey] = value[objectKey];
|
||||
});
|
||||
|
||||
return sortedObject;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a domain object has numeric telemetry data.
|
||||
* A domain object has numeric telemetry if it:
|
||||
* 1. Has a telemetry property
|
||||
* 2. Has telemetry metadata with domain values (like timestamps)
|
||||
* 3. Has range values (measurements) where at least one is numeric
|
||||
*
|
||||
* @method hasNumericTelemetry
|
||||
* @param {import('openmct').DomainObject} domainObject The domain object to check
|
||||
* @returns {boolean} True if the object has numeric telemetry, false otherwise
|
||||
*/
|
||||
hasNumericTelemetry(domainObject) {
|
||||
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
const rangeValues = metadata.valuesForHints(['range']);
|
||||
const domains = metadata.valuesForHints(['domain']);
|
||||
|
||||
return (
|
||||
domains.length > 0 &&
|
||||
rangeValues.length > 0 &&
|
||||
!rangeValues.every((value) => value.format === 'string')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a numeric hash value for an options object. The hash is consistent
|
||||
* for equivalent option objects regardless of property order.
|
||||
*
|
||||
* This is used to create compact, unique cache keys for telemetry subscriptions with
|
||||
* different options configurations. The hash function ensures that identical options
|
||||
* objects will always generate the same hash value, while different options objects
|
||||
* (even with small differences) will generate different hash values.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} options The options object to hash
|
||||
* @returns {number} A positive integer hash of the options object
|
||||
*/
|
||||
#hashOptions(options) {
|
||||
const sanitizedOptionsString = JSON.stringify(
|
||||
options,
|
||||
this.sanitizeForSerialization.bind(this)
|
||||
);
|
||||
|
||||
let hash = 0;
|
||||
const prime = 31;
|
||||
const modulus = 1e9 + 9; // Large prime number
|
||||
|
||||
for (let i = 0; i < sanitizedOptionsString.length; i++) {
|
||||
const char = sanitizedOptionsString.charCodeAt(i);
|
||||
// Calculate new hash value while keeping numbers manageable
|
||||
hash = Math.floor((hash * prime + char) % modulus);
|
||||
}
|
||||
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique cache key for a telemetry subscription based on the
|
||||
* domain object identifier and options (which includes strategy).
|
||||
*
|
||||
* Uses a hash of the options object to create compact cache keys while still
|
||||
* ensuring unique keys for different subscription configurations.
|
||||
*
|
||||
* @private
|
||||
* @param {import('openmct').DomainObject} domainObject The domain object being subscribed to
|
||||
* @param {Object} options The subscription options object (including strategy)
|
||||
* @returns {string} A unique key string for caching the subscription
|
||||
*/
|
||||
#getSubscriptionCacheKey(domainObject, options) {
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
|
||||
return `${keyString}:${this.#hashOptions(options)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
|
||||
* The request will be modified when it is received and will be returned in it's modified state
|
||||
@ -418,16 +529,14 @@ export default class TelemetryAPI {
|
||||
this.#subscribeCache = {};
|
||||
}
|
||||
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
|
||||
// Override the requested strategy with the strategy supported by the provider
|
||||
const optionsWithSupportedStrategy = {
|
||||
...options,
|
||||
strategy: supportedStrategy
|
||||
};
|
||||
// If batching is supported, we need to cache a subscription for each strategy -
|
||||
// latest and batched.
|
||||
const cacheKey = `${keyString}:${supportedStrategy}`;
|
||||
|
||||
const cacheKey = this.#getSubscriptionCacheKey(domainObject, optionsWithSupportedStrategy);
|
||||
let subscriber = this.#subscribeCache[cacheKey];
|
||||
|
||||
if (!subscriber) {
|
||||
@ -678,15 +787,6 @@ export default class TelemetryAPI {
|
||||
return this.metadataCache.get(domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a domain object from the telemetry metadata cache.
|
||||
* @param {import('openmct').DomainObject} domainObject
|
||||
*/
|
||||
|
||||
removeMetadataFromCache(domainObject) {
|
||||
this.metadataCache.delete(domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value formatter for a given valueMetadata.
|
||||
*
|
||||
|
@ -86,23 +86,14 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
}
|
||||
this._setTimeSystem(this.options.timeContext.getTimeSystem());
|
||||
this.lastBounds = this.options.timeContext.getBounds();
|
||||
// prioritize passed options over time bounds
|
||||
if (this.options.start) {
|
||||
this.lastBounds.start = this.options.start;
|
||||
}
|
||||
if (this.options.end) {
|
||||
this.lastBounds.end = this.options.end;
|
||||
}
|
||||
this._watchBounds();
|
||||
this._watchTimeSystem();
|
||||
this._watchTimeModeChange();
|
||||
|
||||
const historicalTelemetryLoadedPromise = this._requestHistoricalTelemetry();
|
||||
this._requestHistoricalTelemetry();
|
||||
this._initiateSubscriptionTelemetry();
|
||||
|
||||
this.loaded = true;
|
||||
|
||||
return historicalTelemetryLoadedPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,7 +113,6 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
}
|
||||
|
||||
this.removeAllListeners();
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -178,7 +168,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this._processNewTelemetry(historicalData, false);
|
||||
this._processNewTelemetry(historicalData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -192,9 +182,10 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
const options = { ...this.options };
|
||||
//We always want to receive all available values in telemetry tables.
|
||||
options.strategy = this.openmct.telemetry.SUBSCRIBE_STRATEGY.BATCH;
|
||||
|
||||
this.unsubscribe = this.openmct.telemetry.subscribe(
|
||||
this.domainObject,
|
||||
(datum) => this._processNewTelemetry(datum, true),
|
||||
(datum) => this._processNewTelemetry(datum),
|
||||
options
|
||||
);
|
||||
}
|
||||
@ -205,10 +196,9 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
*
|
||||
* @param {(Object|Object[])} telemetryData - telemetry data object or
|
||||
* array of telemetry data objects
|
||||
* @param {boolean} isSubscriptionData - `true` if the telemetry data is new subscription data,
|
||||
* @private
|
||||
*/
|
||||
_processNewTelemetry(telemetryData, isSubscriptionData = false) {
|
||||
_processNewTelemetry(telemetryData) {
|
||||
if (telemetryData === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -223,19 +213,12 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
let hasDataBeforeStartBound = false;
|
||||
let size = this.options.size;
|
||||
let enforceSize = size !== undefined && this.options.enforceSize;
|
||||
const boundsToUse = this.lastBounds;
|
||||
if (!isSubscriptionData && this.options.start) {
|
||||
boundsToUse.start = this.options.start;
|
||||
}
|
||||
if (!isSubscriptionData && this.options.end) {
|
||||
boundsToUse.end = this.options.end;
|
||||
}
|
||||
|
||||
// loop through, sort and dedupe
|
||||
for (let datum of data) {
|
||||
parsedValue = this.parseTime(datum);
|
||||
beforeStartOfBounds = parsedValue < boundsToUse.start;
|
||||
afterEndOfBounds = parsedValue > boundsToUse.end;
|
||||
beforeStartOfBounds = parsedValue < this.lastBounds.start;
|
||||
afterEndOfBounds = parsedValue > this.lastBounds.end;
|
||||
|
||||
if (
|
||||
!afterEndOfBounds &&
|
||||
@ -414,10 +397,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this.emit('add', added, [this.boundedTelemetry.length]);
|
||||
}
|
||||
} else {
|
||||
// user bounds change, reset and remove initial requested bounds (we're using new bounds)
|
||||
delete this.options?.start;
|
||||
delete this.options?.end;
|
||||
this.lastBounds = bounds;
|
||||
// user bounds change, reset
|
||||
this._reset();
|
||||
}
|
||||
}
|
||||
@ -497,9 +477,9 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this.boundedTelemetry = [];
|
||||
this.futureBuffer = [];
|
||||
|
||||
const telemetryLoadPromise = this._requestHistoricalTelemetry();
|
||||
this.emit('clear');
|
||||
|
||||
this.emit('clear', telemetryLoadPromise);
|
||||
this._requestHistoricalTelemetry();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -359,6 +359,18 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
* @override
|
||||
*/
|
||||
isFixed() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.isFixed(...arguments);
|
||||
} else {
|
||||
return super.isFixed(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
* @override
|
||||
@ -400,7 +412,7 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the time context to the global time context
|
||||
* Reset the time context from the global time context
|
||||
*/
|
||||
resetContext() {
|
||||
if (this.upstreamTimeContext) {
|
||||
@ -428,6 +440,10 @@ class IndependentTimeContext extends TimeContext {
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.getBounds());
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||
// Also emit the mode in case it's different from previous time context
|
||||
if (this.getMode()) {
|
||||
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -502,6 +518,10 @@ class IndependentTimeContext extends TimeContext {
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.getBounds());
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||
// Also emit the mode in case it's different from the global time context
|
||||
if (this.getMode()) {
|
||||
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
|
||||
}
|
||||
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
|
||||
this.globalTimeContext.emit('refreshContext', viewKey);
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
|
||||
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
|
||||
|
||||
import { TIME_CONTEXT_EVENTS } from './constants';
|
||||
import GlobalTimeContext from './GlobalTimeContext.js';
|
||||
|
||||
/**
|
||||
@ -142,7 +143,7 @@ class TimeAPI extends GlobalTimeContext {
|
||||
addIndependentContext(key, value, clockKey) {
|
||||
let timeContext = this.getIndependentContext(key);
|
||||
|
||||
//stop following upstream time context since the view has it's own
|
||||
//stop following upstream time context since the view has its own
|
||||
timeContext.resetContext();
|
||||
|
||||
if (clockKey) {
|
||||
@ -152,6 +153,9 @@ class TimeAPI extends GlobalTimeContext {
|
||||
timeContext.setMode(FIXED_MODE_KEY, value);
|
||||
}
|
||||
|
||||
// Also emit the mode in case it's different from the previous time context
|
||||
timeContext.emit(TIME_CONTEXT_EVENTS.modeChanged, structuredClone(timeContext.getMode()));
|
||||
|
||||
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
|
||||
this.emit('refreshContext', key);
|
||||
|
||||
|
@ -89,6 +89,17 @@ export default class TypeRegistry {
|
||||
get(typeKey) {
|
||||
return this.types[typeKey] || UNKNOWN_TYPE;
|
||||
}
|
||||
/**
|
||||
* List all registered types.
|
||||
* @returns {Type[]} all registered types
|
||||
*/
|
||||
getAllTypes() {
|
||||
return this.types;
|
||||
}
|
||||
/**
|
||||
* Import legacy types.
|
||||
* @param {TypeDefinition[]} types the types to import
|
||||
*/
|
||||
importLegacyTypes(types) {
|
||||
types
|
||||
.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
|
||||
|
@ -25,10 +25,14 @@
|
||||
* Originally created by hudsonfoo on 09/02/16
|
||||
*/
|
||||
|
||||
function replaceDotsWithUnderscores(filename) {
|
||||
const regex = /\./gi;
|
||||
function sanitizeFilename(filename) {
|
||||
const replacedPeriods = filename.replace(/\./g, '_');
|
||||
const safeFilename = replacedPeriods.replace(/[^a-zA-Z0-9_\-.\s]/g, '');
|
||||
|
||||
return filename.replace(regex, '_');
|
||||
// Handle leading/trailing spaces and periods
|
||||
const trimmedFilename = safeFilename.trim().replace(/^\.+|\.+$/g, '');
|
||||
|
||||
return trimmedFilename;
|
||||
}
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
@ -150,7 +154,7 @@ class ImageExporter {
|
||||
* @returns {promise}
|
||||
*/
|
||||
async exportJPG(element, filename, className) {
|
||||
const processedFilename = replaceDotsWithUnderscores(filename);
|
||||
const processedFilename = sanitizeFilename(filename);
|
||||
|
||||
const img = await this.renderElement(element, {
|
||||
imageType: 'jpg',
|
||||
@ -167,7 +171,7 @@ class ImageExporter {
|
||||
* @returns {promise}
|
||||
*/
|
||||
async exportPNG(element, filename, className) {
|
||||
const processedFilename = replaceDotsWithUnderscores(filename);
|
||||
const processedFilename = sanitizeFilename(filename);
|
||||
|
||||
const img = await this.renderElement(element, {
|
||||
imageType: 'png',
|
||||
|
@ -24,6 +24,9 @@ export default function (folderName, couchPlugin, searchFilter) {
|
||||
location: 'ROOT'
|
||||
});
|
||||
}
|
||||
},
|
||||
search() {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
});
|
||||
|
||||
@ -35,9 +38,17 @@ export default function (folderName, couchPlugin, searchFilter) {
|
||||
);
|
||||
},
|
||||
load() {
|
||||
return couchProvider.getObjectsByFilter(searchFilter).then((objects) => {
|
||||
return objects.map((object) => object.identifier);
|
||||
});
|
||||
let searchResults;
|
||||
|
||||
if (searchFilter.viewName !== undefined) {
|
||||
// Use a view to search, instead of an _all_docs find
|
||||
searchResults = couchProvider.getObjectsByView(searchFilter);
|
||||
} else {
|
||||
// Use the _find endpoint to search _all_docs
|
||||
searchResults = couchProvider.getObjectsByFilter(searchFilter);
|
||||
}
|
||||
|
||||
return searchResults;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -41,9 +41,10 @@ export default class LADTableConfiguration extends EventEmitter {
|
||||
}
|
||||
|
||||
getConfiguration() {
|
||||
const configuration = this.domainObject.configuration || {};
|
||||
configuration.hiddenColumns = configuration.hiddenColumns || {};
|
||||
const configuration = this.domainObject.configuration ?? {};
|
||||
configuration.hiddenColumns = configuration.hiddenColumns ?? {};
|
||||
configuration.isFixedLayout = configuration.isFixedLayout ?? true;
|
||||
configuration.objectStyles = configuration.objectStyles ?? {};
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import LadTableConfiguration from './components/LadTableConfiguration.vue';
|
||||
export default function LADTableConfigurationViewProvider(openmct) {
|
||||
return {
|
||||
key: 'lad-table-configuration',
|
||||
name: 'LAD Table Configuration',
|
||||
name: 'Config',
|
||||
canView(selection) {
|
||||
if (selection.length !== 1 || selection[0].length === 0) {
|
||||
return false;
|
||||
@ -61,7 +61,7 @@ export default function LADTableConfigurationViewProvider(openmct) {
|
||||
_destroy = destroy;
|
||||
},
|
||||
priority() {
|
||||
return 1;
|
||||
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
|
||||
},
|
||||
destroy() {
|
||||
if (_destroy) {
|
||||
|
@ -22,28 +22,24 @@
|
||||
|
||||
<template>
|
||||
<div class="c-inspect-properties">
|
||||
<template v-if="isEditing">
|
||||
<div class="c-inspect-properties__header">Table Column Visibility</div>
|
||||
<ul class="c-inspect-properties__section">
|
||||
<li v-for="(title, key) in headers" :key="key" class="c-inspect-properties__row">
|
||||
<div class="c-inspect-properties__label" title="Show or hide column">
|
||||
<label :for="key + 'ColumnControl'">{{ title }}</label>
|
||||
</div>
|
||||
<div class="c-inspect-properties__value">
|
||||
<input
|
||||
:id="key + 'ColumnControl'"
|
||||
type="checkbox"
|
||||
:checked="configuration.hiddenColumns[key] !== true"
|
||||
@change="toggleColumn(key)"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="c-inspect-properties__header">LAD Table Configuration</div>
|
||||
<div class="c-inspect-properties__row--span-all">Only available in edit mode.</div>
|
||||
</template>
|
||||
<div class="c-inspect-properties__header">Table Column Visibility</div>
|
||||
<ul class="c-inspect-properties__section">
|
||||
<li v-for="(title, key) in headers" :key="key" class="c-inspect-properties__row">
|
||||
<div class="c-inspect-properties__label" title="Show or hide column">
|
||||
<label :for="key + 'ColumnControl'">{{ title }}</label>
|
||||
</div>
|
||||
<div class="c-inspect-properties__value">
|
||||
<input
|
||||
v-if="isEditing"
|
||||
:id="key + 'ColumnControl'"
|
||||
type="checkbox"
|
||||
:checked="configuration.hiddenColumns[key] !== true"
|
||||
@change="toggleColumn(key)"
|
||||
/>
|
||||
<span v-if="!isEditing && configuration.hiddenColumns[key] !== true">Visible</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -62,7 +58,8 @@ export default {
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
configuration: ladTableConfiguration.getConfiguration(),
|
||||
items: [],
|
||||
ladTableObjects: []
|
||||
ladTableObjects: [],
|
||||
ladTelemetryObjects: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -150,11 +147,14 @@ export default {
|
||||
this.ladTableObjects.push(ladTable);
|
||||
|
||||
const composition = this.openmct.composition.get(ladTable.domainObject);
|
||||
|
||||
composition.on('add', this.addItem);
|
||||
composition.on('remove', this.removeItem);
|
||||
composition.load();
|
||||
|
||||
this.compositions.push({
|
||||
composition
|
||||
composition,
|
||||
addCallback: this.addItem,
|
||||
removeCallback: this.removeItem
|
||||
});
|
||||
},
|
||||
removeLadTable(identifier) {
|
||||
|
@ -39,6 +39,9 @@ export default function plugin() {
|
||||
cssClass: 'icon-tabular-lad',
|
||||
initialize(domainObject) {
|
||||
domainObject.composition = [];
|
||||
domainObject.configuration = {
|
||||
objectStyles: {}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -332,7 +332,11 @@ export default {
|
||||
this.domainObject.configuration.axes.xKey === undefined ||
|
||||
this.domainObject.configuration.axes.yKey === undefined
|
||||
) {
|
||||
return;
|
||||
const { xKey, yKey } = this.identifyAxesKeys(axisMetadata);
|
||||
this.openmct.objects.mutate(this.domainObject, 'configuration.axes', {
|
||||
xKey,
|
||||
yKey
|
||||
});
|
||||
}
|
||||
|
||||
let xValues = [];
|
||||
@ -431,6 +435,30 @@ export default {
|
||||
subscribeToAll() {
|
||||
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||
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 };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -41,7 +41,7 @@ export default function BarGraphInspectorViewProvider(openmct) {
|
||||
_destroy = destroy;
|
||||
},
|
||||
priority: function () {
|
||||
return openmct.priority.HIGH + 1;
|
||||
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
|
||||
},
|
||||
destroy: function () {
|
||||
if (_destroy) {
|
||||
|
@ -40,7 +40,7 @@ export default function ScatterPlotInspectorViewProvider(openmct) {
|
||||
_destroy = destroy;
|
||||
},
|
||||
priority: function () {
|
||||
return openmct.priority.HIGH + 1;
|
||||
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
|
||||
},
|
||||
destroy: function () {
|
||||
if (_destroy) {
|
||||
|
@ -1,85 +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 mount from 'utils/mount';
|
||||
|
||||
import CompsInspectorView from './components/CompsInspectorView.vue';
|
||||
|
||||
export default class ConditionSetViewProvider {
|
||||
constructor(openmct, compsManagerPool) {
|
||||
this.openmct = openmct;
|
||||
this.name = 'Config';
|
||||
this.key = 'comps-configuration';
|
||||
this.compsManagerPool = compsManagerPool;
|
||||
}
|
||||
|
||||
canView(selection) {
|
||||
if (selection.length !== 1 || selection[0].length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let object = selection[0][0].context.item;
|
||||
return object && object.type === 'comps';
|
||||
}
|
||||
|
||||
view(selection) {
|
||||
let _destroy = null;
|
||||
const domainObject = selection[0][0].context.item;
|
||||
const openmct = this.openmct;
|
||||
const compsManagerPool = this.compsManagerPool;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
const { destroy } = mount(
|
||||
{
|
||||
el: element,
|
||||
components: {
|
||||
CompsInspectorView: CompsInspectorView
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
compsManagerPool
|
||||
},
|
||||
template: '<comps-inspector-view></comps-inspector-view>'
|
||||
},
|
||||
{
|
||||
app: openmct.app,
|
||||
element
|
||||
}
|
||||
);
|
||||
_destroy = destroy;
|
||||
},
|
||||
showTab: function (isEditing) {
|
||||
return isEditing;
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
},
|
||||
destroy: function () {
|
||||
if (_destroy) {
|
||||
_destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,345 +0,0 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
|
||||
export default class CompsManager extends EventEmitter {
|
||||
#openmct;
|
||||
#domainObject;
|
||||
#composition;
|
||||
#telemetryObjects = {};
|
||||
#telemetryCollections = {};
|
||||
#telemetryLoadedPromises = [];
|
||||
#telemetryOptions = {};
|
||||
#loaded = false;
|
||||
#compositionLoaded = false;
|
||||
#telemetryProcessors = {};
|
||||
#loadVersion = 0;
|
||||
#currentLoadPromise = null;
|
||||
|
||||
constructor(openmct, domainObject) {
|
||||
super();
|
||||
this.#openmct = openmct;
|
||||
this.#domainObject = domainObject;
|
||||
this.clearData = this.clearData.bind(this);
|
||||
}
|
||||
|
||||
#getNextAlphabeticalParameterName() {
|
||||
const parameters = this.#domainObject.configuration.comps.parameters;
|
||||
const existingNames = new Set(parameters.map((p) => p.name));
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
|
||||
let suffix = '';
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
for (let letter of alphabet) {
|
||||
const proposedName = letter + suffix;
|
||||
if (!existingNames.has(proposedName)) {
|
||||
return proposedName;
|
||||
}
|
||||
}
|
||||
// Increment suffix after exhausting the alphabet
|
||||
suffix = (parseInt(suffix, 10) || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
addParameter(telemetryObject) {
|
||||
const keyString = this.#openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
const metaData = this.#openmct.telemetry.getMetadata(telemetryObject);
|
||||
const timeSystem = this.#openmct.time.getTimeSystem();
|
||||
const domains = metaData?.valuesForHints(['domain']);
|
||||
const timeMetaData = domains.find((d) => d.key === timeSystem.key);
|
||||
// in the valuesMetadata, find the first numeric data type
|
||||
const rangeItems = metaData.valueMetadatas.filter(
|
||||
(metaDatum) => metaDatum.hints && metaDatum.hints.range
|
||||
);
|
||||
rangeItems.sort((a, b) => a.hints.range - b.hints.range);
|
||||
let valueToUse = rangeItems[0]?.key;
|
||||
if (!valueToUse) {
|
||||
// if no numeric data type, just use the first one
|
||||
valueToUse = metaData.valueMetadatas[0]?.key;
|
||||
}
|
||||
this.#domainObject.configuration.comps.parameters.push({
|
||||
keyString,
|
||||
name: `${this.#getNextAlphabeticalParameterName()}`,
|
||||
valueToUse,
|
||||
testValue: 0,
|
||||
timeMetaData,
|
||||
accumulateValues: false,
|
||||
sampleSize: null
|
||||
});
|
||||
this.emit('parameterAdded', this.#domainObject);
|
||||
}
|
||||
|
||||
getParameters() {
|
||||
const parameters = this.#domainObject.configuration.comps.parameters;
|
||||
const parametersWithTimeKey = parameters.map((parameter) => {
|
||||
return {
|
||||
...parameter,
|
||||
timeKey: this.#telemetryCollections[parameter.keyString]?.timeKey
|
||||
};
|
||||
});
|
||||
return parametersWithTimeKey;
|
||||
}
|
||||
|
||||
getTelemetryObjectForParameter(keyString) {
|
||||
return this.#telemetryObjects[keyString];
|
||||
}
|
||||
|
||||
getMetaDataValuesForParameter(keyString) {
|
||||
const telemetryObject = this.getTelemetryObjectForParameter(keyString);
|
||||
const metaData = this.#openmct.telemetry.getMetadata(telemetryObject);
|
||||
return metaData.valueMetadatas;
|
||||
}
|
||||
|
||||
deleteParameter(keyString) {
|
||||
this.#domainObject.configuration.comps.parameters =
|
||||
this.#domainObject.configuration.comps.parameters.filter(
|
||||
(parameter) => parameter.keyString !== keyString
|
||||
);
|
||||
// if there are no parameters referencing this parameter keyString, remove the telemetry object too
|
||||
const parameterExists = this.#domainObject.configuration.comps.parameters.some(
|
||||
(parameter) => parameter.keyString === keyString
|
||||
);
|
||||
if (!parameterExists) {
|
||||
this.emit('parameterRemoved', this.#domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
setDomainObject(passedDomainObject) {
|
||||
this.#domainObject = passedDomainObject;
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this.#loaded;
|
||||
}
|
||||
|
||||
async load(telemetryOptions) {
|
||||
// Increment the load version to mark a new load operation
|
||||
const loadVersion = ++this.#loadVersion;
|
||||
|
||||
if (!_.isEqual(this.#telemetryOptions, telemetryOptions)) {
|
||||
console.debug(
|
||||
`😩 Reloading comps manager ${this.#domainObject.name} due to telemetry options change.`,
|
||||
telemetryOptions
|
||||
);
|
||||
this.#destroy();
|
||||
}
|
||||
|
||||
this.#telemetryOptions = telemetryOptions;
|
||||
|
||||
// Start the load process and store the promise
|
||||
this.#currentLoadPromise = (async () => {
|
||||
// Load composition if not already loaded
|
||||
if (!this.#compositionLoaded) {
|
||||
await this.#loadComposition();
|
||||
// Check if a newer load has been initiated
|
||||
if (loadVersion !== this.#loadVersion) {
|
||||
console.debug(
|
||||
`🔄 Reloading comps manager in composition wait ${this.#domainObject.name} due to newer load.`
|
||||
);
|
||||
await this.#currentLoadPromise;
|
||||
return;
|
||||
}
|
||||
this.#compositionLoaded = true;
|
||||
}
|
||||
|
||||
// Start listening to telemetry if not already done
|
||||
if (!this.#loaded) {
|
||||
await this.#startListeningToUnderlyingTelemetry();
|
||||
// Check again for newer load
|
||||
if (loadVersion !== this.#loadVersion) {
|
||||
console.debug(
|
||||
`🔄 Reloading comps manager in telemetry wait ${this.#domainObject.name} due to newer load.`
|
||||
);
|
||||
await this.#currentLoadPromise;
|
||||
return;
|
||||
}
|
||||
console.debug(
|
||||
`✅ Comps manager ${this.#domainObject.name} is ready.`,
|
||||
this.#telemetryCollections
|
||||
);
|
||||
this.#loaded = true;
|
||||
}
|
||||
})();
|
||||
|
||||
// Await the load process
|
||||
await this.#currentLoadPromise;
|
||||
}
|
||||
|
||||
async #startListeningToUnderlyingTelemetry() {
|
||||
Object.keys(this.#telemetryCollections).forEach((keyString) => {
|
||||
if (!this.#telemetryCollections[keyString].loaded) {
|
||||
this.#telemetryCollections[keyString].on('add', this.#getTelemetryProcessor(keyString));
|
||||
this.#telemetryCollections[keyString].on('clear', this.clearData);
|
||||
const telemetryLoadedPromise = this.#telemetryCollections[keyString].load();
|
||||
this.#telemetryLoadedPromises.push(telemetryLoadedPromise);
|
||||
}
|
||||
});
|
||||
await Promise.all(this.#telemetryLoadedPromises);
|
||||
this.#telemetryLoadedPromises = [];
|
||||
}
|
||||
|
||||
#destroy() {
|
||||
this.stopListeningToUnderlyingTelemetry();
|
||||
this.#composition = null;
|
||||
this.#telemetryCollections = {};
|
||||
this.#compositionLoaded = false;
|
||||
this.#loaded = false;
|
||||
this.#telemetryObjects = {};
|
||||
}
|
||||
|
||||
stopListeningToUnderlyingTelemetry() {
|
||||
this.#loaded = false;
|
||||
Object.keys(this.#telemetryCollections).forEach((keyString) => {
|
||||
const specificTelemetryProcessor = this.#telemetryProcessors[keyString];
|
||||
delete this.#telemetryProcessors[keyString];
|
||||
this.#telemetryCollections[keyString].off('add', specificTelemetryProcessor);
|
||||
this.#telemetryCollections[keyString].off('clear', this.clearData);
|
||||
this.#telemetryCollections[keyString].destroy();
|
||||
});
|
||||
}
|
||||
|
||||
getTelemetryObjects() {
|
||||
return this.#telemetryObjects;
|
||||
}
|
||||
|
||||
async #loadComposition() {
|
||||
this.#composition = this.#openmct.composition.get(this.#domainObject);
|
||||
if (this.#composition) {
|
||||
this.#composition.on('add', this.#addTelemetryObject);
|
||||
this.#composition.on('remove', this.#removeTelemetryObject);
|
||||
await this.#composition.load();
|
||||
}
|
||||
}
|
||||
|
||||
#getParameterForKeyString(keyString) {
|
||||
return this.#domainObject.configuration.comps.parameters.find(
|
||||
(parameter) => parameter.keyString === keyString
|
||||
);
|
||||
}
|
||||
|
||||
getTelemetryForComps(newTelemetry) {
|
||||
const telemetryForComps = {};
|
||||
const newTelemetryKey = Object.keys(newTelemetry)[0];
|
||||
const newTelemetryParameter = this.#getParameterForKeyString(newTelemetryKey);
|
||||
const newTelemetryData = newTelemetry[newTelemetryKey];
|
||||
const otherTelemetryKeys = Object.keys(this.#telemetryCollections).slice(0);
|
||||
if (newTelemetryParameter.accumulateValues) {
|
||||
telemetryForComps[newTelemetryKey] = this.#telemetryCollections[newTelemetryKey].getAll();
|
||||
} else {
|
||||
telemetryForComps[newTelemetryKey] = newTelemetryData;
|
||||
}
|
||||
otherTelemetryKeys.forEach((keyString) => {
|
||||
telemetryForComps[keyString] = [];
|
||||
});
|
||||
|
||||
const otherTelemetryKeysNotAccumulating = otherTelemetryKeys.filter(
|
||||
(keyString) => !this.#getParameterForKeyString(keyString).accumulateValues
|
||||
);
|
||||
const otherTelemetryKeysAccumulating = otherTelemetryKeys.filter(
|
||||
(keyString) => this.#getParameterForKeyString(keyString).accumulateValues
|
||||
);
|
||||
|
||||
// if we're accumulating, just add all the data
|
||||
otherTelemetryKeysAccumulating.forEach((keyString) => {
|
||||
telemetryForComps[keyString] = this.#telemetryCollections[keyString].getAll();
|
||||
});
|
||||
|
||||
// for the others, march through the new telemetry data and add data to the frame from the other telemetry objects
|
||||
// using LOCF
|
||||
newTelemetryData.forEach((newDatum) => {
|
||||
otherTelemetryKeysNotAccumulating.forEach((otherKeyString) => {
|
||||
const otherCollection = this.#telemetryCollections[otherKeyString];
|
||||
// otherwise we need to find the closest datum to the new datum
|
||||
let insertionPointForNewData = otherCollection._sortedIndex(newDatum);
|
||||
const otherCollectionData = otherCollection.getAll();
|
||||
if (insertionPointForNewData && insertionPointForNewData >= otherCollectionData.length) {
|
||||
insertionPointForNewData = otherCollectionData.length - 1;
|
||||
}
|
||||
// get the closest datum to the new datum
|
||||
const closestDatum = otherCollectionData[insertionPointForNewData];
|
||||
if (closestDatum) {
|
||||
telemetryForComps[otherKeyString].push(closestDatum);
|
||||
}
|
||||
});
|
||||
});
|
||||
return telemetryForComps;
|
||||
}
|
||||
|
||||
#removeTelemetryObject = (telemetryObjectIdentifier) => {
|
||||
const keyString = this.#openmct.objects.makeKeyString(telemetryObjectIdentifier);
|
||||
delete this.#telemetryObjects[keyString];
|
||||
this.#telemetryCollections[keyString]?.destroy();
|
||||
delete this.#telemetryCollections[keyString];
|
||||
// remove all parameters that reference this telemetry object
|
||||
this.deleteParameter(keyString);
|
||||
};
|
||||
|
||||
requestUnderlyingTelemetry() {
|
||||
const underlyingTelemetry = {};
|
||||
Object.keys(this.#telemetryCollections).forEach((collectionKey) => {
|
||||
const collection = this.#telemetryCollections[collectionKey];
|
||||
underlyingTelemetry[collectionKey] = collection.getAll();
|
||||
});
|
||||
return underlyingTelemetry;
|
||||
}
|
||||
|
||||
#getTelemetryProcessor(keyString) {
|
||||
if (this.#telemetryProcessors[keyString]) {
|
||||
return this.#telemetryProcessors[keyString];
|
||||
}
|
||||
|
||||
const telemetryProcessor = (newTelemetry) => {
|
||||
this.emit('underlyingTelemetryUpdated', { [keyString]: newTelemetry });
|
||||
};
|
||||
this.#telemetryProcessors[keyString] = telemetryProcessor;
|
||||
return telemetryProcessor;
|
||||
}
|
||||
|
||||
#telemetryProcessor = (newTelemetry, keyString) => {
|
||||
this.emit('underlyingTelemetryUpdated', { [keyString]: newTelemetry });
|
||||
};
|
||||
|
||||
clearData(telemetryLoadedPromise) {
|
||||
this.#loaded = false;
|
||||
this.#telemetryLoadedPromises.push(telemetryLoadedPromise);
|
||||
}
|
||||
|
||||
setOutputFormat(outputFormat) {
|
||||
this.#domainObject.configuration.comps.outputFormat = outputFormat;
|
||||
this.emit('outputFormatChanged', outputFormat);
|
||||
}
|
||||
|
||||
getOutputFormat() {
|
||||
return this.#domainObject.configuration.comps.outputFormat;
|
||||
}
|
||||
|
||||
getExpression() {
|
||||
return this.#domainObject.configuration.comps.expression;
|
||||
}
|
||||
|
||||
#addTelemetryObject = (telemetryObject) => {
|
||||
const keyString = this.#openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
this.#telemetryObjects[keyString] = telemetryObject;
|
||||
this.#telemetryCollections[keyString] = this.#openmct.telemetry.requestCollection(
|
||||
telemetryObject,
|
||||
this.#telemetryOptions
|
||||
);
|
||||
|
||||
// check to see if we have a corresponding parameter
|
||||
// if not, add one
|
||||
const parameterExists = this.#domainObject.configuration.comps.parameters.some(
|
||||
(parameter) => parameter.keyString === keyString
|
||||
);
|
||||
if (!parameterExists) {
|
||||
this.addParameter(telemetryObject);
|
||||
}
|
||||
};
|
||||
|
||||
static getCompsManager(domainObject, openmct, compsManagerPool) {
|
||||
const id = openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
if (!compsManagerPool[id]) {
|
||||
compsManagerPool[id] = new CompsManager(openmct, domainObject);
|
||||
}
|
||||
|
||||
return compsManagerPool[id];
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
import { evaluate } from 'mathjs';
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
onconnect = function (e) {
|
||||
const port = e.ports[0];
|
||||
|
||||
port.onmessage = function (event) {
|
||||
const { type, callbackID, telemetryForComps, expression, parameters, newTelemetry } =
|
||||
event.data;
|
||||
let responseType = 'unknown';
|
||||
let error = null;
|
||||
let result = [];
|
||||
try {
|
||||
if (type === 'calculateRequest') {
|
||||
responseType = 'calculationRequestResult';
|
||||
console.debug(`📫 Received new calculation request with callback ID ${callbackID}`);
|
||||
result = calculateRequest(telemetryForComps, parameters, expression);
|
||||
} else if (type === 'calculateSubscription') {
|
||||
responseType = 'calculationSubscriptionResult';
|
||||
result = calculateSubscription(telemetryForComps, newTelemetry, parameters, expression);
|
||||
} else if (type === 'init') {
|
||||
port.postMessage({ type: 'ready' });
|
||||
return;
|
||||
} else {
|
||||
throw new Error('Invalid message type');
|
||||
}
|
||||
} catch (errorInCalculation) {
|
||||
error = errorInCalculation;
|
||||
}
|
||||
console.debug(`📭 Sending response for callback ID ${callbackID}`, result);
|
||||
port.postMessage({ type: responseType, callbackID, result, error });
|
||||
};
|
||||
};
|
||||
|
||||
function getFullDataFrame(telemetryForComps, parameters) {
|
||||
const dataFrame = {};
|
||||
Object.keys(telemetryForComps)?.forEach((key) => {
|
||||
const parameter = parameters.find((p) => p.keyString === key);
|
||||
const dataSet = telemetryForComps[key];
|
||||
const telemetryMap = new Map(dataSet.map((item) => [item[parameter.timeKey], item]));
|
||||
dataFrame[key] = telemetryMap;
|
||||
});
|
||||
return dataFrame;
|
||||
}
|
||||
|
||||
function calculateSubscription(telemetryForComps, newTelemetry, parameters, expression) {
|
||||
const dataFrame = getFullDataFrame(telemetryForComps, parameters);
|
||||
const calculation = calculate(dataFrame, parameters, expression);
|
||||
const newTelemetryKey = Object.keys(newTelemetry)[0];
|
||||
const newTelemetrySize = newTelemetry[newTelemetryKey].length;
|
||||
let trimmedCalculation = calculation;
|
||||
if (calculation.length > newTelemetrySize) {
|
||||
trimmedCalculation = calculation.slice(calculation.length - newTelemetrySize);
|
||||
}
|
||||
return trimmedCalculation;
|
||||
}
|
||||
|
||||
function calculateRequest(telemetryForComps, parameters, expression) {
|
||||
const dataFrame = getFullDataFrame(telemetryForComps, parameters);
|
||||
return calculate(dataFrame, parameters, expression);
|
||||
}
|
||||
|
||||
function calculate(dataFrame, parameters, expression) {
|
||||
const sumResults = [];
|
||||
// ensure all parameter keyStrings have corresponding telemetry data
|
||||
if (!expression) {
|
||||
return sumResults;
|
||||
}
|
||||
// set up accumulated data structure
|
||||
const accumulatedData = {};
|
||||
parameters.forEach((parameter) => {
|
||||
if (parameter.accumulateValues) {
|
||||
accumulatedData[parameter.name] = [];
|
||||
}
|
||||
});
|
||||
|
||||
// take the first parameter keyString as the reference
|
||||
const referenceParameter = parameters[0];
|
||||
const otherParameters = parameters.slice(1);
|
||||
// iterate over the reference telemetry data
|
||||
const referenceTelemetry = dataFrame[referenceParameter.keyString];
|
||||
referenceTelemetry?.forEach((referenceTelemetryItem) => {
|
||||
let referenceValue = referenceTelemetryItem[referenceParameter.valueToUse];
|
||||
if (referenceParameter.accumulateValues) {
|
||||
accumulatedData[referenceParameter.name].push(referenceValue);
|
||||
referenceValue = accumulatedData[referenceParameter.name];
|
||||
}
|
||||
if (
|
||||
referenceParameter.accumulateValues &&
|
||||
referenceParameter.sampleSize &&
|
||||
referenceParameter.sampleSize > 0
|
||||
) {
|
||||
// enforce sample size by ensuring referenceValue has the latest n elements
|
||||
// if we don't have at least the sample size, skip this iteration
|
||||
if (!referenceValue.length || referenceValue.length < referenceParameter.sampleSize) {
|
||||
return;
|
||||
}
|
||||
referenceValue = referenceValue.slice(-referenceParameter.sampleSize);
|
||||
}
|
||||
|
||||
const scope = {
|
||||
[referenceParameter.name]: referenceValue
|
||||
};
|
||||
const referenceTime = referenceTelemetryItem[referenceParameter.timeKey];
|
||||
// iterate over the other parameters to set the scope
|
||||
let missingData = false;
|
||||
otherParameters.forEach((parameter) => {
|
||||
const otherDataFrame = dataFrame[parameter.keyString];
|
||||
const otherTelemetry = otherDataFrame.get(referenceTime);
|
||||
if (otherTelemetry === undefined || otherTelemetry === null) {
|
||||
missingData = true;
|
||||
return;
|
||||
}
|
||||
let otherValue = otherTelemetry[parameter.valueToUse];
|
||||
if (parameter.accumulateValues) {
|
||||
accumulatedData[parameter.name].push(referenceValue);
|
||||
otherValue = accumulatedData[referenceParameter.name];
|
||||
}
|
||||
scope[parameter.name] = otherValue;
|
||||
});
|
||||
if (missingData) {
|
||||
return;
|
||||
}
|
||||
const compsOutput = evaluate(expression, scope);
|
||||
sumResults.push({ [referenceParameter.timeKey]: referenceTime, compsOutput });
|
||||
});
|
||||
return sumResults;
|
||||
}
|
@ -1,80 +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 CompsManager from './CompsManager.js';
|
||||
|
||||
export default class CompsMetadataProvider {
|
||||
#openmct = null;
|
||||
#compsManagerPool = null;
|
||||
|
||||
constructor(openmct, compsManagerPool) {
|
||||
this.#openmct = openmct;
|
||||
this.#compsManagerPool = compsManagerPool;
|
||||
}
|
||||
|
||||
supportsMetadata(domainObject) {
|
||||
return domainObject.type === 'comps';
|
||||
}
|
||||
|
||||
getDefaultDomains(domainObject) {
|
||||
return this.#openmct.time.getAllTimeSystems().map(function (ts, i) {
|
||||
return {
|
||||
key: ts.key,
|
||||
name: ts.name,
|
||||
format: ts.timeFormat,
|
||||
hints: {
|
||||
domain: i
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getMetadata(domainObject) {
|
||||
const specificCompsManager = CompsManager.getCompsManager(
|
||||
domainObject,
|
||||
this.#openmct,
|
||||
this.#compsManagerPool
|
||||
);
|
||||
// if there are any parameters, grab the first one's timeMetaData
|
||||
const timeMetaData = specificCompsManager?.getParameters()[0]?.timeMetaData;
|
||||
const metaDataToReturn = {
|
||||
values: [
|
||||
{
|
||||
key: 'compsOutput',
|
||||
source: 'compsOutput',
|
||||
name: 'Output',
|
||||
derived: true,
|
||||
formatString: specificCompsManager.getOutputFormat(),
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
if (timeMetaData) {
|
||||
metaDataToReturn.values.push(timeMetaData);
|
||||
} else {
|
||||
const defaultDomains = this.getDefaultDomains(domainObject);
|
||||
metaDataToReturn.values.push(...defaultDomains);
|
||||
}
|
||||
return metaDataToReturn;
|
||||
}
|
||||
}
|
@ -1,184 +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 CompsManager from './CompsManager.js';
|
||||
|
||||
export default class CompsTelemetryProvider {
|
||||
#openmct = null;
|
||||
#sharedWorker = null;
|
||||
#compsManagerPool = null;
|
||||
#lastUniqueID = 1;
|
||||
#requestPromises = {};
|
||||
#subscriptionCallbacks = {};
|
||||
// id is random 4 digit number
|
||||
#id = Math.floor(Math.random() * 9000) + 1000;
|
||||
|
||||
constructor(openmct, compsManagerPool) {
|
||||
this.#openmct = openmct;
|
||||
this.#compsManagerPool = compsManagerPool;
|
||||
this.#openmct.on('start', this.#startSharedWorker.bind(this));
|
||||
}
|
||||
|
||||
isTelemetryObject(domainObject) {
|
||||
return domainObject.type === 'comps';
|
||||
}
|
||||
|
||||
supportsRequest(domainObject) {
|
||||
return domainObject.type === 'comps';
|
||||
}
|
||||
|
||||
supportsSubscribe(domainObject) {
|
||||
return domainObject.type === 'comps';
|
||||
}
|
||||
|
||||
#getCallbackID() {
|
||||
return this.#lastUniqueID++;
|
||||
}
|
||||
|
||||
request(domainObject, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const specificCompsManager = CompsManager.getCompsManager(
|
||||
domainObject,
|
||||
this.#openmct,
|
||||
this.#compsManagerPool
|
||||
);
|
||||
specificCompsManager.load(options).then(() => {
|
||||
const callbackID = this.#getCallbackID();
|
||||
const telemetryForComps = JSON.parse(
|
||||
JSON.stringify(specificCompsManager.requestUnderlyingTelemetry())
|
||||
);
|
||||
const expression = specificCompsManager.getExpression();
|
||||
const parameters = JSON.parse(JSON.stringify(specificCompsManager.getParameters()));
|
||||
if (!expression || !parameters) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
this.#requestPromises[callbackID] = { resolve, reject };
|
||||
const payload = {
|
||||
type: 'calculateRequest',
|
||||
telemetryForComps,
|
||||
expression,
|
||||
parameters,
|
||||
callbackID
|
||||
};
|
||||
console.debug(
|
||||
`📝 Requesting calculation for ${domainObject.name} with callback ID ${callbackID}:`,
|
||||
options,
|
||||
payload
|
||||
);
|
||||
this.#sharedWorker.port.postMessage(payload);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#computeOnNewTelemetry(specificCompsManager, callbackID, newTelemetry) {
|
||||
if (!specificCompsManager.isReady()) {
|
||||
return;
|
||||
}
|
||||
const expression = specificCompsManager.getExpression();
|
||||
const telemetryForComps = specificCompsManager.getTelemetryForComps(newTelemetry);
|
||||
const parameters = JSON.parse(JSON.stringify(specificCompsManager.getParameters()));
|
||||
if (!expression || !parameters) {
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
type: 'calculateSubscription',
|
||||
telemetryForComps,
|
||||
newTelemetry,
|
||||
expression,
|
||||
parameters,
|
||||
callbackID
|
||||
};
|
||||
this.#sharedWorker.port.postMessage(payload);
|
||||
}
|
||||
|
||||
subscribe(domainObject, callback) {
|
||||
const specificCompsManager = CompsManager.getCompsManager(
|
||||
domainObject,
|
||||
this.#openmct,
|
||||
this.#compsManagerPool
|
||||
);
|
||||
const callbackID = this.#getCallbackID();
|
||||
this.#subscriptionCallbacks[callbackID] = callback;
|
||||
const boundComputeOnNewTelemetry = this.#computeOnNewTelemetry.bind(
|
||||
this,
|
||||
specificCompsManager,
|
||||
callbackID
|
||||
);
|
||||
specificCompsManager.on('underlyingTelemetryUpdated', boundComputeOnNewTelemetry);
|
||||
const telemetryOptions = {
|
||||
strategy: 'latest',
|
||||
size: 1
|
||||
};
|
||||
specificCompsManager.load(telemetryOptions);
|
||||
console.debug(
|
||||
`📝 Starting subscription for ${domainObject.name} with callback ID ${callbackID}`
|
||||
);
|
||||
return () => {
|
||||
delete this.#subscriptionCallbacks[callbackID];
|
||||
specificCompsManager.stopListeningToUnderlyingTelemetry();
|
||||
specificCompsManager.off('underlyingTelemetryUpdated', boundComputeOnNewTelemetry);
|
||||
};
|
||||
}
|
||||
|
||||
#startSharedWorker() {
|
||||
if (this.#sharedWorker) {
|
||||
throw new Error('Shared worker already started');
|
||||
}
|
||||
const sharedWorkerURL = `${this.#openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}compsMathWorker.js`;
|
||||
|
||||
this.#sharedWorker = new SharedWorker(sharedWorkerURL, `Comps Math Worker`);
|
||||
this.#sharedWorker.port.onmessage = this.onSharedWorkerMessage.bind(this);
|
||||
this.#sharedWorker.port.onmessageerror = this.onSharedWorkerMessageError.bind(this);
|
||||
this.#sharedWorker.port.start();
|
||||
|
||||
this.#sharedWorker.port.postMessage({ type: 'init' });
|
||||
|
||||
this.#openmct.on('destroy', () => {
|
||||
this.#sharedWorker.port.close();
|
||||
});
|
||||
}
|
||||
|
||||
onSharedWorkerMessage(event) {
|
||||
const { type, result, callbackID, error } = event.data;
|
||||
if (
|
||||
type === 'calculationSubscriptionResult' &&
|
||||
this.#subscriptionCallbacks[callbackID] &&
|
||||
result.length
|
||||
) {
|
||||
this.#subscriptionCallbacks[callbackID](result);
|
||||
} else if (type === 'calculationRequestResult' && this.#requestPromises[callbackID]) {
|
||||
if (error) {
|
||||
console.error('📝 Error calculating request:', event.data);
|
||||
this.#requestPromises[callbackID].resolve([]);
|
||||
} else {
|
||||
console.debug(`🧮 Calculation request result for ${callbackID}:`, result);
|
||||
this.#requestPromises[callbackID].resolve(result);
|
||||
}
|
||||
delete this.#requestPromises[callbackID];
|
||||
}
|
||||
}
|
||||
|
||||
onSharedWorkerMessageError(event) {
|
||||
console.error('❌ Shared worker message error:', event);
|
||||
}
|
||||
}
|
@ -1,98 +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 mount from 'utils/mount';
|
||||
|
||||
import CompsView from './components/CompsView.vue';
|
||||
|
||||
const DEFAULT_VIEW_PRIORITY = 100;
|
||||
|
||||
export default class ConditionSetViewProvider {
|
||||
constructor(openmct, compsManagerPool) {
|
||||
this.openmct = openmct;
|
||||
this.name = 'Comps View';
|
||||
this.key = 'comps.view';
|
||||
this.cssClass = 'icon-derived-telemetry';
|
||||
this.compsManagerPool = compsManagerPool;
|
||||
}
|
||||
|
||||
canView(domainObject, objectPath) {
|
||||
return domainObject.type === 'comps' && this.openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
canEdit(domainObject, objectPath) {
|
||||
return domainObject.type === 'comps' && this.openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
view(domainObject, objectPath) {
|
||||
let _destroy = null;
|
||||
let component = null;
|
||||
|
||||
return {
|
||||
show: (container, isEditing) => {
|
||||
const { vNode, destroy } = mount(
|
||||
{
|
||||
el: container,
|
||||
components: {
|
||||
CompsView
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
domainObject,
|
||||
objectPath,
|
||||
compsManagerPool: this.compsManagerPool
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing
|
||||
};
|
||||
},
|
||||
template: '<CompsView :isEditing="isEditing"></CompsView>'
|
||||
},
|
||||
{
|
||||
app: this.openmct.app,
|
||||
element: container
|
||||
}
|
||||
);
|
||||
_destroy = destroy;
|
||||
component = vNode.componentInstance;
|
||||
},
|
||||
onEditModeChange: (isEditing) => {
|
||||
component.isEditing = isEditing;
|
||||
},
|
||||
destroy: () => {
|
||||
if (_destroy) {
|
||||
_destroy();
|
||||
}
|
||||
component = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
priority(domainObject) {
|
||||
if (domainObject.type === 'comps') {
|
||||
return Number.MAX_VALUE;
|
||||
} else {
|
||||
return DEFAULT_VIEW_PRIORITY;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,76 +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.
|
||||
-->
|
||||
<template>
|
||||
<div class="c-inspect-properties">
|
||||
<template v-if="isEditing">
|
||||
<ul class="c-inspect-properties__section">
|
||||
<li class="c-inspect-properties__row">
|
||||
<div class="c-inspect-properties__label" title="Output Format">
|
||||
<label for="OutputFormatControl">Output Format</label>
|
||||
</div>
|
||||
<div class="c-inspect-properties__value">
|
||||
<input
|
||||
id="OutputFormatControl"
|
||||
v-model="inputFormatValue"
|
||||
type="text"
|
||||
class="c-input--flex"
|
||||
@change="changeInputFormat()"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, onBeforeMount, onBeforeUnmount, ref } from 'vue';
|
||||
|
||||
import CompsManager from '../CompsManager';
|
||||
|
||||
const isEditing = ref(false);
|
||||
const inputFormatValue = ref('');
|
||||
|
||||
const openmct = inject('openmct');
|
||||
const domainObject = inject('domainObject');
|
||||
const compsManagerPool = inject('compsManagerPool');
|
||||
|
||||
onBeforeMount(() => {
|
||||
isEditing.value = openmct.editor.isEditing();
|
||||
openmct.editor.on('isEditing', toggleEdit);
|
||||
inputFormatValue.value = domainObject.configuration.comps.outputFormat;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
openmct.editor.off('isEditing', toggleEdit);
|
||||
});
|
||||
|
||||
function toggleEdit(passedIsEditing) {
|
||||
isEditing.value = passedIsEditing;
|
||||
}
|
||||
|
||||
function changeInputFormat() {
|
||||
openmct.objects.mutate(domainObject, `configuration.comps.outputFormat`, inputFormatValue.value);
|
||||
const compsManager = CompsManager.getCompsManager(domainObject, openmct, compsManagerPool);
|
||||
compsManager.setOutputFormat(inputFormatValue.value);
|
||||
}
|
||||
</script>
|
@ -1,358 +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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="c-comps" aria-label="Derived Telemetry">
|
||||
<section class="c-section c-comps-output">
|
||||
<div class="c-output-featured">
|
||||
<span class="c-output-featured__label">Current Output</span>
|
||||
<span class="c-output-featured__value" aria-label="Current Output Value">
|
||||
<template
|
||||
v-if="testDataApplied && currentTestOutput !== undefined && currentTestOutput !== null"
|
||||
>
|
||||
{{ currentTestOutput }}
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
!testDataApplied && currentCompOutput !== undefined && currentCompOutput !== null
|
||||
"
|
||||
>
|
||||
{{ currentCompOutput }}
|
||||
</template>
|
||||
<template v-else> --- </template>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
id="telemetryReferenceSection"
|
||||
class="c-comps__section c-comps__refs-and-controls"
|
||||
aria-describedby="telemetryReferences"
|
||||
>
|
||||
<div class="c-cs__header c-section__header">
|
||||
<div id="telemetryReferences" class="c-cs__header-label c-section__label">
|
||||
Telemetry References
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="['c-comps__refs-controls c-cdef__controls', { disabled: !parameters?.length }]">
|
||||
<label v-if="isEditing" class="c-toggle-switch">
|
||||
<input type="checkbox" :checked="testDataApplied" @change="toggleTestData" />
|
||||
<span class="c-toggle-switch__slider" aria-label="Apply Test Data"></span>
|
||||
<span class="c-toggle-switch__label">Apply Test Values</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="c-comps__refs">
|
||||
<div v-for="parameter in parameters" :key="parameter.keyString" class="c-comps__ref">
|
||||
<span class="c-test-datum__string">Reference</span>
|
||||
<input
|
||||
v-if="isEditing"
|
||||
v-model="parameter.name"
|
||||
:aria-label="`Reference Name Input for ${parameter.name}`"
|
||||
type="text"
|
||||
class="c-input--md"
|
||||
@change="updateParameters"
|
||||
/>
|
||||
<div v-else class="--em">{{ parameter.name }}</div>
|
||||
<span class="c-test-datum__string">=</span>
|
||||
<span
|
||||
class="c-comps__path-and-field"
|
||||
:aria-label="`Reference ${parameter.name} Object Path`"
|
||||
>
|
||||
<ObjectPathString
|
||||
:domain-object="compsManager.getTelemetryObjectForParameter(parameter.keyString)"
|
||||
:show-object-itself="true"
|
||||
class="--em"
|
||||
/>
|
||||
<!-- drop down to select value from telemetry -->
|
||||
<select v-if="isEditing" v-model="parameter.valueToUse" @change="updateParameters">
|
||||
<option
|
||||
v-for="parameterValueOption in compsManager.getMetaDataValuesForParameter(
|
||||
parameter.keyString
|
||||
)"
|
||||
:key="parameterValueOption.key"
|
||||
:value="parameterValueOption.key"
|
||||
>
|
||||
{{ parameterValueOption.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-else>{{ parameter.valueToUse }}</div>
|
||||
<div
|
||||
:class="[
|
||||
'c-comps__refs-controls c-cdef__controls',
|
||||
{ disabled: !parameters?.length }
|
||||
]"
|
||||
>
|
||||
<label v-if="isEditing" class="c-toggle-switch">
|
||||
<span class="c-toggle-switch__label">Accumulate Values</span>
|
||||
<input
|
||||
v-model="parameter.accumulateValues"
|
||||
type="checkbox"
|
||||
@change="updateAccumulateValues(parameter)"
|
||||
/>
|
||||
<span
|
||||
class="c-toggle-switch__slider"
|
||||
aria-label="Toggle Parameter Accumulation"
|
||||
></span>
|
||||
</label>
|
||||
|
||||
<span v-if="isEditing && parameter.accumulateValues" class="c-test-datum__string"
|
||||
>Sample Size</span
|
||||
>
|
||||
<input
|
||||
v-if="isEditing && parameter.accumulateValues"
|
||||
v-model="parameter.sampleSize"
|
||||
:aria-label="`Sample Size for ${parameter.name}`"
|
||||
type="number"
|
||||
class="c-input--md"
|
||||
@change="updateParameters"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span v-if="isEditing" class="c-test-datum__string">Test value</span>
|
||||
<input
|
||||
v-if="isEditing"
|
||||
v-model="parameter.testValue"
|
||||
:aria-label="`Reference Test Value for ${parameter.name}`"
|
||||
type="text"
|
||||
class="c-input--md"
|
||||
@change="updateTestValue(parameter)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="expressionSection" class="c-comps__section c-comps__expression">
|
||||
<div class="c-cs__header c-section__header">
|
||||
<div class="c-cs__header-label c-section__label">Expression</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!parameters?.length && isEditing" class="hint">
|
||||
Drag in telemetry to add references for an expression.
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
v-if="parameters?.length && isEditing"
|
||||
v-model="expression"
|
||||
class="c-comps__expression-value"
|
||||
placeholder="Enter an expression"
|
||||
@change="updateExpression"
|
||||
></textarea>
|
||||
<div v-else>
|
||||
<div class="c-comps__expression-value" aria-label="Expression">
|
||||
{{ expression }}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="expression && expressionOutput"
|
||||
class="icon-alert-triangle c-comps__expression-msg --bad"
|
||||
>
|
||||
Invalid: {{ expressionOutput }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="expression && !expressionOutput && isEditing"
|
||||
class="c-comps__expression-msg --good"
|
||||
>
|
||||
Expression valid
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { evaluate } from 'mathjs';
|
||||
import { inject, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
import ObjectPathString from '../../../ui/components/ObjectPathString.vue';
|
||||
import CompsManager from '../CompsManager';
|
||||
|
||||
const openmct = inject('openmct');
|
||||
const domainObject = inject('domainObject');
|
||||
const compsManagerPool = inject('compsManagerPool');
|
||||
const compsManager = CompsManager.getCompsManager(domainObject, openmct, compsManagerPool);
|
||||
const currentCompOutput = ref(null);
|
||||
const currentTestOutput = ref(null);
|
||||
const testDataApplied = ref(false);
|
||||
const parameters = ref(null);
|
||||
const expression = ref(null);
|
||||
const expressionOutput = ref(null);
|
||||
const outputFormat = ref(null);
|
||||
|
||||
let outputTelemetryCollection;
|
||||
|
||||
const props = defineProps({
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const telemetryOptions = {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
};
|
||||
outputTelemetryCollection = openmct.telemetry.requestCollection(domainObject, telemetryOptions);
|
||||
outputTelemetryCollection.on('add', telemetryProcessor);
|
||||
outputTelemetryCollection.on('clear', clearData);
|
||||
compsManager.on('parameterAdded', reloadParameters);
|
||||
compsManager.on('parameterRemoved', reloadParameters);
|
||||
compsManager.on('outputFormatChanged', updateOutputFormat);
|
||||
await outputTelemetryCollection.load(telemetryOptions); // will implicitly load compsManager
|
||||
parameters.value = compsManager.getParameters();
|
||||
expression.value = compsManager.getExpression();
|
||||
outputFormat.value = compsManager.getOutputFormat();
|
||||
applyTestData();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
outputTelemetryCollection.off('add', telemetryProcessor);
|
||||
outputTelemetryCollection.off('clear', clearData);
|
||||
compsManager.off('parameterAdded', reloadParameters);
|
||||
compsManager.off('parameterRemoved', reloadParameters);
|
||||
compsManager.off('outputFormatChanged', updateOutputFormat);
|
||||
outputTelemetryCollection.destroy();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.isEditing,
|
||||
(editMode) => {
|
||||
if (!editMode) {
|
||||
testDataApplied.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function updateOutputFormat() {
|
||||
outputFormat.value = compsManager.getOutputFormat();
|
||||
// delete the metadata cache so that the new output format is used
|
||||
openmct.telemetry.removeMetadataFromCache(domainObject);
|
||||
}
|
||||
|
||||
function reloadParameters(passedDomainObject) {
|
||||
// Because this is triggered by a composition change, we have
|
||||
// to defer mutation of our domain object, otherwise we might
|
||||
// mutate an outdated version of the domain object.
|
||||
setTimeout(function () {
|
||||
domainObject.configuration.comps.parameters = passedDomainObject.configuration.comps.parameters;
|
||||
parameters.value = domainObject.configuration.comps.parameters;
|
||||
openmct.objects.mutate(domainObject, `configuration.comps.parameters`, parameters.value);
|
||||
compsManager.setDomainObject(domainObject);
|
||||
applyTestData();
|
||||
});
|
||||
}
|
||||
|
||||
function updateParameters() {
|
||||
openmct.objects.mutate(domainObject, `configuration.comps.parameters`, parameters.value);
|
||||
compsManager.setDomainObject(domainObject);
|
||||
applyTestData();
|
||||
}
|
||||
|
||||
function updateAccumulateValues(parameter) {
|
||||
if (parameter.accumulateValues) {
|
||||
parameter.testValue = [''];
|
||||
} else {
|
||||
parameter.testValue = '';
|
||||
}
|
||||
updateParameters();
|
||||
}
|
||||
|
||||
function updateTestValue(parameter) {
|
||||
if (parameter.accumulateValues && parameter.testValue === '') {
|
||||
parameter.testValue = [];
|
||||
}
|
||||
updateParameters();
|
||||
}
|
||||
|
||||
function toggleTestData() {
|
||||
testDataApplied.value = !testDataApplied.value;
|
||||
if (testDataApplied.value) {
|
||||
applyTestData();
|
||||
} else {
|
||||
clearData();
|
||||
}
|
||||
}
|
||||
|
||||
function updateExpression() {
|
||||
openmct.objects.mutate(domainObject, `configuration.comps.expression`, expression.value);
|
||||
compsManager.setDomainObject(domainObject);
|
||||
applyTestData();
|
||||
}
|
||||
|
||||
function getValueFormatter() {
|
||||
const metaData = openmct.telemetry.getMetadata(domainObject);
|
||||
const outputMetaDatum = metaData.values().find((metaDatum) => metaDatum.key === 'compsOutput');
|
||||
return openmct.telemetry.getValueFormatter(outputMetaDatum);
|
||||
}
|
||||
|
||||
function applyTestData() {
|
||||
if (!expression.value || !parameters.value) {
|
||||
return;
|
||||
}
|
||||
const scope = parameters.value.reduce((acc, parameter) => {
|
||||
// try to parse the test value as JSON
|
||||
try {
|
||||
const parsedValue = JSON.parse(parameter.testValue);
|
||||
acc[parameter.name] = parsedValue;
|
||||
} catch (error) {
|
||||
acc[parameter.name] = parameter.testValue;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// see which parameters are misconfigured as non-arrays
|
||||
const misconfiguredParameterNames = parameters.value
|
||||
.filter((parameter) => {
|
||||
return parameter.accumulateValues && !Array.isArray(scope[parameter.name]);
|
||||
})
|
||||
.map((parameter) => parameter.name);
|
||||
if (misconfiguredParameterNames.length) {
|
||||
const misconfiguredParameterNamesString = misconfiguredParameterNames.join(', ');
|
||||
currentTestOutput.value = null;
|
||||
expressionOutput.value = `Reference "${misconfiguredParameterNamesString}" set to accumulating, but test values aren't arrays.`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const testOutput = evaluate(expression.value, scope);
|
||||
const formattedData = getValueFormatter().format(testOutput);
|
||||
currentTestOutput.value = formattedData;
|
||||
expressionOutput.value = null;
|
||||
} catch (error) {
|
||||
currentTestOutput.value = null;
|
||||
expressionOutput.value = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function telemetryProcessor(data) {
|
||||
if (testDataApplied.value) {
|
||||
return;
|
||||
}
|
||||
// new data will come in as array, so just take the last element
|
||||
const currentOutput = data[data.length - 1]?.compsOutput;
|
||||
const formattedOutput = getValueFormatter().format(currentOutput);
|
||||
currentCompOutput.value = formattedOutput;
|
||||
}
|
||||
|
||||
function clearData() {
|
||||
currentCompOutput.value = null;
|
||||
}
|
||||
</script>
|
@ -1,112 +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.
|
||||
*****************************************************************************/
|
||||
@mixin expressionMsg($fg, $bg) {
|
||||
$op: 0.4;
|
||||
color: rgba($fg, $op * 1.5);
|
||||
background: rgba($bg, $op);
|
||||
}
|
||||
|
||||
.c-comps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $interiorMarginLg;
|
||||
|
||||
.is-editing & {
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
|
||||
&__output {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: $interiorMargin;
|
||||
|
||||
&-label {
|
||||
flex: 0 0 auto;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&-value {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__section,
|
||||
&__refs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&__ref {
|
||||
@include discreteItem();
|
||||
display: grid;
|
||||
gap: $interiorMargin;
|
||||
grid-template-columns: max-content max-content min-content 1fr;
|
||||
padding: $interiorMargin;
|
||||
line-height: 170%; // Aligns text with controls like selects
|
||||
}
|
||||
|
||||
&__path-and-field {
|
||||
align-items: start;
|
||||
display: flex;
|
||||
gap: $interiorMargin;
|
||||
}
|
||||
|
||||
&__expression {
|
||||
*[class*=value] {
|
||||
font-family: monospace;
|
||||
//font-size: 1.1em;
|
||||
resize: vertical; // Only applies to textarea
|
||||
}
|
||||
div[class*=value] {
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
&__expression-msg {
|
||||
@include expressionMsg($colorOkFg, $colorOk);
|
||||
border-radius: $basicCr;
|
||||
display: flex; // Creates hanging indent from :before icon
|
||||
padding: $interiorMarginSm $interiorMarginLg $interiorMarginSm $interiorMargin;
|
||||
//text-wrap: normal;
|
||||
max-width: max-content;
|
||||
|
||||
&:before {
|
||||
content: $glyph-icon-check;
|
||||
font-family: symbolsfont;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&.--bad {
|
||||
@include expressionMsg($colorErrorFg, $colorError);
|
||||
|
||||
&:before {
|
||||
content: $glyph-icon-alert-triangle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.--em {
|
||||
color: $colorBodyFgEm;
|
||||
//font-weight: bold;
|
||||
}
|
||||
}
|
@ -1,60 +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 CompsInspectorViewProvider from './CompsInspectorViewProvider.js';
|
||||
import CompsMetadataProvider from './CompsMetadataProvider.js';
|
||||
import CompsTelemetryProvider from './CompsTelemetryProvider.js';
|
||||
import CompsViewProvider from './CompsViewProvider.js';
|
||||
|
||||
export default function CompsPlugin() {
|
||||
const compsManagerPool = {};
|
||||
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('comps', {
|
||||
name: 'Derived Telemetry',
|
||||
key: 'comps',
|
||||
description:
|
||||
'Add one or more telemetry end points, apply a mathematical operation to them, and output the result as new telemetry.',
|
||||
creatable: true,
|
||||
cssClass: 'icon-derived-telemetry',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.configuration = {
|
||||
comps: {
|
||||
expression: '',
|
||||
parameters: []
|
||||
}
|
||||
};
|
||||
domainObject.composition = [];
|
||||
domainObject.telemetry = {};
|
||||
}
|
||||
});
|
||||
openmct.composition.addPolicy((parent, child) => {
|
||||
if (parent.type === 'comps' && !openmct.telemetry.isTelemetryObject(child)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
openmct.telemetry.addProvider(new CompsMetadataProvider(openmct, compsManagerPool));
|
||||
openmct.telemetry.addProvider(new CompsTelemetryProvider(openmct, compsManagerPool));
|
||||
openmct.objectViews.addProvider(new CompsViewProvider(openmct, compsManagerPool));
|
||||
openmct.inspectorViews.addProvider(new CompsInspectorViewProvider(openmct, compsManagerPool));
|
||||
};
|
||||
}
|
@ -44,48 +44,57 @@ import { getLatestTimestamp } from './utils/time.js';
|
||||
* }
|
||||
*/
|
||||
export default class Condition extends EventEmitter {
|
||||
#definition;
|
||||
/**
|
||||
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
|
||||
* @constructor
|
||||
* @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
|
||||
* @param definition: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
|
||||
* @param openmct
|
||||
* @param conditionManager
|
||||
*/
|
||||
constructor(conditionConfiguration, openmct, conditionManager) {
|
||||
constructor(definition, openmct, conditionManager) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.conditionManager = conditionManager;
|
||||
this.id = conditionConfiguration.id;
|
||||
this.criteria = [];
|
||||
this.result = undefined;
|
||||
this.timeSystems = this.openmct.time.getAllTimeSystems();
|
||||
if (conditionConfiguration.configuration.criteria) {
|
||||
this.createCriteria(conditionConfiguration.configuration.criteria);
|
||||
this.#definition = definition;
|
||||
|
||||
if (definition.configuration.criteria) {
|
||||
this.createCriteria(definition.configuration.criteria);
|
||||
}
|
||||
|
||||
this.trigger = conditionConfiguration.configuration.trigger;
|
||||
this.trigger = definition.configuration.trigger;
|
||||
this.summary = '';
|
||||
this.handleCriterionUpdated = this.handleCriterionUpdated.bind(this);
|
||||
this.handleOldTelemetryCriterion = this.handleOldTelemetryCriterion.bind(this);
|
||||
this.handleTelemetryStaleness = this.handleTelemetryStaleness.bind(this);
|
||||
}
|
||||
get id() {
|
||||
return this.#definition.id;
|
||||
}
|
||||
get configuration() {
|
||||
return this.#definition.configuration;
|
||||
}
|
||||
|
||||
updateResult(datum) {
|
||||
if (!datum || !datum.id) {
|
||||
updateResult(latestDataTable, telemetryIdThatChanged) {
|
||||
if (!latestDataTable) {
|
||||
console.log('no data received');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate
|
||||
if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) {
|
||||
if (this.hasNoTelemetry() || this.isTelemetryUsed(telemetryIdThatChanged)) {
|
||||
const currentTimeSystemKey = this.openmct.time.getTimeSystem().key;
|
||||
this.criteria.forEach((criterion) => {
|
||||
if (this.isAnyOrAllTelemetry(criterion)) {
|
||||
criterion.updateResult(datum, this.conditionManager.telemetryObjects);
|
||||
criterion.updateResult(latestDataTable, this.conditionManager.telemetryObjects);
|
||||
} else {
|
||||
if (criterion.usesTelemetry(datum.id)) {
|
||||
criterion.updateResult(datum);
|
||||
const relevantDatum = latestDataTable.get(criterion.telemetryObjectIdAsString);
|
||||
if (criterion.shouldUpdateResult(relevantDatum, currentTimeSystemKey)) {
|
||||
criterion.updateResult(relevantDatum, currentTimeSystemKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -102,9 +111,11 @@ export default class Condition extends EventEmitter {
|
||||
}
|
||||
|
||||
hasNoTelemetry() {
|
||||
return this.criteria.every((criterion) => {
|
||||
return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === '';
|
||||
const usesSomeTelemetry = this.criteria.some((criterion) => {
|
||||
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetry !== '';
|
||||
});
|
||||
|
||||
return !usesSomeTelemetry;
|
||||
}
|
||||
|
||||
isTelemetryUsed(id) {
|
||||
@ -182,7 +193,7 @@ export default class Condition extends EventEmitter {
|
||||
findCriterion(id) {
|
||||
let criterion;
|
||||
|
||||
for (let i = 0, ii = this.criteria.length; i < ii; i++) {
|
||||
for (let i = 0; i < this.criteria.length; i++) {
|
||||
if (this.criteria[i].id === id) {
|
||||
criterion = {
|
||||
item: this.criteria[i],
|
||||
@ -247,7 +258,7 @@ export default class Condition extends EventEmitter {
|
||||
this.timeSystems,
|
||||
this.openmct.time.getTimeSystem()
|
||||
);
|
||||
this.conditionManager.updateCurrentCondition(latestTimestamp);
|
||||
this.conditionManager.updateCurrentCondition(latestTimestamp, this);
|
||||
}
|
||||
|
||||
handleTelemetryStaleness() {
|
||||
|
@ -27,6 +27,12 @@ import Condition from './Condition.js';
|
||||
import { getLatestTimestamp } from './utils/time.js';
|
||||
|
||||
export default class ConditionManager extends EventEmitter {
|
||||
#latestDataTable = new Map();
|
||||
|
||||
/**
|
||||
* @param {import('openmct.js').DomainObject} conditionSetDomainObject
|
||||
* @param {import('openmct.js').OpenMCT} openmct
|
||||
*/
|
||||
constructor(conditionSetDomainObject, openmct) {
|
||||
super();
|
||||
this.openmct = openmct;
|
||||
@ -304,22 +310,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.persistConditions();
|
||||
}
|
||||
|
||||
getCurrentCondition() {
|
||||
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
||||
let currentCondition = conditionCollection[conditionCollection.length - 1];
|
||||
|
||||
for (let i = 0; i < conditionCollection.length - 1; i++) {
|
||||
const condition = this.findConditionById(conditionCollection[i].id);
|
||||
if (condition.result) {
|
||||
//first condition to be true wins
|
||||
currentCondition = conditionCollection[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return currentCondition;
|
||||
}
|
||||
|
||||
getCurrentConditionLAD(conditionResults) {
|
||||
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
||||
let currentCondition = conditionCollection[conditionCollection.length - 1];
|
||||
@ -410,26 +400,34 @@ export default class ConditionManager extends EventEmitter {
|
||||
|
||||
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
|
||||
const timeSystemKey = this.openmct.time.getTimeSystem().key;
|
||||
let timestamp = {};
|
||||
const currentTimestamp = normalizedDatum[timeSystemKey];
|
||||
const timestamp = {};
|
||||
|
||||
timestamp[timeSystemKey] = currentTimestamp;
|
||||
this.#latestDataTable.set(normalizedDatum.id, normalizedDatum);
|
||||
|
||||
if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {
|
||||
this.updateConditionResults(normalizedDatum);
|
||||
this.updateCurrentCondition(timestamp);
|
||||
const matchingCondition = this.updateConditionResults(normalizedDatum.id);
|
||||
this.updateCurrentCondition(timestamp, matchingCondition);
|
||||
}
|
||||
}
|
||||
|
||||
updateConditionResults(normalizedDatum) {
|
||||
updateConditionResults(keyStringForUpdatedTelemetryObject) {
|
||||
//We want to stop when the first condition evaluates to true.
|
||||
this.conditions.some((condition) => {
|
||||
condition.updateResult(normalizedDatum);
|
||||
const matchingCondition = this.conditions.find((condition) => {
|
||||
condition.updateResult(this.#latestDataTable, keyStringForUpdatedTelemetryObject);
|
||||
|
||||
return condition.result === true;
|
||||
});
|
||||
|
||||
return matchingCondition;
|
||||
}
|
||||
|
||||
updateCurrentCondition(timestamp) {
|
||||
const currentCondition = this.getCurrentCondition();
|
||||
updateCurrentCondition(timestamp, matchingCondition) {
|
||||
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
||||
const defaultCondition = conditionCollection[conditionCollection.length - 1];
|
||||
|
||||
const currentCondition = matchingCondition || defaultCondition;
|
||||
|
||||
this.emit(
|
||||
'conditionSetResultUpdated',
|
||||
@ -444,11 +442,13 @@ export default class ConditionManager extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
getTestData(metadatum) {
|
||||
getTestData(metadatum, identifier) {
|
||||
let data = undefined;
|
||||
if (this.testData.applied) {
|
||||
const found = this.testData.conditionTestInputs.find(
|
||||
(testInput) => testInput.metadata === metadatum.source
|
||||
(testInput) =>
|
||||
testInput.metadata === metadatum.source &&
|
||||
this.openmct.objects.areIdsEqual(testInput.telemetry, identifier)
|
||||
);
|
||||
if (found) {
|
||||
data = found.value;
|
||||
@ -463,7 +463,7 @@ export default class ConditionManager extends EventEmitter {
|
||||
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
|
||||
|
||||
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
|
||||
const testValue = this.getTestData(metadatum);
|
||||
const testValue = this.getTestData(metadatum, endpoint.identifier);
|
||||
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
|
||||
datum[metadatum.key] =
|
||||
testValue !== undefined
|
||||
@ -480,7 +480,7 @@ export default class ConditionManager extends EventEmitter {
|
||||
|
||||
updateTestData(testData) {
|
||||
if (!_.isEqual(testData, this.testData)) {
|
||||
this.testData = testData;
|
||||
this.testData = JSON.parse(JSON.stringify(testData));
|
||||
this.openmct.objects.mutate(
|
||||
this.conditionSetDomainObject,
|
||||
'configuration.conditionTestData',
|
||||
|
@ -53,6 +53,7 @@ describe('The condition', function () {
|
||||
valueMetadatas: [
|
||||
{
|
||||
key: 'some-key',
|
||||
source: 'some-key',
|
||||
name: 'Some attribute',
|
||||
hints: {
|
||||
range: 2
|
||||
@ -60,6 +61,7 @@ describe('The condition', function () {
|
||||
},
|
||||
{
|
||||
key: 'utc',
|
||||
source: 'utc',
|
||||
name: 'Time',
|
||||
format: 'utc',
|
||||
hints: {
|
||||
@ -88,17 +90,32 @@ describe('The condition', function () {
|
||||
openmct.telemetry = jasmine.createSpyObj('telemetry', [
|
||||
'isTelemetryObject',
|
||||
'subscribe',
|
||||
'getMetadata'
|
||||
'getMetadata',
|
||||
'getValueFormatter'
|
||||
]);
|
||||
openmct.telemetry.isTelemetryObject.and.returnValue(true);
|
||||
openmct.telemetry.subscribe.and.returnValue(function () {});
|
||||
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
|
||||
openmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
|
||||
return {
|
||||
parse(input) {
|
||||
return input;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
mockTimeSystems = {
|
||||
key: 'utc'
|
||||
};
|
||||
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems', 'on', 'off']);
|
||||
openmct.time = jasmine.createSpyObj('time', [
|
||||
'getTimeSystem',
|
||||
'getAllTimeSystems',
|
||||
'on',
|
||||
'off'
|
||||
]);
|
||||
openmct.time.getTimeSystem.and.returnValue({ key: 'utc' });
|
||||
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
|
||||
//openmct.time.getTimeSystem.and.returnValue();
|
||||
openmct.time.on.and.returnValue(() => {});
|
||||
openmct.time.off.and.returnValue(() => {});
|
||||
|
||||
@ -113,7 +130,7 @@ describe('The condition', function () {
|
||||
id: '1234-5678-9999-0000',
|
||||
operation: 'equalTo',
|
||||
input: ['0'],
|
||||
metadata: 'value',
|
||||
metadata: 'testSource',
|
||||
telemetry: testTelemetryObject.identifier
|
||||
}
|
||||
]
|
||||
@ -156,37 +173,24 @@ describe('The condition', function () {
|
||||
expect(conditionObj.criteria.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('gets the result of a condition when new telemetry data is received', function () {
|
||||
conditionObj.updateResult({
|
||||
value: '0',
|
||||
utc: 'Hi',
|
||||
id: testTelemetryObject.identifier.key
|
||||
});
|
||||
expect(conditionObj.result).toBeTrue();
|
||||
});
|
||||
|
||||
it('gets the result of a condition when new telemetry data is received', function () {
|
||||
conditionObj.updateResult({
|
||||
value: '1',
|
||||
utc: 'Hi',
|
||||
id: testTelemetryObject.identifier.key
|
||||
});
|
||||
expect(conditionObj.result).toBeFalse();
|
||||
});
|
||||
|
||||
it('keeps the old result new telemetry data is not used by it', function () {
|
||||
conditionObj.updateResult({
|
||||
const latestDataTable = new Map();
|
||||
latestDataTable.set(testTelemetryObject.identifier.key, {
|
||||
value: '0',
|
||||
utc: 'Hi',
|
||||
id: testTelemetryObject.identifier.key
|
||||
});
|
||||
conditionObj.updateResult(latestDataTable, testTelemetryObject.identifier.key);
|
||||
|
||||
expect(conditionObj.result).toBeTrue();
|
||||
|
||||
conditionObj.updateResult({
|
||||
latestDataTable.set('1234', {
|
||||
value: '1',
|
||||
utc: 'Hi',
|
||||
id: '1234'
|
||||
});
|
||||
|
||||
conditionObj.updateResult(latestDataTable, '1234');
|
||||
expect(conditionObj.result).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
@ -24,7 +24,7 @@
|
||||
<div
|
||||
class="c-condition-h"
|
||||
:class="{ 'is-drag-target': draggingOver }"
|
||||
aria-label="Condition Set Condition"
|
||||
:aria-label="conditionSetLabel"
|
||||
@dragover.prevent
|
||||
@drop.prevent="dropCondition($event, conditionIndex)"
|
||||
@dragenter="dragEnter($event, conditionIndex)"
|
||||
@ -53,7 +53,9 @@
|
||||
@click="expanded = !expanded"
|
||||
></span>
|
||||
|
||||
<span class="c-condition__name">{{ condition.configuration.name }}</span>
|
||||
<span class="c-condition__name" aria-label="Condition Name Label">{{
|
||||
condition.configuration.name
|
||||
}}</span>
|
||||
<span class="c-condition__summary">
|
||||
<template v-if="!condition.isDefault && !canEvaluateCriteria"> Define criteria </template>
|
||||
<span v-else>
|
||||
@ -160,8 +162,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="c-cdef__separator c-row-separator"></div>
|
||||
<div class="c-cdef__controls" :disabled="!telemetry.length">
|
||||
<div class="c-cdef__controls">
|
||||
<button
|
||||
:disabled="!telemetry.length"
|
||||
:aria-label="`Add Criteria - ${!telemetry.length ? 'Disabled' : 'Enabled'}`"
|
||||
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
|
||||
@click="addCriteria"
|
||||
>
|
||||
@ -257,6 +261,17 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
conditionSetLabel() {
|
||||
let label;
|
||||
|
||||
if (this.condition.id === this.currentConditionId) {
|
||||
label = 'Active Condition Set Condition';
|
||||
} else {
|
||||
label = 'Condition Set Condition';
|
||||
}
|
||||
|
||||
return label;
|
||||
},
|
||||
triggers() {
|
||||
const keys = Object.keys(TRIGGER);
|
||||
const triggerOptions = [];
|
||||
|
@ -23,9 +23,9 @@
|
||||
<template>
|
||||
<div class="c-cs" :class="{ 'is-stale': isStale }" aria-label="Condition Set">
|
||||
<section class="c-cs__current-output c-section">
|
||||
<div class="c-output-featured">
|
||||
<span class="c-output-featured__label">Current Output</span>
|
||||
<span class="c-output-featured__value" aria-label="Current Output Value">
|
||||
<div class="c-cs__content c-cs__current-output-value">
|
||||
<span class="c-cs__current-output-value__label">Current Output</span>
|
||||
<span class="c-cs__current-output-value__value" aria-label="Current Output Value">
|
||||
<template v-if="currentConditionOutput">
|
||||
{{ currentConditionOutput }}
|
||||
</template>
|
||||
|
@ -114,7 +114,7 @@
|
||||
class="c-button c-button--major icon-plus labeled"
|
||||
@click="addTestInput"
|
||||
>
|
||||
<span class="c-cs-button__label">Add Test Datum</span>
|
||||
<span class="c-cs-button__label" aria-label="Add Test Datum">Add Test Datum</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -51,7 +51,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
gap: $interiorMargin;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@ -90,24 +89,23 @@
|
||||
&__conditions {
|
||||
flex: 1 1 auto;
|
||||
|
||||
//> * + * {
|
||||
// margin-top: $interiorMarginSm;
|
||||
//}
|
||||
> * + * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $interiorMarginSm;
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
//+ * {
|
||||
// margin-top: $interiorMarginSm;
|
||||
//}
|
||||
+ * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
.c-button {
|
||||
@ -123,7 +121,6 @@
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $interiorMargin;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user