Compare commits

..

19 Commits

Author SHA1 Message Date
Andrew Henry
4cc0a9e402 Support binary websockets 2024-01-26 10:35:34 -08:00
Andrew Henry
12b921a006 Clean up and reconnect on websocket close 2024-01-24 08:24:26 -08:00
Andrew Henry
34b36d544a Merge branch 'master' into subscriptions-support-array-callbacks 2024-01-24 07:58:14 -08:00
Andrew Henry
e135498401 Addressed review comments 2024-01-24 07:54:10 -08:00
Andrew Henry
c4a3ace027 Fixed broken tests 2024-01-22 18:35:15 -08:00
Andrew Henry
ad02ba7ffe Added more documentation 2024-01-22 07:20:13 -08:00
Andrew Henry
0bdd5efcba Renamed classes 2024-01-20 19:18:15 -08:00
Andrew Henry
2e3dc4da9a renamed class. changed throttling strategy to be driven by the main thread 2024-01-20 17:54:33 -08:00
Andrew Henry
61edd0f810 Adding docs 2024-01-19 15:44:27 -08:00
Andrew Henry
4c86b66624 Merge branch 'master' into subscriptions-support-array-callbacks 2024-01-19 13:17:21 -08:00
Andrew Henry
f079c3a3b9 Renamed BatchingWebSocketProvider to BatchingWebSocket 2024-01-19 13:15:30 -08:00
Andrew Henry
947810b5d7 Added copyright statement 2024-01-18 17:43:10 -08:00
Andrew Henry
c28ced5c29 Don't hide original error 2024-01-18 08:53:07 -08:00
Andrew Henry
e530fd8e8b Merge branch 'master' into subscriptions-support-array-callbacks 2024-01-17 12:41:39 -08:00
Andrew Henry
74c1cdf468 Default to latest strategy 2024-01-16 15:41:03 -08:00
Andrew Henry
0061d162e1 Support batch size based throttling 2024-01-16 14:59:23 -08:00
Andrew Henry
2f2af0bac5 Added configurable batch size and throttling rate 2024-01-12 17:00:03 -08:00
Andrew Henry
a87ffee264 Added batching worker 2024-01-11 09:52:26 -08:00
Andrew Henry
69b2f05de2 Support subscription batching from API, Tables, and Plots 2024-01-09 15:26:25 -08:00
162 changed files with 1291 additions and 5416 deletions

View File

@@ -5,20 +5,20 @@ executors:
- image: mcr.microsoft.com/playwright:v1.39.0-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
PERCY_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
ubuntu:
machine:
image: ubuntu-2204:current
docker_layer_caching: true
parameters:
BUST_CACHE:
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
description: 'Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!'
default: false
type: boolean
commands:
build_and_install:
description: "All steps used to build and install. Will use cache if found"
description: 'All steps used to build and install. Will use cache if found'
parameters:
node-version:
type: string
@@ -30,7 +30,7 @@ commands:
node-version: << parameters.node-version >>
- run: npm install --no-audit --progress=false
restore_cache_cmd:
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
description: 'Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache'
parameters:
node-version:
type: string
@@ -42,7 +42,7 @@ commands:
- restore_cache:
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
save_cache_cmd:
description: "Custom command for saving cache."
description: 'Custom command for saving cache.'
parameters:
node-version:
type: string
@@ -53,7 +53,7 @@ commands:
- ~/.npm
- node_modules
generate_and_store_version_and_filesystem_artifacts:
description: "Track important packages and files"
description: 'Track important packages and files'
steps:
- run: |
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
@@ -64,7 +64,7 @@ commands:
- store_artifacts:
path: /tmp/artifacts/
generate_e2e_code_cov_report:
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
description: 'Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test'
parameters:
suite:
type: string
@@ -105,11 +105,7 @@ jobs:
node-version: <<parameters.node-version>>
- browser-tools/install-chrome:
replace-existing: false
- run:
command: |
mkdir -p dist/reports/tests/
TESTFILES=$(circleci tests glob "src/**/*Spec.js")
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
- run: npm run test
- run: npm run cov:unit:publish
- save_cache_cmd:
node-version: <<parameters.node-version>>
@@ -127,20 +123,16 @@ jobs:
suite: #stable or full
type: string
executor: pw-focal-development
parallelism: 7
parallelism: 6
steps:
- build_and_install:
node-version: lts/hydrogen
- when: #Only install chrome-beta when running the 'full' suite to save $$$
condition:
equal: ["full", <<parameters.suite>>]
equal: ['full', <<parameters.suite>>]
steps:
- run: npx playwright install chrome-beta
- run:
command: |
mkdir test-results
TESTFILES=$(circleci tests glob "e2e/**/*.spec.js")
echo "$TESTFILES" | circleci tests run --command="xargs npm run test:e2e:<<parameters.suite>>" --verbose --split-by=timings
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
- when:
condition:
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
@@ -247,7 +239,6 @@ jobs:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
@@ -260,6 +251,8 @@ workflows:
- e2e-test:
name: e2e-stable
suite: stable
- mem-test
- perf-test
- visual-a11y-tests:
name: visual-test-ci
suite: ci
@@ -285,7 +278,7 @@ workflows:
- e2e-couchdb
triggers:
- schedule:
cron: "0 0 * * *"
cron: '0 0 * * *'
filters:
branches:
only:

View File

@@ -492,14 +492,9 @@
"gcov",
"WCAG",
"stackedplot",
"Andale",
"unnormalized",
"checksnapshots",
"specced",
"composables",
"countup"
"Andale"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
"ignorePaths": [
"package.json",
"dist/**",

View File

@@ -1,61 +0,0 @@
name: 'pr:e2e:flakefinder'
on:
push:
branches: master
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
jobs:
e2e-flakefinder:
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:flakefinder') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.action == 'opened'
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/hydrogen'
- name: Cache NPM dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.39.0 install
- run: npm install --cache ~/.npm --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times)
run: npm run test:e2e:stable -- --retries=0 --repeat-each=10 --max-failures=50
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
with:
path: test-results
- name: Remove pr:e2e:flakefinder label (if present)
if: always()
uses: actions/github-script@v6
with:
script: |
const { owner, repo, number } = context.issue;
const labelToRemove = 'pr:e2e:flakefinder';
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: number,
name: labelToRemove
});
} catch (error) {
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
}

View File

@@ -1,58 +0,0 @@
name: 'e2e-perf'
on:
push:
branches: master
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
jobs:
e2e-full:
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:perf') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/hydrogen'
- name: Cache NPM dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.39.0 install
- run: npm install --cache ~/.npm --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
with:
path: test-results
- name: Remove pr:e2e:perf label (if present)
if: always()
uses: actions/github-script@v6
with:
script: |
const { owner, repo, number } = context.issue;
const labelToRemove = 'pr:e2e:perf';
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: number,
name: labelToRemove
});
} catch (error) {
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
}

View File

@@ -1,14 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"rvest.vs-code-prettier-eslint"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": ["octref.vetur"]
}

View File

@@ -109,7 +109,7 @@ For those interested in the mechanics of snapshot testing with Playwright, you c
// from our package.json or circleCI configuration file
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
npm install
npm run test:e2e:checksnapshots
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
```
### Updating Snapshots
@@ -134,12 +134,6 @@ npm install
npm run test:e2e:updatesnapshots
```
Once that's done, you'll need to run the following to verify that the changes do not cause more problems:
```sh
npm run test:e2e:checksnapshots
```
## Automated Accessibility (a11y) Testing
Open MCT incorporates accessibility testing through two primary methods to ensure its compliance with accessibility standards:

View File

@@ -61,6 +61,7 @@ export async function scanForA11yViolations(page, testCaseName, options = {}) {
const builder = new AxeBuilder({ page });
builder.withTags(['wcag2aa']);
// https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md
builder.disableRules(['color-contrast']);
const accessibilityScanResults = await builder.analyze();
// Assert that no violations should be present

View File

@@ -6,8 +6,7 @@
"end": 1660343797000,
"type": "Group 1",
"color": "orange",
"textColor": "white",
"id": 1
"textColor": "white"
},
{
"name": "Past event 2",
@@ -15,8 +14,7 @@
"end": 1660429160000,
"type": "Group 1",
"color": "orange",
"textColor": "white",
"id": 2
"textColor": "white"
},
{
"name": "Past event 3",
@@ -24,8 +22,7 @@
"end": 1660503981000,
"type": "Group 1",
"color": "orange",
"textColor": "white",
"id": 3
"textColor": "white"
},
{
"name": "Past event 4",
@@ -33,8 +30,7 @@
"end": 1660624108000,
"type": "Group 1",
"color": "orange",
"textColor": "white",
"id": 4
"textColor": "white"
},
{
"name": "Past event 5",
@@ -42,8 +38,7 @@
"end": 1660681529000,
"type": "Group 1",
"color": "orange",
"textColor": "white",
"id": 5
"textColor": "white"
}
]
}

View File

@@ -6,8 +6,7 @@
"end": 1660343797000,
"type": "Group 1",
"color": "orange",
"textColor": "white",
"id": 1
"textColor": "white"
},
{
"name": "Time until supper",
@@ -15,8 +14,7 @@
"end": 1650420410000,
"type": "Group 2",
"color": "blue",
"textColor": "white",
"id": 2
"textColor": "white"
}
],
"Group 2": [
@@ -26,8 +24,7 @@
"end": 1650320102001,
"type": "Group 2",
"color": "green",
"textColor": "white",
"id": 3
"textColor": "white"
},
{
"name": "Time since last accident",
@@ -35,8 +32,7 @@
"end": 1650320102002,
"type": "Group 1",
"color": "yellow",
"textColor": "white",
"id": 4
"textColor": "white"
}
]
}

View File

@@ -1,127 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify persistability checks
*/
import { fileURLToPath } from 'url';
import { expect, test } from '../../baseFixtures.js';
test.describe('Mission Status @addInit', () => {
const NO_GO = '0';
const GO = '1';
test.beforeEach(async ({ page }) => {
// FIXME: determine if plugins will be added to index.html or need to be injected
await page.addInitScript({
path: fileURLToPath(new URL('../../helper/addInitExampleUser.js', import.meta.url))
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await expect(page.getByText('Select Role')).toBeVisible();
// Description should be empty https://github.com/nasa/openmct/issues/6978
await expect(page.getByLabel('Dialog message')).toBeHidden();
// set role
await page.getByRole('button', { name: 'Select', exact: true }).click();
// dismiss role confirmation popup
await page.getByRole('button', { name: 'Dismiss' }).click();
});
test('Basic functionality', async ({ page }) => {
const imageryStatusSelect = page.getByRole('combobox', { name: 'Imagery' });
const commandingStatusSelect = page.getByRole('combobox', { name: 'Commanding' });
const drivingStatusSelect = page.getByRole('combobox', { name: 'Driving' });
const missionStatusPanel = page.getByRole('dialog', { name: 'User Control Panel' });
await test.step('Mission status panel shows/hides when toggled', async () => {
// Ensure that clicking the button toggles the dialog
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(missionStatusPanel).toBeVisible();
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(missionStatusPanel).toBeHidden();
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(missionStatusPanel).toBeVisible();
// Ensure that clicking the close button closes the dialog
await page.getByLabel('Close Mission Status Panel').click();
await expect(missionStatusPanel).toBeHidden();
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(missionStatusPanel).toBeVisible();
// Ensure clicking off the dialog also closes it
await page.getByLabel('My Items Grid View').click();
await expect(missionStatusPanel).toBeHidden();
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(missionStatusPanel).toBeVisible();
});
await test.step('Mission action statuses have correct defaults and can be set', async () => {
await expect(imageryStatusSelect).toHaveValue(NO_GO);
await expect(commandingStatusSelect).toHaveValue(NO_GO);
await expect(drivingStatusSelect).toHaveValue(NO_GO);
await setMissionStatus(page, 'Imagery', GO);
await expect(imageryStatusSelect).toHaveValue(GO);
await expect(commandingStatusSelect).toHaveValue(NO_GO);
await expect(drivingStatusSelect).toHaveValue(NO_GO);
await setMissionStatus(page, 'Commanding', GO);
await expect(imageryStatusSelect).toHaveValue(GO);
await expect(commandingStatusSelect).toHaveValue(GO);
await expect(drivingStatusSelect).toHaveValue(NO_GO);
await setMissionStatus(page, 'Driving', GO);
await expect(imageryStatusSelect).toHaveValue(GO);
await expect(commandingStatusSelect).toHaveValue(GO);
await expect(drivingStatusSelect).toHaveValue(GO);
await setMissionStatus(page, 'Imagery', NO_GO);
await expect(imageryStatusSelect).toHaveValue(NO_GO);
await expect(commandingStatusSelect).toHaveValue(GO);
await expect(drivingStatusSelect).toHaveValue(GO);
await setMissionStatus(page, 'Commanding', NO_GO);
await expect(imageryStatusSelect).toHaveValue(NO_GO);
await expect(commandingStatusSelect).toHaveValue(NO_GO);
await expect(drivingStatusSelect).toHaveValue(GO);
await setMissionStatus(page, 'Driving', NO_GO);
await expect(imageryStatusSelect).toHaveValue(NO_GO);
await expect(commandingStatusSelect).toHaveValue(NO_GO);
await expect(drivingStatusSelect).toHaveValue(NO_GO);
});
});
});
/**
*
* @param {import('@playwright/test').Page} page
* @param {'Commanding'|'Imagery'|'Driving'} action
* @param {'0'|'1'} status
*/
async function setMissionStatus(page, action, status) {
await page.getByRole('combobox', { name: action }).selectOption(status);
await expect(
page.getByRole('alert').filter({ hasText: 'Successfully set mission status' })
).toBeVisible();
await page.getByLabel('Dismiss').click();
}

View File

@@ -27,7 +27,7 @@ import {
assertPlanActivities,
assertPlanOrderedSwimLanes
} from '../../../helper/planningUtils.js';
import { expect, test } from '../../../pluginFixtures.js';
import { test } from '../../../pluginFixtures.js';
const testPlan1 = JSON.parse(
fs.readFileSync(
@@ -63,47 +63,4 @@ test.describe('Plan', () => {
});
await assertPlanOrderedSwimLanes(page, testPlanWithOrderedLanes, planWithSwimLanes.url);
});
test('Allows setting the state of an activity when selected.', async ({ page }) => {
const groups = Object.keys(testPlan1);
const firstGroupKey = groups[0];
const firstGroupItems = testPlan1[firstGroupKey];
const firstActivity = firstGroupItems[0];
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
const startBound = firstActivity.start;
// Set the endBound to the end time of the current activity
let endBound = lastActivity.end;
// eslint-disable-next-line playwright/no-conditional-in-test
if (endBound === startBound) {
// Prevent oddities with setting start and end bound equal
// via URL params
endBound += 1;
}
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${plan.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`
);
// select the first activity in the list
await page.getByText('Past event 1').click();
// Find the activity state section in the inspector
await page.getByRole('tab', { name: 'Activity' }).click();
// Check that activity state dropdown selection shows the `set status` option by default
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
'Not started'
);
// Change the selection of the activity status
await page.getByRole('combobox').selectOption({ label: 'Aborted' });
// select a different activity and back to the previous one
await page.getByText('Past event 2').click();
await page.getByText('Past event 1').click();
// Check that activity state dropdown selection shows the previously selected option by default
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
'Aborted'
);
});
});

View File

@@ -30,11 +30,6 @@ const examplePlanSmall3 = JSON.parse(
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
)
);
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
)
);
// eslint-disable-next-line no-unused-vars
const START_TIME_COLUMN = 0;
// eslint-disable-next-line no-unused-vars
@@ -43,10 +38,55 @@ const TIME_TO_FROM_COLUMN = 2;
// eslint-disable-next-line no-unused-vars
const ACTIVITY_COLUMN = 3;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;
const NUM_COLUMNS = 4;
const testPlan = {
TEST_GROUP: [
{
name: 'Past event 1',
start: 1660320408000,
end: 1660343797000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 2',
start: 1660406808000,
end: 1660429160000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 3',
start: 1660493208000,
end: 1660503981000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 4',
start: 1660579608000,
end: 1660624108000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 5',
start: 1660666008000,
end: 1660681529000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
}
]
};
test.describe('Time List', () => {
test("Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties", async ({
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
page
}) => {
// Goto baseURL
@@ -63,16 +103,12 @@ test.describe('Time List', () => {
await test.step('Create a Plan and add it to the timelist', async () => {
await createPlanFromJSON(page, {
name: 'Test Plan',
json: examplePlanSmall1,
json: testPlan,
parent: timelist.uuid
});
const groups = Object.keys(examplePlanSmall1);
const firstGroupKey = groups[0];
const firstGroupItems = examplePlanSmall1[firstGroupKey];
const firstActivity = firstGroupItems[0];
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
const startBound = firstActivity.start;
const endBound = lastActivity.end;
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
@@ -82,7 +118,7 @@ test.describe('Time List', () => {
// Verify all events are displayed
const eventCount = await page.getByRole('row').count();
// subtracting one for the header
await expect(eventCount - 1).toEqual(firstGroupItems.length);
await expect(eventCount - 1).toEqual(testPlan.TEST_GROUP.length);
});
await test.step('Does not show milliseconds in times', async () => {
@@ -95,81 +131,6 @@ test.describe('Time List', () => {
await expect(row.locator('.--end')).not.toContainText('.');
await expect(row.locator('.--duration')).not.toContainText('.');
});
await test.step('Shows activity properties when a row is selected', async () => {
await page.getByRole('row').nth(2).click();
// Find the activity state section in the inspector
await page.getByRole('tab', { name: 'Activity' }).click();
// Check that activity state label is displayed in the inspector.
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
'Not started'
);
});
});
});
test("View a timelist in expanded view, verify all the activities are displayed and selecting an activity shows it's properties", async ({
page
}) => {
// Goto baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
const timelist = await test.step('Create a Time List', async () => {
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
expect(objectName).toBe(createdTimeList.name);
return createdTimeList;
});
await test.step('Create a Plan and add it to the timelist', async () => {
await createPlanFromJSON(page, {
name: 'Test Plan',
json: examplePlanSmall1,
parent: timelist.uuid
});
// Ensure that all activities are shown in the expanded view
const groups = Object.keys(examplePlanSmall1);
const firstGroupKey = groups[0];
const firstGroupItems = examplePlanSmall1[firstGroupKey];
const firstActivity = firstGroupItems[0];
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
const startBound = firstActivity.start;
const endBound = lastActivity.end;
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
);
// Change the object to edit mode
await page.getByRole('button', { name: 'Edit Object' }).click();
// Find the display properties section in the inspector
await page.getByRole('tab', { name: 'View Properties' }).click();
// Switch to expanded view and save the setting
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
// Click on the "Save" button
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Verify all events are displayed
const eventCount = await page.getByRole('row').count();
await expect(eventCount).toEqual(firstGroupItems.length);
});
await test.step('Shows activity properties when a row is selected', async () => {
await page.getByRole('row').nth(2).click();
// Find the activity state section in the inspector
await page.getByRole('tab', { name: 'Activity' }).click();
// Check that activity state label is displayed in the inspector.
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
'Not started'
);
});
});
@@ -186,8 +147,8 @@ test("View a timelist in expanded view, verify all the activities are displayed
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;
/**
* @typedef {Object} CountdownOrUpObject
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
* @typedef {Object} CountdownObject
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, otherwise undefined).
* @property {string} days - The number of days in the countdown (undefined if there are no days).
* @property {string} hours - The number of hours in the countdown.
* @property {string} minutes - The number of minutes in the countdown.
@@ -259,13 +220,11 @@ test.describe('Time List with controlled clock', () => {
await test.step(`Countdown cell ${i + 1} counts down`, async () => {
const countdownCell = countdownCells[i];
// Get the initial countdown timestamp object
const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// should not have a '-' sign
await expect(countdownCell).not.toHaveText('-');
const beforeCountdown = await getAndAssertCountdownObject(page, i + 3);
// Wait until it changes
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new countdown timestamp object
const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
const afterCountdown = await getAndAssertCountdownObject(page, i + 3);
// Verify that the new countdown timestamp object is less than the old one
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
});
@@ -274,17 +233,15 @@ test.describe('Time List with controlled clock', () => {
// Verify that the count-up cells are counting up
for (let i = 0; i < countUpCells.length; i++) {
await test.step(`Count-up cell ${i + 1} counts up`, async () => {
const countUpCell = countUpCells[i];
const countdownCell = countUpCells[i];
// Get the initial count-up timestamp object
const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// should not have a '+' sign
await expect(countUpCell).not.toHaveText('+');
const beforeCountdown = await getAndAssertCountdownObject(page, i + 1);
// Wait until it changes
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new count-up timestamp object
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
const afterCountdown = await getAndAssertCountdownObject(page, i + 1);
// Verify that the new count-up timestamp object is greater than the old one
expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
expect(Number(afterCountdown.seconds)).toBeGreaterThan(Number(beforeCountdown.seconds));
});
}
});
@@ -314,13 +271,13 @@ async function getCellTextByIndex(page, rowIndex, columnIndex) {
}
/**
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
* Get the text from the countdown cell in the given row, assert that it matches the countdown
* regex, and return an object representing the countdown.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex the row index
* @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
* @returns {Promise<CountdownObject>} countdownObject
*/
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
async function getAndAssertCountdownObject(page, rowIndex) {
const timeToFrom = await getCellTextByIndex(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN);
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);

View File

@@ -35,7 +35,7 @@ import { expect, test } from '../../../../pluginFixtures.js';
let conditionSetUrl;
test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => {
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
test.beforeAll(async ({ browser }) => {
//TODO: This needs to be refactored
const context = await browser.newContext();
@@ -68,35 +68,30 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
});
//Begin suite of tests again localStorage
test.fixme(
'Condition set object properties persist in main view and inspector @localStorage',
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
test('Condition set object properties persist in main view and inspector @localStorage', async ({
page
}) => {
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect
.soft(page.locator('.l-browse-bar__object-name'))
.toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect
.soft(page.locator('.l-browse-bar__object-name'))
.toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
//Assertions on loaded Condition Set in Inspector
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
//Reload Page
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
//Reload Page
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
//Re-verify after reload
await expect
.soft(page.locator('.l-browse-bar__object-name'))
.toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
}
);
//Re-verify after reload
await expect
.soft(page.locator('.l-browse-bar__object-name'))
.toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
});
test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;

View File

@@ -161,13 +161,6 @@ test.describe('Display Layout', () => {
const trimmedDisplayValue = displayLayoutValue.trim();
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
// ensure we can right click on the alpha-numeric widget and view historical data
await page.getByLabel('Sine', { exact: true }).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Plot Container Style Target')).toBeVisible();
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({
page

View File

@@ -136,11 +136,7 @@ test.describe('Gauge', () => {
// TODO: Verify changes in the UI
});
test.fixme('Gauge does not display NaN when data not available', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
test('Gauge does not display NaN when data not available', async ({ page }) => {
// Create a Gauge
const gauge = await createDomainObjectWithDefaults(page, {
type: 'Gauge'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -63,66 +63,6 @@ test.describe('Overlay Plot', () => {
await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)');
});
test('Plot legend expands by default', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7403'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the legend is collapsed by default
await expect(page.getByLabel('Plot Legend Collapsed')).toBeVisible();
await expect(page.getByLabel('Plot Legend Expanded')).toBeHidden();
await expect(page.getByLabel('Expand by Default')).toHaveText('No');
expect(await page.getByLabel('Plot Legend Item').count()).toBe(3);
// Change the legend to expand by default
await page.getByLabel('Edit Object').click();
await page.getByLabel('Expand By Default').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Assert that the legend is now open
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes');
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
// Assert that the legend is expanded on page load
await page.reload();
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes');
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
});
test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({
page
}) => {
@@ -284,37 +224,31 @@ test.describe('Overlay Plot', () => {
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
});
test.fixme(
'Clicking on an item in the elements pool brings up the plot preview with data points',
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({
page
}) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await waitForPlotsToRender(page);
await page.getByLabel('Edit Object').click();
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await waitForPlotsToRender(page);
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Elements' }).click();
await page.getByRole('tab', { name: 'Elements' }).click();
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
}
);
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
});
});
/**

View File

@@ -52,11 +52,7 @@ test.describe('Plot Rendering', () => {
expect(createMineFolderRequests.length).toEqual(0);
});
test.fixme('Plot is rendered when infinity values exist', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
test('Plot is rendered when infinity values exist', async ({ page }) => {
// Edit Plot
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);

View File

@@ -1,88 +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 } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Plots work in Previews', () => {
test('We can preview plot in display layouts', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a Sinewave Generator
const sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Test Display Layout'
});
// Edit Display Layout
await page.getByLabel('Edit Object').click();
// Expand the 'My Items' folder in the left tree
await page.getByLabel(`Expand ${myItemsFolderName} folder`).click();
// Add the Sine Wave Generator to the Display Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const layoutGridHolder = page.getByLabel('Test Display Layout Layout Grid');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// right click on the plot and select view large
await page.getByLabel('Sine', { exact: true }).click({ button: 'right' });
await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByLabel('Close').click();
await page.getByLabel('Expand Test Display Layout layout').click();
// change to a plot and ensure embiggen works
await page.getByLabel('Edit Object').click();
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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(
page.getByLabel('Test Display Layout Layout', { exact: true }).getByLabel('Plot Canvas')
).toBeVisible();
await expect(page.getByLabel('Preview Container')).toBeHidden();
await page.getByLabel('Large View').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByLabel('Close').click();
// get last sinewave tree item (in the display layout)
await page
.getByRole('treeitem', { name: /Sine Wave Generator/ })
.locator('a')
.last()
.click({ button: 'right' });
await page.getByLabel('View', { exact: true }).click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByLabel('Close').click();
});
});

View File

@@ -257,56 +257,6 @@ test.describe('Stacked Plot', () => {
await assertAggregateLegendIsVisible(page);
});
test('can toggle between aggregate and per child legends', async ({ page }) => {
// make some an overlay plot
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
parent: stackedPlot.uuid
});
// make some SWGs for the overlay plot
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(stackedPlot.url);
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByLabel('Inspector Views').getByRole('checkbox').uncheck();
await page.getByLabel('Expand By Default').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(page.getByLabel('Plot Legend Expanded')).toHaveCount(1);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
// reload and ensure the legend is still expanded
await page.reload();
await expect(page.getByLabel('Plot Legend Expanded')).toHaveCount(1);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
// change to collapsed by default
await page.getByLabel('Edit Object').click();
await page.getByLabel('Expand By Default').uncheck();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(page.getByLabel('Plot Legend Collapsed')).toHaveCount(1);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
// change it to individual legends
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByLabel('Show Legends For Children').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(page.getByLabel('Plot Legend Collapsed')).toHaveCount(4);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
});
});
/**

View File

@@ -1,33 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite is dedicated to testing the preview plugin.
*/
import { test } from '../../../../pluginFixtures.js';
test.describe('Preview mode', () => {
test.fixme('all context menu items are available for a telemetry table', async ({ page }) => {
// compare the context menu options when viewing a telemetry table directly
// vs when it is presented in preview mode (e.g. edit mode is enabled and the table is clicked on from the tree)
});
});

View File

@@ -1,125 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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, expandEntireTree } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Reload action', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
const alphaTable = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
name: 'Alpha Table'
});
const betaTable = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
name: 'Beta Table'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: alphaTable.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.001'
}
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: betaTable.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.001'
}
});
await page.goto(displayLayout.url);
// Expand all folders
await expandEntireTree(page);
await page.getByLabel('Edit Object', { exact: true }).click();
await page.dragAndDrop(`text='Alpha Table'`, '.l-layout__grid-holder', {
targetPosition: { x: 0, y: 0 }
});
await page.dragAndDrop(`text='Beta Table'`, '.l-layout__grid-holder', {
targetPosition: { x: 0, y: 250 }
});
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
});
test('can reload display layout and its children', async ({ page }) => {
const beforeReloadAlphaTelemetryValue = await page
.getByLabel('Alpha Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
const beforeReloadBetaTelemetryValue = await page
.getByLabel('Beta Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
// reload alpha
await page.getByTitle('View menu items').first().click();
await page.getByRole('menuitem', { name: /Reload/ }).click();
const afterReloadAlphaTelemetryValue = await page
.getByLabel('Alpha Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
const afterReloadBetaTelemetryValue = await page
.getByLabel('Beta Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
expect(beforeReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);
expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue);
// now reload parent
await page.getByTitle('More actions').click();
await page.getByRole('menuitem', { name: /Reload/ }).click();
const fullReloadAlphaTelemetryValue = await page
.getByLabel('Alpha Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
const fullReloadBetaTelemetryValue = await page
.getByLabel('Beta Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);
expect(fullReloadBetaTelemetryValue).not.toEqual(afterReloadBetaTelemetryValue);
});
});

View File

@@ -32,9 +32,9 @@ const setBorderColor = '#ff00ff';
const setBackgroundColor = '#5b0f00';
const setTextColor = '#e6b8af';
const defaultFrameBorderColor = '#e6b8af'; //default border color
const defaultBorderTargetColor = '#acacac';
const defaultTextColor = '#acacac'; // default text color
const inheritedColor = '#acacac'; // inherited from the body style
const defaultBorderTargetColor = '#aaaaaa';
const defaultTextColor = '#aaaaaa'; // default text color
const inheritedColor = '#aaaaaa'; // inherited from the body style
const pukeGreen = '#6aa84f'; //Ugliest green known to man
const NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value
@@ -397,8 +397,8 @@ test.describe('Flexible Layout styling', () => {
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Reload page and verify that styles persist
await page.reload({ waitUntil: 'domcontentloaded' });

View File

@@ -36,9 +36,9 @@ import { test } from '../../../../pluginFixtures.js';
const setBorderColor = '#ff00ff';
const setBackgroundColor = '#5b0f00';
const setTextColor = '#e6b8af';
const defaultTextColor = '#acacac'; // default text color
const defaultTextColor = '#aaaaaa'; // default text color
const NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value
const DEFAULT_PLOT_VIEW_BORDER_COLOR = '#acacac';
const DEFAULT_PLOT_VIEW_BORDER_COLOR = '#AAAAAA';
const setFontSize = '72px';
const setFontWeight = '700'; //bold for monospace bold
const setFontFamily = '"Andale Mono", sans-serif';

View File

@@ -24,18 +24,13 @@ import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Tabs View', () => {
let tabsView;
let table;
let notebook;
let sineWaveGenerator;
test.beforeEach(async ({ page }) => {
test('Renders tabbed elements', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
tabsView = await createDomainObjectWithDefaults(page, {
const tabsView = await createDomainObjectWithDefaults(page, {
type: 'Tabs View'
});
table = await createDomainObjectWithDefaults(page, {
const table = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
parent: tabsView.uuid
});
@@ -43,21 +38,19 @@ test.describe('Tabs View', () => {
type: 'Event Message Generator',
parent: table.uuid
});
notebook = await createDomainObjectWithDefaults(page, {
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
parent: tabsView.uuid
});
sineWaveGenerator = await createDomainObjectWithDefaults(page, {
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: tabsView.uuid
});
});
test('Renders tabbed elements', async ({ page }) => {
await page.goto(tabsView.url);
page.goto(tabsView.url);
// select first tab
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
await page.getByLabel(`${table.name} tab`).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
@@ -65,7 +58,7 @@ test.describe('Tabs View', () => {
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
// select second tab
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
await page.getByLabel(`${notebook.name} tab`).click();
// ensure notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
@@ -74,7 +67,7 @@ test.describe('Tabs View', () => {
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
// select third tab
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
// expect sine wave generator visible
await expect(page.locator('.c-plot')).toBeVisible();
@@ -85,7 +78,7 @@ test.describe('Tabs View', () => {
await expect(page.locator('canvas').nth(1)).toBeVisible();
// now try to select the first tab again
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
await page.getByLabel(`${table.name} tab`).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
@@ -93,29 +86,3 @@ test.describe('Tabs View', () => {
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
});
});
test.describe('Tabs View CRUD', () => {
let tabsView;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
tabsView = await createDomainObjectWithDefaults(page, {
type: 'Tabs View'
});
});
test('Eager Load Tabs is the default and then can be toggled off', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7198'
});
await page.goto(tabsView.url);
await page.getByLabel('Edit Object').click();
await page.getByLabel('More actions').click();
await page.getByLabel('Edit Properties...').click();
await expect(await page.getByLabel('Eager Load Tabs')).not.toBeChecked();
await page.getByLabel('Eager Load Tabs').setChecked(true);
await expect(await page.getByLabel('Eager Load Tabs')).toBeChecked();
});
});

View File

@@ -64,9 +64,10 @@ test.describe('Telemetry Table', () => {
// Get the most recent telemetry date
const latestTelemetryDate = await page
.getByLabel('table content')
.getByLabel('utc table cell')
.locator('table.c-telemetry-table__body > tbody > tr')
.last()
.locator('td')
.nth(1)
.getAttribute('title');
// Verify that it is <= our new end bound
@@ -90,7 +91,7 @@ test.describe('Telemetry Table', () => {
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Roger');
let cells = await page.getByRole('cell').getByText(/Roger/).all();
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBeGreaterThan(1);
// ensure the text content of each cell contains the search term
@@ -102,10 +103,7 @@ test.describe('Telemetry Table', () => {
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Dodger');
cells = await page
.getByRole('cell')
.getByText(/Dodger/)
.all();
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBe(0);
// ensure the text content of each cell contains the search term
@@ -137,7 +135,7 @@ test.describe('Telemetry Table', () => {
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Rr]oger/');
let cells = await page.getByRole('cell').getByText(/Roger/).all();
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBeGreaterThan(1);
// ensure the text content of each cell contains the search term
@@ -149,10 +147,7 @@ test.describe('Telemetry Table', () => {
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Dd]oger/');
cells = await page
.getByRole('cell')
.getByText(/Dodger/)
.all();
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBe(0);
// ensure the text content of each cell contains the search term

View File

@@ -359,11 +359,7 @@ test.describe('Verify tooltips', () => {
expect(tooltipText).toBe(sineWaveObject3.path);
});
test.fixme('display tooltip path for telemetry table names', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
test('display tooltip path for telemetry table names', async ({ page }) => {
// set endBound to 10 seconds after start bound
const url = await page.url();
const parsedUrl = new URL(url.replace('#', '!'));

View File

@@ -24,7 +24,7 @@ import { createDomainObjectWithDefaults, waitForPlotsToRender } from '../../appA
import { expect, test } from '../../pluginFixtures.js';
test.describe('Tabs View', () => {
test('Renders tabbed elements only when visible', async ({ page }) => {
test('Renders tabbed elements nicely', async ({ page }) => {
// Code to hook into the requestAnimationFrame function and log each call
let animationCalls = [];
await page.exposeFunction('logCall', (callCount) => {
@@ -64,24 +64,24 @@ 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`).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
// select second tab
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
await page.getByLabel(`${notebook.name} tab`).click();
// expect notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
// select third tab
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
// ensure sine wave generator visible
expect(await page.locator('.c-plot').isVisible()).toBe(true);
// now select notebook and clear animation calls
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
await page.getByLabel(`${notebook.name} tab`).click();
animationCalls = [];
// expect notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
@@ -89,7 +89,7 @@ test.describe('Tabs View', () => {
// select sine wave generator and clear animation calls
animationCalls = [];
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
// ensure sine wave generator visible
await waitForPlotsToRender(page);

View File

@@ -20,16 +20,14 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { test } from '../../avpFixtures.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_URL } from '../../constants.js';
test.describe('a11y - Default', () => {
test.describe('a11y - Default @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
test('main view', async ({ page }, testInfo) => {
await page.goto('./');
//Skipping for https://github.com/nasa/openmct/issues/7421
//await scanForA11yViolations(page, testInfo.title);
test('main view @a11y', async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -26,7 +26,7 @@ Tests the branding associated with the default deployment. At least the about mo
import percySnapshot from '@percy/playwright';
import { expect, test } from '../../../avpFixtures.js';
import { expect, scanForA11yViolations, test } from '../../../avpFixtures.js';
import { VISUAL_URL } from '../../../constants.js';
//Declare the scope of the visual test
@@ -78,7 +78,6 @@ test.describe('Visual - Header @a11y', () => {
await expect(await page.getByLabel('Show Snapshots')).toBeVisible();
});
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@@ -22,7 +22,7 @@
import percySnapshot from '@percy/playwright';
import { test } from '../../../avpFixtures.js';
import { scanForA11yViolations, test } from '../../../avpFixtures.js';
import { MISSION_TIME, VISUAL_URL } from '../../../constants.js';
//Declare the scope of the visual test
@@ -55,7 +55,6 @@ test.describe('Visual - Inspector @ally', () => {
});
});
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@@ -1,93 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../appActions.js';
import { VISUAL_URL } from '../../constants.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('Visual - Example Imagery', () => {
let exampleImagery;
let parentLayout;
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
parentLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Parent Layout'
});
exampleImagery = await createDomainObjectWithDefaults(page, {
type: 'Example Imagery',
name: 'Example Imagery Test',
parent: parentLayout.uuid
});
// Modify Example Imagery to create a really stable Example Imagery
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: 'More actions' }).click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page
.locator('#imageLocation-textarea')
.fill(
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg'
);
await page.getByRole('button', { name: 'Save' }).click();
await page.reload({ waitUntil: 'domcontentloaded' });
await page.getByTitle('Collapse Browse Pane').click();
await page.getByTitle('Collapse Inspect Pane').click();
});
test('Example Imagery in Fixed Time', async ({ page, theme }) => {
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
await percySnapshot(page, `Example Imagery in Fixed Time (theme: ${theme})`);
await page.getByLabel('Image Wrapper').hover();
await percySnapshot(page, `Example Imagery Hover in Fixed Time (theme: ${theme})`);
});
test('Example Imagery in Real Time', async ({ page, theme }) => {
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
await setRealTimeMode(page, true);
//Temporary to close the dialog
await page.getByLabel('Submit time offsets').click();
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
await percySnapshot(page, `Example Imagery in Real Time (theme: ${theme})`);
});
test('Example Imagery in Display Layout', async ({ page, theme }) => {
await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' });
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
await percySnapshot(page, `Example Imagery in Display Layout (theme: ${theme})`);
});
});

View File

@@ -1,57 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import percySnapshot from '@percy/playwright';
import { fileURLToPath } from 'url';
import { expect, scanForA11yViolations, test } from '../../avpFixtures.js';
test.describe('Mission Status Visual Tests @a11y', () => {
const GO = '1';
test.beforeEach(async ({ page }) => {
await page.addInitScript({
path: fileURLToPath(new URL('../../helper/addInitExampleUser.js', import.meta.url))
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await expect(page.getByText('Select Role')).toBeVisible();
// Description should be empty https://github.com/nasa/openmct/issues/6978
await expect(page.locator('c-message__action-text')).toBeHidden();
// set role
await page.getByRole('button', { name: 'Select', exact: true }).click();
// dismiss role confirmation popup
await page.getByRole('button', { name: 'Dismiss' }).click();
});
test('Mission status panel', async ({ page, theme }) => {
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(page.getByRole('dialog', { name: 'User Control Panel' })).toBeVisible();
await percySnapshot(page, `Mission status panel w/ default statuses (theme: '${theme}')`);
await page.getByRole('combobox', { name: 'Commanding' }).selectOption(GO);
await expect(
page.getByRole('alert').filter({ hasText: 'Successfully set mission status' })
).toBeVisible();
await page.getByLabel('Dismiss').click();
await percySnapshot(page, `Mission status panel w/ non-default status (theme: '${theme}')`);
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -23,11 +23,11 @@
import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults, expandTreePaneItemByName } from '../../appActions.js';
import { test } from '../../avpFixtures.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_URL } from '../../constants.js';
import { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js';
test.describe('Visual - Restricted Notebook @a11y', () => {
test.describe('Visual - Restricted Notebook', () => {
test.beforeEach(async ({ page }) => {
const restrictedNotebook = await startAndAddRestrictedNotebookObject(page);
await page.goto(restrictedNotebook.url + '?hideTree=true&hideInspector=true');
@@ -39,7 +39,7 @@ test.describe('Visual - Restricted Notebook @a11y', () => {
});
});
test.describe('Visual - Notebook @a11y', () => {
test.describe('Visual - Notebook', () => {
let notebook;
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
@@ -125,8 +125,7 @@ test.describe('Visual - Notebook @a11y', () => {
// Take a snapshot
await percySnapshot(page, `Notebook Selected Entry Text Area Active (theme: '${theme}')`);
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -24,7 +24,7 @@ import percySnapshot from '@percy/playwright';
import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
import { test } from '../../avpFixtures.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_URL } from '../../constants.js';
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
@@ -34,7 +34,7 @@ const examplePlanSmall = JSON.parse(
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
test.describe('Visual - Planning', () => {
test.describe('Visual - Planning @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
@@ -75,25 +75,7 @@ test.describe('Visual - Planning', () => {
parent: ganttChart.uuid
});
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`, {
scope: snapshotScope
});
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByLabel('Edit Object').click();
await page.getByLabel('Clip Activity Names').click();
// Close the inspect pane and save the changes
await page.getByRole('button', { name: 'Collapse Inspect Pane' }).click();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Dismiss the notification
await page.getByLabel('Dismiss').click();
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`, {
await percySnapshot(page, `Gantt Chart View (theme: ${theme})`, {
scope: snapshotScope
});
});
@@ -116,31 +98,8 @@ test.describe('Visual - Planning', () => {
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, {
scope: snapshotScope
});
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByLabel('Edit Object').click();
await page.getByLabel('Clip Activity Names').click();
// Close the inspect pane and save the changes
await page.getByRole('button', { name: 'Collapse Inspect Pane' }).click();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Dismiss the notification
await page.getByLabel('Dismiss').click();
await percySnapshot(
page,
`Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names`,
{
scope: snapshotScope
}
);
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -60,22 +60,10 @@ const STATUSES = [
statusFgColor: '#fff'
}
];
const MISSION_STATUSES = [
{
key: 0,
label: 'NO GO'
},
{
key: 1,
label: 'GO'
}
];
/**
* @implements {StatusUserProvider}
*/
export default class ExampleUserProvider extends EventEmitter {
#actionToStatusMap;
constructor(
openmct,
{ statusRoles } = {
@@ -85,11 +73,6 @@ export default class ExampleUserProvider extends EventEmitter {
super();
this.openmct = openmct;
this.#actionToStatusMap = {
Imagery: MISSION_STATUSES[0],
Commanding: MISSION_STATUSES[0],
Driving: MISSION_STATUSES[0]
};
this.user = undefined;
this.loggedIn = false;
this.autoLoginUser = undefined;
@@ -127,11 +110,6 @@ export default class ExampleUserProvider extends EventEmitter {
canSetPollQuestion() {
return Promise.resolve(true);
}
canSetMissionStatus() {
return Promise.resolve(true);
}
hasRole(roleId) {
if (!this.loggedIn) {
Promise.resolve(undefined);
@@ -144,28 +122,6 @@ export default class ExampleUserProvider extends EventEmitter {
return this.user.getRoles();
}
getPossibleMissionActions() {
return Promise.resolve(Object.keys(this.#actionToStatusMap));
}
getPossibleMissionActionStatuses() {
return Promise.resolve(MISSION_STATUSES);
}
getStatusForMissionAction(action) {
return Promise.resolve(this.#actionToStatusMap[action]);
}
setStatusForMissionAction(action, status) {
this.#actionToStatusMap[action] = status;
this.emit('missionStatusChange', {
action,
status
});
return true;
}
getAllStatusRoles() {
return Promise.resolve(this.statusRoles);
}

View File

@@ -92,8 +92,6 @@ GeneratorProvider.prototype.request = function (domainObject, request) {
var workerRequest = this.makeWorkerRequest(domainObject, request);
workerRequest.start = request.start;
workerRequest.end = request.end;
workerRequest.size = request.size;
workerRequest.strategy = request.strategy;
return this.workerInterface.request(workerRequest);
};

View File

@@ -130,37 +130,48 @@
var now = Date.now();
var start = request.start;
var end = request.end > now ? now : request.end;
var amplitude = request.amplitude;
var period = request.period;
var offset = request.offset;
var dataRateInHz = request.dataRateInHz;
var phase = request.phase;
var randomness = request.randomness;
var loadDelay = Math.max(request.loadDelay, 0);
var size = request.size;
var duration = end - start;
var infinityValues = request.infinityValues;
var exceedFloat32 = request.exceedFloat32;
var step = 1000 / dataRateInHz;
var maxPoints = Math.floor(duration / step);
var nextStep = start - (start % step) + step;
var data = [];
if (request.strategy === 'minmax' && size) {
// Calculate the number of cycles to include based on size (2 points per cycle)
var totalCycles = Math.min(Math.floor(size / 2), Math.floor(duration / period));
for (let cycle = 0; cycle < totalCycles; cycle++) {
// Distribute cycles evenly across the time range
let cycleStart = start + (duration / totalCycles) * cycle;
let minPointTime = cycleStart; // Assuming min at the start of the cycle
let maxPointTime = cycleStart + period / 2; // Assuming max at the halfway of the cycle
data.push(createDataPoint(minPointTime, request), createDataPoint(maxPointTime, request));
}
} else {
for (let i = 0; i < maxPoints && nextStep < end; i++, nextStep += step) {
data.push(createDataPoint(nextStep, request));
}
}
if (request.strategy !== 'minmax' && size) {
data = data.slice(-size);
for (; nextStep < end && data.length < 5000; nextStep += step) {
data.push({
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(
nextStep,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(
nextStep,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
)
});
}
if (loadDelay === 0) {
@@ -170,35 +181,6 @@
}
}
function createDataPoint(time, request) {
return {
utc: time,
yesterday: time - 60 * 60 * 24 * 1000,
sin: sin(
time,
request.period,
request.amplitude,
request.offset,
request.phase,
request.randomness,
request.infinityValues,
request.exceedFloat32
),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(
time,
request.period,
request.amplitude,
request.offset,
request.phase,
request.randomness,
request.infinityValues,
request.exceedFloat32
)
};
}
function postOnRequest(message, request, data) {
self.postMessage({
id: message.id,

View File

@@ -28,12 +28,12 @@
"d3-axis": "3.0.0",
"d3-scale": "4.0.2",
"d3-selection": "3.0.0",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint": "8.54.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-compat": "4.2.0",
"eslint-plugin-no-unsanitized": "4.0.2",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-unicorn": "49.0.0",
"eslint-plugin-vue": "9.18.1",
@@ -57,9 +57,9 @@
"karma-webpack": "5.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"marked": "11.2.0",
"marked": "11.1.0",
"mini-css-extract-plugin": "2.7.6",
"moment": "2.30.1",
"moment": "2.29.4",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41",
"npm-run-all2": "6.1.1",
@@ -67,13 +67,12 @@
"painterro": "1.2.87",
"plotly.js-basic-dist-min": "2.20.0",
"plotly.js-gl2d-dist-min": "2.20.0",
"prettier": "3.2.5",
"prettier-eslint": "16.3.0",
"prettier": "2.8.7",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.11.0",
"sass": "1.68.0",
"sass-loader": "14.0.0",
"sass-loader": "13.3.2",
"sinon": "17.0.0",
"style-loader": "3.3.3",
"terser-webpack-plugin": "5.3.9",
@@ -112,7 +111,6 @@
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:generatedata": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @generatedata",
"test:e2e:checksnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --retries=0",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep-invert @unstable",
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --grep-invert @unstable",

View File

@@ -251,7 +251,6 @@ export class MCT extends EventEmitter {
this.install(this.plugins.FlexibleLayout());
this.install(this.plugins.GoToOriginalAction());
this.install(this.plugins.OpenInNewTabAction());
this.install(this.plugins.ReloadAction());
this.install(this.plugins.WebPage());
this.install(this.plugins.Condition());
this.install(this.plugins.ConditionWidget());

View File

@@ -28,8 +28,7 @@
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:aria-disabled="action.isDisabled"
:class="action.cssClass"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:aria-label="action.name"
:title="action.description"
@click="action.onItemClicked"
@@ -52,8 +51,7 @@
v-for="action in options.actions"
:key="action.name"
role="menuitem"
:aria-disabled="action.isDisabled"
:class="action.cssClass"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:aria-label="action.name"
:title="action.description"
@click="action.onItemClicked"

View File

@@ -37,8 +37,7 @@
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:aria-disabled="action.isDisabled"
:class="action.cssClass"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"

View File

@@ -99,13 +99,7 @@ export default class ObjectAPI {
this.cache = {};
this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = [
'notebook',
'restricted-notebook',
'plan',
'annotation',
'activity-states'
];
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
this.errors = {
Conflict: ConflictError
@@ -354,9 +348,6 @@ export default class ObjectAPI {
isPersistable(idOrKeyString) {
let identifier = utils.parseKeyString(idOrKeyString);
let provider = this.getProvider(identifier);
if (provider?.isReadOnly) {
return !provider.isReadOnly();
}
return provider !== undefined && provider.create !== undefined && provider.update !== undefined;
}

View File

@@ -61,7 +61,6 @@ class Overlay extends EventEmitter {
dismiss() {
this.emit('destroy');
this.destroy();
this.container.remove();
}
//Ensures that any callers are notified that the overlay is dismissed

View File

@@ -145,7 +145,7 @@ class BatchingWebSocket extends EventTarget {
* In this event it will sacrifice the oldest telemetry in the batch in favor of the
* most recent telemetry. The user will be informed that telemetry has been dropped.
*
* This should be set appropriately for the expected data rate. eg. If telemetry
* This should be specced appropriately for the expected data rate. eg. If telemetry
* is received at 10Hz for each telemetry point, then a minimal combination of batch
* size and rate is 10 and 1000 respectively. Ideally you would add some margin, so
* 15 would probably be a better batch size.

View File

@@ -84,7 +84,6 @@ const SUBSCRIBE_STRATEGY = {
*/
export default class TelemetryAPI {
#isGreedyLAD;
#subscribeCache;
get SUBSCRIBE_STRATEGY() {
return SUBSCRIBE_STRATEGY;
@@ -107,7 +106,6 @@ export default class TelemetryAPI {
this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();
this.#isGreedyLAD = true;
this.BatchingWebSocket = BatchingWebSocket;
this.#subscribeCache = {};
}
abortAllRequests() {
@@ -415,104 +413,82 @@ export default class TelemetryAPI {
* the subscription
*/
subscribe(domainObject, callback, options = { strategy: SUBSCRIBE_STRATEGY.LATEST }) {
const requestedStrategy = options.strategy || SUBSCRIBE_STRATEGY.LATEST;
if (domainObject.type === 'unknown') {
return () => {};
}
const provider = this.findSubscriptionProvider(domainObject, options);
const supportsBatching =
Boolean(provider?.supportsBatching) && provider?.supportsBatching(domainObject, options);
// Default behavior is to use the latest strategy, as opposed to the new "batch" strategy
if (options.strategy === undefined || options.strategy === null) {
options.strategy = SUBSCRIBE_STRATEGY.LATEST;
}
if (!this.#subscribeCache) {
this.#subscribeCache = {};
const provider = this.findSubscriptionProvider(domainObject, options);
if (!this.subscribeCache) {
this.subscribeCache = {};
}
const keyString = objectUtils.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}`;
let subscriber = this.#subscribeCache[cacheKey];
let subscriber = this.subscribeCache[keyString];
if (!subscriber) {
subscriber = this.#subscribeCache[cacheKey] = {
latestCallbacks: [],
batchCallbacks: []
subscriber = this.subscribeCache[keyString] = {
callbacks: [callback]
};
if (provider) {
subscriber.unsubscribe = provider.subscribe(
domainObject,
invokeCallbackWithRequestedStrategy,
optionsWithSupportedStrategy
options.strategy === SUBSCRIBE_STRATEGY.BATCH
? subscriptionCallbackForArray
: subscriptionCallbackForSingleValue,
options
);
} else {
subscriber.unsubscribe = function () {};
}
}
if (requestedStrategy === SUBSCRIBE_STRATEGY.BATCH) {
subscriber.batchCallbacks.push(callback);
} else {
subscriber.latestCallbacks.push(callback);
subscriber.callbacks.push(callback);
}
// Guarantees that view receive telemetry in the expected form
function invokeCallbackWithRequestedStrategy(data) {
invokeCallbacksWithArray(data, subscriber.batchCallbacks);
invokeCallbacksWithSingleValue(data, subscriber.latestCallbacks);
}
function invokeCallbacksWithArray(data, batchCallbacks) {
//
if (data === undefined || data === null || data.length === 0) {
function subscriptionCallbackForArray(value) {
if (value === undefined || value === null || value.length === 0) {
throw new Error(
'Attempt to invoke telemetry subscription callback with no telemetry datum'
);
}
if (!Array.isArray(data)) {
data = [data];
if (!Array.isArray(value)) {
value = [value];
}
batchCallbacks.forEach((cb) => {
cb(data);
subscriber.callbacks.forEach(function (cb) {
cb(value);
});
}
function invokeCallbacksWithSingleValue(data, latestCallbacks) {
if (Array.isArray(data)) {
data = data[data.length - 1];
function subscriptionCallbackForSingleValue(value) {
if (Array.isArray(value)) {
value = value[value.length - 1];
}
if (data === undefined || data === null) {
if (value === undefined || value === null) {
throw new Error(
'Attempt to invoke telemetry subscription callback with no telemetry datum'
);
}
latestCallbacks.forEach((cb) => {
cb(data);
subscriber.callbacks.forEach(function (cb) {
cb(value);
});
}
return function unsubscribe() {
subscriber.latestCallbacks = subscriber.latestCallbacks.filter(function (cb) {
subscriber.callbacks = subscriber.callbacks.filter(function (cb) {
return cb !== callback;
});
subscriber.batchCallbacks = subscriber.batchCallbacks.filter(function (cb) {
return cb !== callback;
});
if (subscriber.latestCallbacks.length === 0 && subscriber.batchCallbacks.length === 0) {
if (subscriber.callbacks.length === 0) {
subscriber.unsubscribe();
delete this.#subscribeCache[cacheKey];
delete this.subscribeCache[keyString];
}
}.bind(this);
}

View File

@@ -327,126 +327,6 @@ describe('Telemetry API', () => {
signal
});
});
describe('telemetry batching support', () => {
let callbacks;
let unsubFunc;
beforeEach(() => {
callbacks = [];
unsubFunc = jasmine.createSpy('unsubscribe');
telemetryProvider.supportsBatching = jasmine.createSpy('supportsBatching');
telemetryProvider.supportsBatching.and.returnValue(true);
telemetryProvider.supportsSubscribe.and.returnValue(true);
telemetryProvider.subscribe.and.callFake(function (obj, cb, options) {
callbacks.push(cb);
return unsubFunc;
});
telemetryAPI.addProvider(telemetryProvider);
});
it('caches subscriptions for batched and latest telemetry subscriptions', () => {
const latestCallback1 = jasmine.createSpy('latestCallback1');
const unsubscribeFromLatest1 = telemetryAPI.subscribe(domainObject, latestCallback1, {
strategy: 'latest'
});
const latestCallback2 = jasmine.createSpy('latestCallback2');
const unsubscribeFromLatest2 = telemetryAPI.subscribe(domainObject, latestCallback2, {
strategy: 'latest'
});
//Expect a single cached subscription for latest telemetry
expect(telemetryProvider.subscribe.calls.count()).toBe(1);
const batchedCallback1 = jasmine.createSpy('batchedCallback1');
const unsubscribeFromBatched1 = telemetryAPI.subscribe(domainObject, batchedCallback1, {
strategy: 'batch'
});
const batchedCallback2 = jasmine.createSpy('batchedCallback2');
const unsubscribeFromBatched2 = telemetryAPI.subscribe(domainObject, batchedCallback2, {
strategy: 'batch'
});
//Expect a single cached subscription for each strategy telemetry
expect(telemetryProvider.subscribe.calls.count()).toBe(2);
unsubscribeFromLatest1();
unsubscribeFromLatest2();
unsubscribeFromBatched1();
unsubscribeFromBatched2();
expect(unsubFunc).toHaveBeenCalledTimes(2);
});
it('subscriptions with the latest strategy are always invoked with a single value', () => {
const latestCallback = jasmine.createSpy('latestCallback1');
telemetryAPI.subscribe(domainObject, latestCallback, {
strategy: 'latest'
});
const batchedValues = [1, 2, 3];
callbacks.forEach((cb) => {
cb(batchedValues);
});
expect(latestCallback).toHaveBeenCalledWith(3);
const singleValue = 1;
callbacks.forEach((cb) => {
cb(singleValue);
});
expect(latestCallback).toHaveBeenCalledWith(1);
});
it('subscriptions with the batch strategy are always invoked with an array', () => {
const batchedCallback = jasmine.createSpy('batchedCallback1');
const latestCallback = jasmine.createSpy('latestCallback1');
telemetryAPI.subscribe(domainObject, batchedCallback, {
strategy: 'batch'
});
telemetryAPI.subscribe(domainObject, latestCallback, {
strategy: 'latest'
});
const batchedValues = [1, 2, 3];
callbacks.forEach((cb) => {
cb(batchedValues);
});
// Callbacks for the 'batch' strategy are always called with an array of values
expect(batchedCallback).toHaveBeenCalledWith(batchedValues);
// Callbacks for the 'latest' strategy are always called with a single value
expect(latestCallback).toHaveBeenCalledWith(3);
callbacks.forEach((cb) => {
cb(1);
});
// Callbacks for the 'batch' strategy are always called with an array of values, even if there is only one value
expect(batchedCallback).toHaveBeenCalledWith([1]);
// Callbacks for the 'latest' strategy are always called with a single value
expect(latestCallback).toHaveBeenCalledWith(1);
});
it('legacy providers are left unchanged, with a single subscription', () => {
delete telemetryProvider.supportsBatching;
const batchCallback = jasmine.createSpy('batchCallback');
telemetryAPI.subscribe(domainObject, batchCallback, {
strategy: 'batch'
});
expect(telemetryProvider.subscribe.calls.mostRecent().args[2].strategy).toBe('latest');
const latestCallback = jasmine.createSpy('latestCallback');
telemetryAPI.subscribe(domainObject, latestCallback, {
strategy: 'latest'
});
expect(telemetryProvider.subscribe.calls.mostRecent().args[2].strategy).toBe('latest');
});
});
});
describe('metadata', () => {

View File

@@ -212,8 +212,6 @@ export default class TelemetryCollection extends EventEmitter {
let added = [];
let addedIndices = [];
let hasDataBeforeStartBound = false;
let size = this.options.size;
let enforceSize = size !== undefined && this.options.enforceSize;
// loop through, sort and dedupe
for (let datum of data) {
@@ -276,13 +274,6 @@ export default class TelemetryCollection extends EventEmitter {
}
} else {
this.emit('add', added, addedIndices);
if (enforceSize && this.boundedTelemetry.length > size) {
const removeCount = this.boundedTelemetry.length - size;
const removed = this.boundedTelemetry.splice(0, removeCount);
this.emit('remove', removed);
}
}
}
}

View File

@@ -61,7 +61,10 @@ export default function installWorker() {
this.#isConnecting = true;
this.#webSocket = new WebSocket(url);
//TODO: Make this configurable
this.#webSocket = new WebSocket(url, 'protobuf');
//TODO: Make this configurable
this.#webSocket.binaryType = 'arraybuffer';
const boundConnected = this.#connected.bind(this);
this.#webSocket.addEventListener('open', boundConnected);
@@ -249,10 +252,15 @@ export default function installWorker() {
if (this.#messageBatcher.shouldBatchMessage(data)) {
this.#messageBatcher.addMessageToBatch(data);
} else {
this.#worker.postMessage({
type: 'message',
message: data
});
this.#worker.postMessage(
{
type: 'message',
message: data
},
{
transfer: data
}
);
}
}
}
@@ -267,11 +275,13 @@ export default function installWorker() {
#maxBatchSize;
#readyForNextBatch;
#worker;
#transferables;
constructor(worker) {
this.#maxBatchSize = 10;
this.#readyForNextBatch = false;
this.#worker = worker;
this.#transferables = [];
this.#resetBatch();
}
#resetBatch() {
@@ -323,6 +333,7 @@ export default function installWorker() {
} else {
this.#hasBatch = true;
}
this.#transferables.push(message);
}
setMaxBatchSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize;
@@ -343,12 +354,18 @@ export default function installWorker() {
#sendNextBatch() {
const batch = this.#batch;
this.#resetBatch();
this.#worker.postMessage({
type: 'batch',
batch
});
this.#worker.postMessage(
{
type: 'batch',
batch
},
{
transfer: this.#transferables
}
);
this.#readyForNextBatch = false;
this.#hasBatch = false;
this.#transferables = [];
}
}

View File

@@ -32,7 +32,6 @@ export default class StatusAPI extends EventEmitter {
this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
this.onMissionActionStatusChange = this.onMissionActionStatusChange.bind(this);
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
this.#openmct.once('destroy', () => {
@@ -41,7 +40,6 @@ export default class StatusAPI extends EventEmitter {
if (typeof provider?.off === 'function') {
provider.off('statusChange', this.onProviderStatusChange);
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
provider.off('missionActionStatusChange', this.onMissionActionStatusChange);
}
});
@@ -102,67 +100,6 @@ export default class StatusAPI extends EventEmitter {
}
}
/**
* Can the currently logged in user set the mission status.
* @returns {Promise<Boolean>} true if the currently logged in user can set the mission status, false otherwise.
*/
canSetMissionStatus() {
const provider = this.#userAPI.getProvider();
if (provider.canSetMissionStatus) {
return provider.canSetMissionStatus();
} else {
return Promise.resolve(false);
}
}
/**
* Fetch the current status for the given mission action
* @param {MissionAction} action
* @returns {string}
*/
getStatusForMissionAction(action) {
const provider = this.#userAPI.getProvider();
if (provider.getStatusForMissionAction) {
return provider.getStatusForMissionAction(action);
} else {
this.#userAPI.error('User provider does not support getting mission action status');
}
}
/**
* Fetch the list of possible mission status options (GO, NO-GO, etc.)
* @returns {Promise<MissionStatusOption[]>} the complete list of possible mission statuses
*/
async getPossibleMissionActionStatuses() {
const provider = this.#userAPI.getProvider();
if (provider.getPossibleMissionActionStatuses) {
const possibleOptions = await provider.getPossibleMissionActionStatuses();
return possibleOptions;
} else {
this.#userAPI.error('User provider does not support mission status options');
}
}
/**
* Fetch the list of possible mission actions
* @returns {Promise<string[]>} the list of possible mission actions
*/
async getPossibleMissionActions() {
const provider = this.#userAPI.getProvider();
if (provider.getPossibleMissionActions) {
const possibleActions = await provider.getPossibleMissionActions();
return possibleActions;
} else {
this.#userAPI.error('User provider does not support mission statuses');
}
}
/**
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
*/
@@ -229,21 +166,6 @@ export default class StatusAPI extends EventEmitter {
}
}
/**
* @param {MissionAction} action
* @param {MissionStatusOption} status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForMissionAction(action, status) {
const provider = this.#userAPI.getProvider();
if (provider.setStatusForMissionAction) {
return provider.setStatusForMissionAction(action, status);
} else {
this.#userAPI.error('User provider does not support setting mission role status');
}
}
/**
* Resets the status of the provided role back to its default status.
* @param {import("./UserAPI").Role} role The role to set the status for.
@@ -323,7 +245,6 @@ export default class StatusAPI extends EventEmitter {
if (typeof provider.on === 'function') {
provider.on('statusChange', this.onProviderStatusChange);
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
provider.on('missionActionStatusChange', this.onMissionActionStatusChange);
}
}
@@ -340,23 +261,14 @@ export default class StatusAPI extends EventEmitter {
onProviderPollQuestionChange(pollQuestion) {
this.emit('pollQuestionChange', pollQuestion);
}
/**
* @private
*/
onMissionActionStatusChange({ action, status }) {
this.emit('missionActionStatusChange', { action, status });
}
}
/**
* @typedef {import('./UserProvider')} UserProvider
*/
/**
* @typedef {import('./StatusUserProvider')} StatusUserProvider
*/
/**
* The PollQuestion type
* @typedef {Object} PollQuestion
@@ -364,19 +276,6 @@ export default class StatusAPI extends EventEmitter {
* @property {Number} timestamp - The time that the poll question was set.
*/
/**
* The MissionStatus type
* @typedef {Object} MissionStatusOption
* @extends {Status}
* @property {String} color A color to be used when displaying the mission status
*/
/**
* @typedef {Object} MissionAction
* @property {String} key A unique identifier for this action
* @property {String} label A human readable label for this action
*/
/**
* The Status type
* @typedef {Object} Status

View File

@@ -23,12 +23,12 @@ import UserProvider from './UserProvider.js';
export default class StatusUserProvider extends UserProvider {
/**
* @param {('statusChange'|'pollQuestionChange'|'missionActionStatusChange')} event the name of the event to listen to
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
* @param {Function} callback a function to invoke when this event occurs
*/
on(event, callback) {}
/**
* @param {('statusChange'|'pollQuestionChange'|'missionActionStatusChange')} event the name of the event to stop listen to
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
* @param {Function} callback the callback function used to register the listener
*/
off(event, callback) {}

View File

@@ -24,6 +24,9 @@ import ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvide
import { createOpenMct, resetApplicationState } from '../../utils/testing.js';
import { MULTIPLE_PROVIDER_ERROR } from './constants.js';
const USERNAME = 'Test User';
const EXAMPLE_ROLE = 'flight';
describe('The User API', () => {
let openmct;
@@ -62,4 +65,48 @@ describe('The User API', () => {
expect(openmct.user.hasProvider()).toBeTrue();
});
});
describe('provides the ability', () => {
let provider;
beforeEach(() => {
provider = new ExampleUserProvider(openmct);
provider.autoLogin(USERNAME);
});
it('to check if a user (not specific) is logged in', (done) => {
expect(openmct.user.isLoggedIn()).toBeFalse();
openmct.user.on('providerAdded', () => {
expect(openmct.user.isLoggedIn()).toBeTrue();
done();
});
// this will trigger the user indicator plugin,
// which will in turn login the user
openmct.user.setProvider(provider);
});
it('to get the current user', (done) => {
openmct.user.setProvider(provider);
openmct.user
.getCurrentUser()
.then((apiUser) => {
expect(apiUser.name).toEqual(USERNAME);
})
.finally(done);
});
it('to check if a user has a specific role (by id)', (done) => {
openmct.user.setProvider(provider);
let junkIdCheckPromise = openmct.user.hasRole('junk-id').then((hasRole) => {
expect(hasRole).toBeFalse();
});
let realIdCheckPromise = openmct.user.hasRole(EXAMPLE_ROLE).then((hasRole) => {
expect(hasRole).toBeTrue();
});
Promise.all([junkIdCheckPromise, realIdCheckPromise]).finally(done);
});
});
});

View File

@@ -38,6 +38,7 @@ describe('the plugin', function () {
let couchPlugin = openmct.plugins.CouchDB(testPath);
openmct.install(couchPlugin);
openmct.install(
new CouchDBSearchFolderPlugin('CouchDB Documents', couchPlugin, {
selector: {

View File

@@ -46,14 +46,14 @@ describe('DeviceMatchers', function () {
return 'is' + deviceType[0].toUpperCase() + deviceType.slice(1);
}
['mobile', 'phone', 'tablet', 'landscape', 'portrait', 'landscape', 'touch'].forEach(
function (deviceType) {
it('detects when a device is a ' + deviceType + ' device', function () {
mockAgent[method(deviceType)].and.returnValue(true);
expect(DeviceMatchers[deviceType](mockAgent)).toBe(true);
mockAgent[method(deviceType)].and.returnValue(false);
expect(DeviceMatchers[deviceType](mockAgent)).toBe(false);
});
}
);
['mobile', 'phone', 'tablet', 'landscape', 'portrait', 'landscape', 'touch'].forEach(function (
deviceType
) {
it('detects when a device is a ' + deviceType + ' device', function () {
mockAgent[method(deviceType)].and.returnValue(true);
expect(DeviceMatchers[deviceType](mockAgent)).toBe(true);
mockAgent[method(deviceType)].and.returnValue(false);
expect(DeviceMatchers[deviceType](mockAgent)).toBe(false);
});
});
});

View File

@@ -1,68 +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 { ACTIVITY_STATES_KEY } from './createActivityStatesIdentifier.js';
/**
* @typedef {object} ActivityStatesInterceptorOptions
* @property {import('../../api/objects/ObjectAPI').Identifier} identifier the {namespace, key} to use for the activity states object.
* @property {string} name The name of the activity states model.
* @property {number} priority the priority of the interceptor. By default, it is low.
*/
/**
* Creates an activity states object in the persistence store. This is used to save plan activity states.
* This will only get invoked when an attempt is made to save the state for an activity and no activity states object exists in the store.
* @param {import('../../../openmct').OpenMCT} openmct
* @param {ActivityStatesInterceptorOptions} options
* @returns {object}
*/
const ACTIVITY_STATES_TYPE = 'activity-states';
function activityStatesInterceptor(openmct, options) {
const { identifier, name, priority = openmct.priority.LOW } = options;
const activityStatesModel = {
identifier,
name,
type: ACTIVITY_STATES_TYPE,
activities: {},
location: null
};
return {
appliesTo: (identifierObject) => {
return identifierObject.key === ACTIVITY_STATES_KEY;
},
invoke: (identifierObject, object) => {
if (!object || openmct.objects.isMissing(object)) {
openmct.objects.save(activityStatesModel);
return activityStatesModel;
}
return object;
},
priority
};
}
export default activityStatesInterceptor;

View File

@@ -1,30 +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.
*****************************************************************************/
export const ACTIVITY_STATES_KEY = 'activity-states';
export function createActivityStatesIdentifier(namespace = '') {
return {
key: ACTIVITY_STATES_KEY,
namespace
};
}

View File

@@ -1,89 +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 { createOpenMct, resetApplicationState } from 'utils/testing';
import {
ACTIVITY_STATES_KEY,
createActivityStatesIdentifier
} from './createActivityStatesIdentifier.js';
const MISSING_NAME = `Missing: ${ACTIVITY_STATES_KEY}`;
const DEFAULT_NAME = 'Activity States';
const activityStatesIdentifier = createActivityStatesIdentifier();
describe('the plugin', () => {
let openmct;
let missingObj = {
identifier: activityStatesIdentifier,
type: 'unknown',
name: MISSING_NAME
};
describe('with no arguments passed in', () => {
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(openmct.plugins.PlanLayout());
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('when installed, adds "Activity States"', async () => {
const activityStatesObject = await openmct.objects.get(activityStatesIdentifier);
expect(activityStatesObject.name).toBe(DEFAULT_NAME);
expect(activityStatesObject).toBeDefined();
});
describe('adds an interceptor that returns a "Activity States" model for', () => {
let activityStatesObject;
let mockNotFoundProvider;
let activeProvider;
beforeEach(async () => {
mockNotFoundProvider = {
get: () => Promise.reject(new Error('Not found')),
create: () => Promise.resolve(missingObj),
update: () => Promise.resolve(missingObj)
};
activeProvider = mockNotFoundProvider;
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
activityStatesObject = await openmct.objects.get(activityStatesIdentifier);
});
it('missing objects', () => {
let idsMatch = openmct.objects.areIdsEqual(
activityStatesObject.identifier,
activityStatesIdentifier
);
expect(activityStatesObject).toBeDefined();
expect(idsMatch).toBeTrue();
});
});
});
});

View File

@@ -84,7 +84,12 @@ import LayoutFrame from './LayoutFrame.vue';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1];
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
const CONTEXT_MENU_ACTIONS = [
'copyToClipboard',
'copyToNotebook',
'viewHistoricalData',
'renderWhenVisible'
];
export default {
makeDefinition(openmct, gridSize, domainObject, position) {

View File

@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="l-grid-view" role="grid" :aria-label="`${domainObject.name} Grid View`">
<div class="l-grid-view" role="grid">
<grid-item
v-for="(item, index) in items"
:key="index"
@@ -38,6 +38,6 @@ import GridItem from './GridItem.vue';
export default {
components: { GridItem },
mixins: [compositionLoader],
inject: ['openmct', 'domainObject']
inject: ['openmct']
};
</script>

View File

@@ -118,7 +118,7 @@
&__metadata {
color: $colorItemFgDetails;
//font-size: 0.9em;
font-size: 0.9em;
body.mobile & {
[class*='__item-count'] {

View File

@@ -1,7 +1,5 @@
/******************************* LIST ITEM */
.c-list-item {
color: $colorItemFgDetails;
&__name__type-icon {
color: $colorItemTreeIcon;
}
@@ -10,12 +8,12 @@
@include ellipsize();
a & {
// .c-list-item_name a element
color: $colorItemFg;
}
}
&:not(.c-list-item__name) {
color: $colorItemFgDetails;
}
&.is-alias {

View File

@@ -145,7 +145,7 @@
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
class="c-imagery__age icon-check c-imagery--new no-animation"
>
ROV
POS
</div>
<!-- camera position fresh -->

View File

@@ -112,7 +112,7 @@ export default {
},
renderPlot(plotObject) {
const wrapper = document.createElement('div');
const visibilityObserver = new VisibilityObserver(wrapper, this.openmct.element);
const visibilityObserver = new VisibilityObserver(wrapper);
const { destroy } = mount(
{

View File

@@ -1,23 +1,22 @@
.c-path,
.c-location {
// Path is two or more items, not clickable
// Location used in Inspector and search results, is clickable
// Location used in Inspector, is clickable
display: flex;
&__item {
display: flex;
font-size: 11px;
align-items: center;
min-width: 0;
&:not(:last-child) {
&:after {
// Right-pointing arrow
color: $colorBodyFgSubtle;
// color: $colorInspectorPropName;
content: $glyph-icon-arrow-right;
font-family: symbolsfont;
font-size: 0.7em;
margin-left: $interiorMarginSm;
opacity: 0.8;
}
}
}

View File

@@ -74,10 +74,6 @@ export default class LocalStorageObjectProvider {
this.localStorage.setItem(this.spaceKey, JSON.stringify(space));
}
isReadOnly() {
return false;
}
/**
* @private
*/

View File

@@ -1,26 +1,8 @@
# My Items plugin
Defines top-level folder named "My Items" to store user-created items. Enabled by default, this can be disabled in a read-only deployment with no user-editable objects.
Defines top-level folder named "My Items" to store user-created items. Enabled by default, this can be disabled in a
read-only deployment with no user-editable objects.
## Installation
```js
openmct.install(openmct.plugins.MyItems());
```
## Options
When installing, the plugin can take several options:
- `name`: The label of the root object. Defaults to "My Items"
- Example: `'Apple Items'`
- `namespace`: The namespace to create the root object in. Defaults to the empty string `''`
- Example: `'apple-namespace'`
- `priority`: The optional priority to install this plugin. Defaults to `openmct.priority.LOW`
- Example: `'openmct.priority.LOW'`
E.g., to install with a custom name and namespace, you could use:
```js
openmct.install(openmct.plugins.MyItems('Apple Items', 'apple-namespace'));
```

View File

@@ -20,7 +20,9 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
function myItemsInterceptor({ openmct, identifierObject, name }) {
import { MY_ITEMS_KEY } from './createMyItemsIdentifier.js';
function myItemsInterceptor(openmct, identifierObject, name) {
const myItemsModel = {
identifier: identifierObject,
name,
@@ -31,10 +33,7 @@ function myItemsInterceptor({ openmct, identifierObject, name }) {
return {
appliesTo: (identifier) => {
return (
identifier.key === myItemsModel.identifier.key &&
identifier.namespace === myItemsModel.identifier.namespace
);
return identifier.key === MY_ITEMS_KEY;
},
invoke: (identifier, object) => {
if (!object || openmct.objects.isMissing(object)) {

View File

@@ -31,13 +31,13 @@ export default function MyItemsPlugin(
priority = undefined
) {
return function install(openmct) {
const identifierObject = createMyItemsIdentifier(namespace);
const identifier = createMyItemsIdentifier(namespace);
if (priority === undefined) {
priority = openmct.priority.LOW;
}
openmct.objects.addGetInterceptor(myItemsInterceptor({ openmct, identifierObject, name }));
openmct.objects.addRoot(identifierObject, priority);
openmct.objects.addGetInterceptor(myItemsInterceptor(openmct, identifier, name));
openmct.objects.addRoot(identifier, priority);
};
}

View File

@@ -96,7 +96,7 @@
</div>
<div
v-if="selectedPage && !selectedPage.isLocked"
:aria-disabled="activeTransaction"
:class="{ disabled: activeTransaction }"
class="c-notebook__drag-area icon-plus"
@click="newEntry(null, $event)"
@dragover="dragOver"

View File

@@ -20,7 +20,11 @@
at runtime from the About dialog for additional information.
-->
<template>
<div :style="position" class="c-status-poll-panel c-status-poll-panel--operator" @click.stop>
<div
:style="position"
class="c-status-poll-panel c-status-poll-panel--operator"
@click.stop="noop"
>
<div class="c-status-poll-panel__section c-status-poll-panel__top">
<div class="c-status-poll-panel__title">Status Poll</div>
<div class="c-status-poll-panel__user-role icon-person">{{ role }}</div>
@@ -187,7 +191,8 @@ export default {
} else {
return status;
}
}
},
noop() {}
}
};
</script>

View File

@@ -80,7 +80,7 @@
keepAliveTimer = setTimeout(self.listenForChanges, keepAliveTime);
if (!couchEventSource || couchEventSource.readyState === EventSource.CLOSED) {
console.debug(`⇿ Opening CouchDB change feed connection for ${changesFeedUrl}`);
console.debug('⇿ Opening CouchDB change feed connection ⇿');
couchEventSource = new EventSource(changesFeedUrl);
couchEventSource.onerror = self.onerror;
couchEventSource.onopen = self.onopen;
@@ -88,7 +88,7 @@
// start listening for events
couchEventSource.addEventListener('message', self.onCouchMessage);
connected = true;
console.debug(`⇿ Opened connection to ${changesFeedUrl}`);
console.debug('⇿ Opened connection ⇿');
}
};

View File

@@ -33,13 +33,13 @@ const HEARTBEAT = 50000;
const ALL_DOCS = '_all_docs?include_docs=true';
class CouchObjectProvider {
constructor({ openmct, databaseConfiguration, couchStatusIndicator }) {
constructor(openmct, options, namespace, indicator) {
options = this.#normalize(options);
this.openmct = openmct;
this.indicator = couchStatusIndicator;
this.url = databaseConfiguration.url;
this.readOnly = databaseConfiguration.readOnly;
this.useDesignDocuments = databaseConfiguration.useDesignDocuments;
this.namespace = databaseConfiguration.namespace;
this.indicator = indicator;
this.url = options.url;
this.useDesignDocuments = options.useDesignDocuments;
this.namespace = namespace;
this.objectQueue = {};
this.observers = {};
this.batchIds = [];
@@ -47,7 +47,6 @@ class CouchObjectProvider {
this.onEventError = this.onEventError.bind(this);
this.flushPersistenceQueue = _.debounce(this.flushPersistenceQueue.bind(this));
this.persistenceQueue = [];
this.rootObject = null;
}
/**
@@ -60,10 +59,7 @@ class CouchObjectProvider {
// eslint-disable-next-line no-undef
const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`;
sharedWorker = new SharedWorker(
sharedWorkerURL,
`CouchDB SSE Shared Worker for ${this.namespace}`
);
sharedWorker = new SharedWorker(sharedWorkerURL, 'CouchDB SSE Shared Worker');
sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this);
sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this);
sharedWorker.port.start();
@@ -97,7 +93,7 @@ class CouchObjectProvider {
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
} else if (event.data.type === 'state') {
const state = this.#messageToIndicatorState(event.data.state);
this.indicator?.setIndicatorToState(state);
this.indicator.setIndicatorToState(state);
} else {
let objectChanges = event.data.objectChanges;
const objectIdentifier = {
@@ -188,8 +184,16 @@ class CouchObjectProvider {
return state;
}
isReadOnly() {
return this.readOnly;
//backwards compatibility, options used to be a url. Now it's an object
#normalize(options) {
if (typeof options === 'string') {
return {
url: options,
useDesignDocuments: false
};
}
return options;
}
async request(subPath, method, body, signal) {
@@ -229,7 +233,7 @@ class CouchObjectProvider {
// Network error, CouchDB unreachable.
if (response === null) {
this.indicator?.setIndicatorToState(DISCONNECTED);
this.indicator.setIndicatorToState(DISCONNECTED);
console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
@@ -252,7 +256,7 @@ class CouchObjectProvider {
* @private
*/
#handleResponseCode(status, json, fetchOptions) {
this.indicator?.setIndicatorToState(this.#statusCodeToIndicatorState(status));
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
if (status === CouchObjectProvider.HTTP_CONFLICT) {
const objectName = JSON.parse(fetchOptions.body)?.model?.name;
throw new this.openmct.objects.errors.Conflict(`Conflict persisting "${objectName}"`);
@@ -680,7 +684,7 @@ class CouchObjectProvider {
}
const indicatorState = this.#messageToIndicatorState(message);
this.indicator?.setIndicatorToState(indicatorState);
this.indicator.setIndicatorToState(indicatorState);
}
/**

View File

@@ -51,10 +51,6 @@ class CouchSearchProvider {
return this.supportedSearchTypes.includes(searchType);
}
isReadOnly() {
return true;
}
search(query, abortSignal, searchType) {
if (searchType === this.searchTypes.OBJECTS) {
return this.searchForObjects(query, abortSignal);

View File

@@ -153,63 +153,13 @@ sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.s
Add a line to install the CouchDB plugin for Open MCT:
```js
openmct.install(
openmct.plugins.CouchDB({
databases: [
{
url: 'http://localhost:5984/openmct',
namespace: '',
additionalNamespaces: [],
readOnly: false,
useDesignDocuments: false,
indicator: true
}
]
})
);
openmct.install(openmct.plugins.CouchDB({url: "http://localhost:5984/openmct", useDesignDocuments: false}));
```
### Configuration Options for OpenMCT
When installing the CouchDB plugin for OpenMCT, you can specify a list of databases with configuration options for each. Here's a breakdown of the available options for each database:
- `url`: The URL to the CouchDB instance, specifying the protocol, hostname, and port as needed.
- Example: `'http://localhost:5984/openmct'`
- `namespace`: The namespace associated with this database.
- Example: `'openmct-sandbox'`
- `additionalNamespaces`: Other namespaces that this plugin should respond to requests for.
- Example: `['apple-namespace', 'pear-namespace']`
- `readOnly`: A boolean indicating whether the database should be treated as read-only. If set to `true`, OpenMCT will not attempt to write to this database.
- Example: `false`
- `useDesignDocuments`: Indicates whether design documents should be used to speed up annotation search.
- Example: `false`
- `indicator`: A boolean to specify whether an indicator should show the status of this CouchDB connection in the OpenMCT interface.
- Example: `true`
Note: If using the `exampleTags` plugin with non-blank namespaces, you'll need to configure it point to a writable database. For example:
```js
openmct.install(
openmct.plugins.example.ExampleTags({ namespaceToSaveAnnotations: 'openmct-sandbox' })
);
```
Note: If using the `MyItems` plugin, be sure to configure a root for each writable namespace. E.g., if you have two namespaces called `apple-namespace` and `pear-namespace`:
```js
openmct.install(openmct.plugins.MyItems('Apple Items', 'apple-namespace'));
openmct.install(openmct.plugins.MyItems('Pear Items', 'pear-namespace'));
```
This will create a root object with the id of `mine` in both namespaces upon load if not already created.
# Validating a successful Installation
1. Start Open MCT by running `npm start` in the `openmct` path.
2. Navigate to <http://localhost:8080/> and create a random object in Open MCT (e.g., a 'Clock') and save.
2. Navigate to <http://localhost:8080/> and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs>
4. Look at the 'JSON' tab and ensure you can see the specific object you created above.
5. All done! 🏆
@@ -292,4 +242,4 @@ To enable them in Open MCT, we need to configure the plugin `useDesignDocuments`
```js
openmct.install(openmct.plugins.CouchDB({url: "http://localhost:5984/openmct", useDesignDocuments: true}));
```
```

View File

@@ -24,81 +24,29 @@ import CouchObjectProvider from './CouchObjectProvider.js';
import CouchSearchProvider from './CouchSearchProvider.js';
import CouchStatusIndicator from './CouchStatusIndicator.js';
const DEFAULT_NAMESPACE = '';
const NAMESPACE = '';
const LEGACY_SPACE = 'mct';
const COUCH_SEARCH_ONLY_NAMESPACE = `COUCH_SEARCH_${Date.now()}`;
export default function CouchPlugin(options) {
function normalizeOptions(unnormalizedOptions) {
const normalizedOptions = {};
if (typeof unnormalizedOptions === 'string') {
normalizedOptions.databases = [
{
url: options,
namespace: DEFAULT_NAMESPACE,
additionalNamespaces: [LEGACY_SPACE],
readOnly: false,
useDesignDocuments: false,
indicator: true
}
];
} else if (!unnormalizedOptions.databases) {
normalizedOptions.databases = [
{
url: unnormalizedOptions.url,
namespace: DEFAULT_NAMESPACE,
additionalNamespaces: [LEGACY_SPACE],
readOnly: false,
useDesignDocuments: unnormalizedOptions.useDesignDocuments,
indicator: true
}
];
} else {
normalizedOptions.databases = unnormalizedOptions.databases;
}
// final sanity check, ensure we have all options
normalizedOptions.databases.forEach((databaseConfiguration) => {
if (!databaseConfiguration.url) {
throw new Error(
`🛑 CouchDB plugin requires a url option. Please check the configuration for namespace ${databaseConfiguration.namespace}`
);
} else if (databaseConfiguration.namespace === undefined) {
// note we can't check for just !databaseConfiguration.namespace because it could be an empty string
throw new Error(
`🛑 CouchDB plugin requires a namespace option. Please check the configuration for url ${databaseConfiguration.url}`
);
}
});
return normalizedOptions;
}
return function install(openmct) {
const normalizedOptions = normalizeOptions(options);
normalizedOptions.databases.forEach((databaseConfiguration) => {
let couchStatusIndicator;
if (databaseConfiguration.indicator) {
const simpleIndicator = openmct.indicators.simpleIndicator();
openmct.indicators.add(simpleIndicator);
couchStatusIndicator = new CouchStatusIndicator(simpleIndicator);
}
// the provider is added to the install function to expose couchProvider to unit tests
install.couchProvider = new CouchObjectProvider({
openmct,
databaseConfiguration,
couchStatusIndicator
});
openmct.objects.addProvider(databaseConfiguration.namespace, install.couchProvider);
databaseConfiguration.additionalNamespaces?.forEach((additionalNamespace) => {
openmct.objects.addProvider(additionalNamespace, install.couchProvider);
});
const simpleIndicator = openmct.indicators.simpleIndicator();
openmct.indicators.add(simpleIndicator);
const couchStatusIndicator = new CouchStatusIndicator(simpleIndicator);
install.couchProvider = new CouchObjectProvider(
openmct,
options,
NAMESPACE,
couchStatusIndicator
);
// need one search provider for whole couch database
const searchOnlyNamespace = `COUCH_SEARCH_${databaseConfiguration.namespace}${Date.now()}`;
openmct.objects.addProvider(
searchOnlyNamespace,
new CouchSearchProvider(install.couchProvider)
);
});
// Unfortunately, for historical reasons, Couch DB produces objects with a mix of namespaces (alternately "mct", and "")
// Installing the same provider under both namespaces means that it can respond to object gets for both namespaces.
openmct.objects.addProvider(LEGACY_SPACE, install.couchProvider);
openmct.objects.addProvider(NAMESPACE, install.couchProvider);
openmct.objects.addProvider(
COUCH_SEARCH_ONLY_NAMESPACE,
new CouchSearchProvider(install.couchProvider)
);
};
}

View File

@@ -135,7 +135,6 @@ export default {
default: 22
}
},
emits: ['activity-selected'],
data() {
return {
lineHeight: 10
@@ -143,11 +142,30 @@ export default {
},
methods: {
setSelectionForActivity(activity, event) {
const element = event.currentTarget;
const multiSelect = event.metaKey;
event.stopPropagation();
this.$emit('activity-selected', {
event,
selection: activity.selection
});
this.openmct.selection.select(
[
{
element: element,
context: {
type: 'activity',
activity: activity
}
},
{
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject,
supportsMultiSelect: true
}
}
],
multiSelect
);
}
}
};

View File

@@ -47,7 +47,6 @@
:width="group.width"
:is-nested="options.isChildObject"
:status="status"
@activity-selected="selectActivity"
/>
</div>
</div>
@@ -135,7 +134,7 @@ export default {
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
this.clipActivityNames = this.configuration.clipActivityNames;
if (this.domainObject.type === 'plan') {
this.setPlanData(this.domainObject);
this.planData = getValidatedData(this.domainObject);
}
const canvas = document.createElement('canvas');
@@ -178,9 +177,6 @@ export default {
this.planViewConfiguration.destroy();
},
methods: {
setPlanData(domainObject) {
this.planData = getValidatedData(domainObject);
},
activityNameFitsRect(activityName, rectWidth) {
return this.getTextWidth(activityName) + TEXT_LEFT_PADDING < rectWidth;
},
@@ -219,7 +215,9 @@ export default {
callback: () => {
this.removeFromComposition(this.planObject);
this.planObject = domainObject;
this.handleSelectFileChange();
this.planData = getValidatedData(domainObject);
this.setStatus(this.openmct.status.get(domainObject.identifier));
this.setScaleAndGenerateActivities();
dialog.dismiss();
}
},
@@ -239,7 +237,9 @@ export default {
} else {
this.planObject = domainObject;
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
this.handleSelectFileChange(domainObject);
this.planData = getValidatedData(domainObject);
this.setStatus(this.openmct.status.get(domainObject.identifier));
this.setScaleAndGenerateActivities();
}
},
handleConfigurationChange(newConfiguration) {
@@ -259,10 +259,8 @@ export default {
this.setScaleAndGenerateActivities();
},
handleSelectFileChange(domainObject) {
const planDomainObject = domainObject || this.domainObject;
this.setPlanData(planDomainObject);
this.setStatus(this.openmct.status.get(planDomainObject.identifier));
handleSelectFileChange() {
this.planData = getValidatedData(this.domainObject);
this.setScaleAndGenerateActivities();
},
removeFromComposition(domainObject) {
@@ -436,7 +434,7 @@ export default {
return;
}
rawActivities.forEach((rawActivity, index) => {
rawActivities.forEach((rawActivity) => {
if (!this.isActivityInBounds(rawActivity)) {
return;
}
@@ -483,10 +481,13 @@ export default {
const activity = {
color: color,
textColor: textColor,
name: rawActivity.name,
exceeds: {
start: this.xScale(this.viewBounds.start) > this.xScale(rawActivity.start),
end: this.xScale(this.viewBounds.end) < this.xScale(rawActivity.end)
},
start: rawActivity.start,
end: rawActivity.end,
row: currentRow,
textLines: textLines,
textStart: textStart,
@@ -495,11 +496,7 @@ export default {
rectStart: rectX1,
rectEnd: showTextInsideRect ? rectX2 : textStart + textWidth,
rectWidth: rectWidth,
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow),
selection: {
groupName,
index
}
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow)
};
activitiesByRow[currentRow].push(activity);
});
@@ -576,31 +573,6 @@ export default {
const activityName = activity.name.toLowerCase().replace(/ /g, '-');
return `${groupName}-${activityName}-${activity.start}-${activity.end}-${row}`;
},
selectActivity({ event, selection }) {
const element = event.currentTarget;
const multiSelect = event.metaKey;
const { groupName, index } = selection;
const rawActivity = this.planData[groupName][index];
this.openmct.selection.select(
[
{
element: element,
context: {
type: 'activity',
activity: rawActivity
}
},
{
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject,
supportsMultiSelect: true
}
}
],
multiSelect
);
}
}
};

View File

@@ -20,35 +20,21 @@
at runtime from the About dialog for additional information.
-->
<template>
<plan-activity-time-view
v-for="activity in activities"
:key="activity.key"
:activity="activity"
:heading="heading"
/>
<plan-activity-properties-view
v-for="activity in activities"
:key="activity.key"
heading="Properties"
:activity="activity"
/>
<plan-activity-status-view
v-if="canPersistState"
:key="activities[0].key"
:activity="activities[0]"
:execution-state="activityExecutionState"
heading="Activity Status"
@update-activity-state="persistActivityState"
/>
<div class="c-inspector__properties c-inspect-properties">
<plan-activity-view
v-for="activity in activities"
:key="activity.id"
:activity="activity"
:heading="heading"
/>
</div>
</template>
<script>
import { getPreciseDuration } from 'utils/duration';
import { v4 as uuid } from 'uuid';
import { getDisplayProperties } from '../../util.js';
import PlanActivityPropertiesView from './PlanActivityPropertiesView.vue';
import PlanActivityStatusView from './PlanActivityStatusView.vue';
import PlanActivityTimeView from './PlanActivityTimeView.vue';
import PlanActivityView from './PlanActivityView.vue';
const propertyLabels = {
start: 'Start DateTime',
@@ -58,34 +44,23 @@ const propertyLabels = {
latestEnd: 'Latest End',
gap: 'Gap',
overlap: 'Overlap',
totalTime: 'Total Time',
description: 'Description'
totalTime: 'Total Time'
};
export default {
components: {
PlanActivityTimeView,
PlanActivityPropertiesView,
PlanActivityStatusView
PlanActivityView
},
inject: ['openmct', 'selection'],
data() {
return {
name: '',
activities: [],
selectedActivities: [],
activityExecutionState: undefined,
heading: ''
};
},
computed: {
canPersistState() {
return this.selectedActivities.length === 1 && this.activities?.[0]?.id;
}
},
mounted() {
this.setFormatters();
this.getPlanData(this.selection);
this.getActivityStates();
this.getActivities();
this.openmct.selection.on('change', this.updateSelection);
this.openmct.time.on('timeSystem', this.setFormatters);
@@ -93,28 +68,8 @@ export default {
beforeUnmount() {
this.openmct.selection.off('change', this.updateSelection);
this.openmct.time.off('timeSystem', this.setFormatters);
if (this.stopObservingActivityStatesObject) {
this.stopObservingActivityStatesObject();
}
},
methods: {
async getActivityStates() {
this.activityStatesObject = await this.openmct.objects.get('activity-states');
this.setActivityStates(this.activityStatesObject);
this.stopObservingActivityStatesObject = this.openmct.objects.observe(
this.activityStatesObject,
'*',
this.setActivityStates
);
},
setActivityStates(newActivitiesStateObject) {
if (this.activities.length) {
const id = this.activities[0].id;
this.activityExecutionState = newActivitiesStateObject.activities[id];
} else {
this.activityExecutionState = undefined;
}
},
setFormatters() {
let timeSystem = this.openmct.time.timeSystem();
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
@@ -129,9 +84,8 @@ export default {
this.selectedActivities = [];
selection.forEach((selectionItem) => {
if (selectionItem[0].context.type === 'activity') {
const activity = { ...selectionItem[0].context.activity };
const activity = selectionItem[0].context.activity;
if (activity) {
activity.key = activity.id ?? activity.name;
this.selectedActivities.push(activity);
}
}
@@ -150,37 +104,20 @@ export default {
this.activities.splice(0);
this.selectedActivities.forEach((selectedActivity, index) => {
const activity = {
id: selectedActivity.id,
key: selectedActivity.key,
timeProperties: {
start: {
label: propertyLabels.start,
value: this.formatTime(selectedActivity.start)
},
end: {
label: propertyLabels.end,
value: this.formatTime(selectedActivity.end)
},
duration: {
label: propertyLabels.duration,
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
}
id: uuid(),
start: {
label: propertyLabels.start,
value: this.formatTime(selectedActivity.start)
},
end: {
label: propertyLabels.end,
value: this.formatTime(selectedActivity.end)
},
duration: {
label: propertyLabels.duration,
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
}
};
activity.metadata = {};
if (selectedActivity.description) {
activity.metadata.description = {
label: propertyLabels.description,
value: selectedActivity.description
};
}
const displayProperties = getDisplayProperties(selectedActivity);
activity.metadata = {
...activity.metadata,
...displayProperties
};
this.activities[index] = activity;
});
},
@@ -204,8 +141,6 @@ export default {
let latestEnd;
let gap;
let overlap;
let id;
let key;
//Sort by start time
let selectedActivities = this.selectedActivities.sort(this.sortFn);
@@ -224,8 +159,6 @@ export default {
earliestStart = Math.min(earliestStart, selectedActivity.start);
latestEnd = Math.max(latestEnd, selectedActivity.end);
} else {
id = selectedActivity.id;
key = selectedActivity.id ?? selectedActivity.name;
earliestStart = selectedActivity.start;
latestEnd = selectedActivity.end;
}
@@ -233,33 +166,30 @@ export default {
let totalTime = latestEnd - earliestStart;
const activity = {
id,
key,
timeProperties: {
earliestStart: {
label: propertyLabels.earliestStart,
value: this.formatTime(earliestStart)
},
latestEnd: {
label: propertyLabels.latestEnd,
value: this.formatTime(latestEnd)
}
id: uuid(),
earliestStart: {
label: propertyLabels.earliestStart,
value: this.formatTime(earliestStart)
},
latestEnd: {
label: propertyLabels.latestEnd,
value: this.formatTime(latestEnd)
}
};
if (gap) {
activity.timeProperties.gap = {
activity.gap = {
label: propertyLabels.gap,
value: this.formatDuration(gap)
};
} else if (overlap) {
activity.timeProperties.overlap = {
activity.overlap = {
label: propertyLabels.overlap,
value: this.formatDuration(overlap)
};
}
activity.timeProperties.totalTime = {
activity.totalTime = {
label: propertyLabels.totalTime,
value: this.formatDuration(totalTime)
};
@@ -271,11 +201,6 @@ export default {
},
formatTime(time) {
return this.timeFormatter.format(time);
},
persistActivityState(data) {
const { key, executionState } = data;
const activitiesPath = `activities.${key}`;
this.openmct.objects.mutate(this.activityStatesObject, activitiesPath, executionState);
}
}
};

View File

@@ -1,81 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2023, 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-inspector__properties c-inspect-properties">
<div v-if="properties.length" class="u-contents">
<div class="c-inspect-properties__header">{{ heading }}</div>
<ul v-for="property in properties" :key="property.id" class="c-inspect-properties__section">
<activity-property :label="property.label" :value="property.value" />
</ul>
</div>
</div>
</template>
<script>
import ActivityProperty from './ActivityProperty.vue';
export default {
components: {
ActivityProperty
},
props: {
activity: {
type: Object,
required: true
},
heading: {
type: String,
required: true
}
},
data() {
return {
properties: []
};
},
mounted() {
this.setProperties();
},
methods: {
setProperties() {
if (!this.activity.metadata) {
return;
}
Object.keys(this.activity.metadata).forEach((key) => {
if (this.activity.metadata[key].label) {
const label = this.activity.metadata[key].label;
const value = String(this.activity.metadata[key].value);
const id = this.activity.id;
this.properties[this.properties.length] = {
id,
label,
value
};
}
});
}
}
};
</script>

View File

@@ -1,127 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2023, 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-inspector__properties c-inspect-properties">
<div class="u-contents">
<div class="c-inspect-properties__header">{{ heading }}</div>
<div class="c-inspect-properties__row">
<div class="c-inspect-properties__label" title="Set Status">Set Status</div>
<div class="c-inspect-properties__value" aria-label="Activity Status Label">
<select
v-model="currentStatusKey"
name="setActivityStatus"
aria-label="Activity Status"
@change="changeActivityStatus"
>
<option
v-for="status in activityStates"
:key="status.key"
:value="status.key"
:aria-selected="currentStatusKey === status.key"
>
{{ status.label }}
</option>
</select>
</div>
</div>
</div>
</div>
</template>
<script>
const activityStates = [
{
key: 'notStarted',
label: 'Not started'
},
{
key: 'in-progress',
label: 'In progress'
},
{
key: 'completed',
label: 'Completed'
},
{
key: 'aborted',
label: 'Aborted'
},
{
key: 'skipped',
label: 'Skipped'
}
];
export default {
props: {
activity: {
type: Object,
required: true
},
executionState: {
type: String,
default() {
return '';
}
},
heading: {
type: String,
required: true
}
},
emits: ['updateActivityState'],
data() {
return {
activityStates: activityStates,
currentStatusKey: activityStates[0].key
};
},
watch: {
executionState() {
this.setActivityStatus();
}
},
mounted() {
this.setActivityStatus();
},
methods: {
setActivityStatus() {
let statusKeyIndex = activityStates.findIndex((state) => state.key === this.executionState);
if (statusKeyIndex < 0) {
statusKeyIndex = 0;
}
this.currentStatusKey = this.activityStates[statusKeyIndex].key;
},
changeActivityStatus() {
if (this.currentStatusKey === '') {
return;
}
this.activity.executionState = this.currentStatusKey;
this.$emit('updateActivityState', {
key: this.activity.id,
executionState: this.currentStatusKey
});
}
}
};
</script>

View File

@@ -21,23 +21,23 @@
-->
<template>
<div class="c-inspector__properties c-inspect-properties">
<div v-if="timeProperties.length" class="u-contents">
<div class="c-inspect-properties__header">
{{ heading }}
</div>
<ul
v-for="timeProperty in timeProperties"
:key="timeProperty.id"
class="c-inspect-properties__section"
>
<activity-property :label="timeProperty.label" :value="timeProperty.value" />
</ul>
<div v-if="timeProperties.length" class="u-contents">
<div class="c-inspect-properties__header">
{{ heading }}
</div>
<ul
v-for="timeProperty in timeProperties"
:key="timeProperty.id"
class="c-inspect-properties__section"
>
<activity-property :label="timeProperty.label" :value="timeProperty.value" />
</ul>
</div>
</template>
<script>
import { v4 as uuid } from 'uuid';
import ActivityProperty from './ActivityProperty.vue';
export default {
@@ -64,14 +64,13 @@ export default {
},
methods: {
setProperties() {
Object.keys(this.activity.timeProperties).forEach((key) => {
if (this.activity.timeProperties[key].label) {
const label = this.activity.timeProperties[key].label;
const value = String(this.activity.timeProperties[key].value);
const id = this.activity.id;
Object.keys(this.activity).forEach((key) => {
if (this.activity[key].label) {
const label = this.activity[key].label;
const value = String(this.activity[key].value);
this.timeProperties[this.timeProperties.length] = {
id,
id: uuid(),
label,
value
};

View File

@@ -20,28 +20,12 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import activityStatesInterceptor from '../activityStates/activityStatesInterceptor.js';
import { createActivityStatesIdentifier } from '../activityStates/createActivityStatesIdentifier.js';
import ganttChartCompositionPolicy from './GanttChartCompositionPolicy.js';
import ActivityInspectorViewProvider from './inspector/ActivityInspectorViewProvider.js';
import GanttChartInspectorViewProvider from './inspector/GanttChartInspectorViewProvider.js';
import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration.js';
import PlanViewProvider from './PlanViewProvider.js';
const ACTIVITY_STATES_DEFAULT_NAME = 'Activity States';
/**
* @typedef {object} PlanOptions
* @property {boolean} creatable true/false to allow creation of a plan via the Create menu.
* @property {string} name The name of the activity states model.
* @property {string} namespace the namespace to use for the activity states object.
* @property {Number} priority the priority of the interceptor. By default, it is low.
*/
/**
*
* @param {PlanOptions} options
* @returns {*} (any)
*/
export default function (options = {}) {
return function install(openmct) {
openmct.types.addType('plan', {
@@ -86,13 +70,5 @@ export default function (options = {}) {
openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct));
openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct));
openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct));
//add activity states get interceptor
const { name = ACTIVITY_STATES_DEFAULT_NAME, namespace = '', priority } = options;
const identifier = createActivityStatesIdentifier(namespace);
openmct.objects.addGetInterceptor(
activityStatesInterceptor(openmct, { identifier, name, priority })
);
};
}

View File

@@ -20,20 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* The SourceMap allows mapping specific implementations of plan domain objects to those expected by Open MCT.
* @typedef {object} SourceMapOption
* @property {string} orderedGroups the property of the plan that lists groups/swim lanes specifying what order they will be displayed in Open MCT.
* @property {string} activities the property of the plan that has the list of activities to be displayed.
* @property {string} groupId the property of the activity that maps to the group/swim lane it should be displayed in.
* @property {string} start The start time property of the activity
* @property {string} end The end time property of the activity
* @property {string} id The unique id of the activity. This is required to allow setting activity states
* @property {object} displayProperties a list of key: value pairs that specifies which properties of the activity should be displayed when it is selected. Ex. {'location': 'Location', 'metadata.length_in_meters', 'Length (meters)'}
* @property {object} filterMetadata a list of strings that specifies which properties of the activity be included for filtering. Ex. {'description','properties.length_in_meters'}
*/
import _ from 'lodash';
export function getValidatedData(domainObject) {
const sourceMap = domainObject.sourceMap;
const json = getObjectJson(domainObject);
@@ -59,24 +45,6 @@ export function getValidatedData(domainObject) {
groupActivity.end = activity[sourceMap.end];
}
if (Array.isArray(sourceMap.filterMetadata)) {
groupActivity.filterMetadataValues = [];
sourceMap.filterMetadata.forEach((property) => {
const value = _.get(activity, property);
if (value !== undefined && value !== null) {
groupActivity.filterMetadataValues.push(value);
}
});
}
if (sourceMap.id) {
groupActivity.id = activity[sourceMap.id];
}
if (sourceMap.displayProperties) {
groupActivity.displayProperties = sourceMap.displayProperties;
}
if (!mappedJson[groupIdKey]) {
mappedJson[groupIdKey] = [];
}
@@ -124,6 +92,7 @@ export function getValidatedGroups(domainObject, planData) {
orderedGroupNames = groups;
}
}
if (orderedGroupNames === undefined) {
orderedGroupNames = Object.keys(planData);
}
@@ -131,37 +100,6 @@ export function getValidatedGroups(domainObject, planData) {
return orderedGroupNames;
}
export function getDisplayProperties(activity) {
let displayProperties = {};
function extractProperties(properties, useKeyAsLabel = false) {
Object.keys(properties).forEach((key) => {
const label = useKeyAsLabel ? key : properties[key];
const value = _.get(activity, key);
if (value) {
displayProperties[key] = { label, value };
}
});
}
if (activity?.displayProperties) {
extractProperties(activity.displayProperties);
} else if (activity?.properties) {
extractProperties(activity.properties, true);
}
return displayProperties;
}
export function getFilteredValues(activity) {
let values = [];
if (Array.isArray(activity.filterMetadataValues)) {
values = activity.filterMetadataValues;
} else if (activity?.properties) {
values = Object.values(activity.properties);
}
return values;
}
export function getContrastingColor(hexColor) {
function cutHex(h, start, end) {
const hStr = h.charAt(0) === '#' ? h.substring(1, 7) : h;

View File

@@ -22,20 +22,8 @@
<template>
<div ref="chart" class="gl-plot-chart-area">
<canvas
id="2dContext"
:style="canvasStyle"
class="js-overlay-canvas"
role="img"
aria-label="Overlay Canvas"
></canvas>
<canvas
id="webglContext"
:style="canvasStyle"
class="js-main-canvas"
role="img"
aria-label="Plot Canvas"
></canvas>
<canvas id="2dContext" :style="canvasStyle" class="js-overlay-canvas" role="img"></canvas>
<canvas id="webglContext" :style="canvasStyle" class="js-main-canvas" role="img"></canvas>
<div ref="limitArea" class="js-limit-area" aria-hidden="true">
<limit-label
v-for="(limitLabel, index) in visibleLimitLabels"
@@ -212,11 +200,7 @@ export default {
this.chartVisible = true;
this.chartContainer = this.$refs.chart;
this.drawnOnce = false;
const rootContainer = this.openmct.element;
const options = {
root: rootContainer
};
this.visibilityObserver = new IntersectionObserver(this.visibilityChanged, options);
this.visibilityObserver = new IntersectionObserver(this.visibilityChanged);
eventHelpers.extend(this);
this.seriesModels = [];
this.config = this.getConfig();
@@ -292,20 +276,13 @@ export default {
return config;
},
visibilityChanged([entry]) {
// Per https://github.com/nasa/openmct/issues/7405, we only want to draw when the chart is visible.
// and we need to use the Open MCT root element as the root of the intersection observer.
if (entry.target === this.chartContainer) {
const wasVisible = this.chartVisible;
const isNowVisible = entry.isIntersecting;
const chartInOverlayWindow = this.chartContainer?.closest('.js-overlay') !== null;
if (!isNowVisible && !chartInOverlayWindow) {
this.chartVisible = false;
this.chartVisible = entry.isIntersecting;
if (!this.chartVisible) {
// destroy the chart
this.destroyCanvas();
} else if (!isNowVisible && chartInOverlayWindow) {
this.chartVisible = true;
} else if (!wasVisible && isNowVisible) {
this.chartVisible = true;
} else if (!wasVisible && this.chartVisible) {
// rebuild the chart
this.buildCanvasElements();
const canvasInitialized = this.readyCanvasForDrawing();
@@ -313,8 +290,8 @@ export default {
this.draw();
}
this.$emit('plot-reinitialize-canvas');
} else {
this.chartVisible = isNowVisible;
} else if (wasVisible && this.chartVisible) {
// ignore, moving on
}
}
},

View File

@@ -94,7 +94,7 @@ export default class XAxisModel extends Model {
*/
defaultModel(options) {
const bounds = options.openmct.time.bounds();
const timeSystem = options.openmct.time.getTimeSystem();
const timeSystem = options.openmct.time.timeSystem();
const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat);
/** @type {XAxisModelType} */

View File

@@ -228,8 +228,8 @@ DrawWebGL.prototype.setDimensions = function (dimensions, origin) {
}
if (dimensions && dimensions.length > 0 && origin && origin.length > 0) {
this.gl?.uniform2fv(this.uDimensions, dimensions);
this.gl?.uniform2fv(this.uOrigin, origin);
this.gl.uniform2fv(this.uDimensions, dimensions);
this.gl.uniform2fv(this.uOrigin, origin);
}
};

View File

@@ -79,7 +79,7 @@
</div>
<div class="grid-cell value">{{ showLegendsForChildren ? 'Yes' : 'No' }}</div>
</li>
<li v-if="showLegendDetails" class="grid-row">
<li class="grid-row">
<div
class="grid-cell label"
title="The position of the legend relative to the plot display area."
@@ -88,27 +88,25 @@
</div>
<div class="grid-cell value capitalize">{{ position }}</div>
</li>
<li v-if="showLegendDetails" class="grid-row">
<li class="grid-row">
<div class="grid-cell label" title="Hide the legend when the plot is small">
Hide when plot small
</div>
<div class="grid-cell value">{{ hideLegendWhenSmall ? 'Yes' : 'No' }}</div>
</li>
<li v-if="showLegendDetails" class="grid-row">
<li class="grid-row">
<div class="grid-cell label" title="Show the legend expanded by default">
Expand by Default
</div>
<div aria-label="Expand by Default" class="grid-cell value">
{{ expandByDefault ? 'Yes' : 'No' }}
</div>
<div class="grid-cell value">{{ expandByDefault ? 'Yes' : 'No' }}</div>
</li>
<li v-if="showLegendDetails" class="grid-row">
<li class="grid-row">
<div class="grid-cell label" title="What to display in the legend when it's collapsed.">
Show when collapsed:
</div>
<div class="grid-cell value">{{ valueToShowWhenCollapsed.replace('nearest', '') }}</div>
</li>
<li v-if="showLegendDetails" class="grid-row">
<li class="grid-row">
<div class="grid-cell label" title="What to display in the legend when it's expanded.">
Show when expanded:
</div>
@@ -166,11 +164,6 @@ export default {
pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked'
);
},
showLegendDetails() {
return (
!this.isStackedPlotObject || (this.isStackedPlotObject && !this.showLegendsForChildren)
);
},
yAxesWithSeries() {
return this.yAxes.filter((yAxis) => yAxis.seriesCount > 0);
}
@@ -181,8 +174,9 @@ export default {
if (!this.isStackedPlotObject) {
this.initYAxesConfiguration();
this.registerListeners();
} else {
this.initLegendConfiguration();
}
this.initLegendConfiguration();
this.loaded = true;
},

View File

@@ -132,8 +132,7 @@ export default {
},
data() {
return {
expanded: false,
status: null
expanded: false
};
},
computed: {

View File

@@ -26,13 +26,12 @@
<div class="grid-cell value">
<input
v-model="showLegendsForChildren"
aria-label="Show Legends For Children"
type="checkbox"
@change="updateForm('showLegendsForChildren')"
/>
</div>
</li>
<li v-if="showLegendDetails" class="grid-row">
<li class="grid-row">
<div
class="grid-cell label"
title="The position of the legend relative to the plot display area."
@@ -48,7 +47,7 @@
</select>
</div>
</li>
<li v-if="showLegendDetails" class="grid-row">
<li class="grid-row">
<div class="grid-cell label" title="Hide the legend when the plot is small">
Hide when plot small
</div>
@@ -60,20 +59,15 @@
/>
</div>
</li>
<li v-if="showLegendDetails" class="grid-row">
<li class="grid-row">
<div class="grid-cell label" title="Show the legend expanded by default">
Expand by default
</div>
<div class="grid-cell value">
<input
v-model="expandByDefault"
aria-label="Expand By Default"
type="checkbox"
@change="updateForm('expandByDefault')"
/>
<input v-model="expandByDefault" type="checkbox" @change="updateForm('expandByDefault')" />
</div>
</li>
<li v-if="showLegendDetails" class="grid-row">
<li class="grid-row">
<div class="grid-cell label" title="What to display in the legend when it's collapsed.">
When collapsed show
</div>
@@ -88,7 +82,7 @@
</select>
</div>
</li>
<li v-if="showLegendDetails" class="grid-row">
<li class="grid-row">
<div class="grid-cell label" title="What to display in the legend when it's expanded.">
When expanded show
</div>
@@ -175,11 +169,6 @@ export default {
(pathObject, pathObjIndex) =>
pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked'
);
},
showLegendDetails() {
return (
!this.isStackedPlotObject || (this.isStackedPlotObject && !this.showLegendsForChildren)
);
}
},
mounted() {

View File

@@ -157,8 +157,7 @@ export default {
limitLines: this.series.get('limitLines'),
markerSize: this.series.get('markerSize'),
validation: {},
swatchActive: false,
status: null
swatchActive: false
};
},
computed: {

View File

@@ -30,17 +30,12 @@
<div
class="c-plot-legend__view-control gl-plot-legend__view-control c-disclosure-triangle is-enabled"
:class="{ 'c-disclosure-triangle--expanded': isLegendExpanded }"
@click="toggleLegend"
@click="expandLegend"
></div>
<div class="c-plot-legend__wrapper" :class="{ 'is-cursor-locked': cursorLocked }">
<!-- COLLAPSED PLOT LEGEND -->
<div
v-if="!isLegendExpanded"
class="plot-wrapper-collapsed-legend"
aria-label="Plot Legend Collapsed"
:class="{ 'is-cursor-locked': cursorLocked }"
>
<div class="plot-wrapper-collapsed-legend" :class="{ 'is-cursor-locked': cursorLocked }">
<div
class="c-state-indicator__alert-cursor-lock icon-cursor-lock"
title="Cursor is point locked. Click anywhere in the plot to unlock."
@@ -55,12 +50,7 @@
/>
</div>
<!-- EXPANDED PLOT LEGEND -->
<div
v-else
class="plot-wrapper-expanded-legend"
aria-label="Plot Legend Expanded"
:class="{ 'is-cursor-locked': cursorLocked }"
>
<div class="plot-wrapper-expanded-legend" :class="{ 'is-cursor-locked': cursorLocked }">
<div
class="c-state-indicator__alert-cursor-lock--verbose icon-cursor-lock"
title="Click anywhere in the plot to unlock."
@@ -155,22 +145,11 @@ export default {
this.legend = this.config.legend;
this.seriesModels = [];
this.listenTo(this.config.legend, 'change:position', this.updatePosition, this);
if (this.domainObject.type === 'telemetry.plot.stacked') {
this.objectComposition = this.openmct.composition.get(this.domainObject);
this.objectComposition.on('add', this.addTelemetryObject);
this.objectComposition.on('remove', this.removeTelemetryObject);
this.objectComposition.load();
} else {
this.registerListeners(this.config);
}
this.listenTo(this.config.legend, 'change:expandByDefault', this.changeExpandDefault, this);
this.initialize();
},
mounted() {
this.loaded = true;
this.isLegendExpanded = this.legend.get('expanded') === true;
this.$emit('expanded', this.isLegendExpanded);
this.updatePosition();
},
beforeUnmount() {
@@ -192,11 +171,6 @@ export default {
this.registerListeners(this.config);
}
},
changeExpandDefault() {
this.isLegendExpanded = this.config.legend.model.expandByDefault;
this.legend.set('expanded', this.isLegendExpanded);
this.$emit('expanded', this.isLegendExpanded);
},
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@@ -224,12 +198,9 @@ export default {
config.series.forEach(this.addSeries, this);
},
addSeries(series) {
const existingSeries = this.getSeries(series.keyString);
if (existingSeries) {
return;
}
this.seriesModels.push(series);
this.seriesModels[this.seriesModels.length] = series;
},
removeSeries(plotSeries) {
this.stopListening(plotSeries);
@@ -238,13 +209,7 @@ export default {
);
this.seriesModels.splice(seriesIndex, 1);
},
getSeries(keyStringToFind) {
const foundSeries = this.seriesModels.find((series) => {
return series.keyString === keyStringToFind;
});
return foundSeries;
},
toggleLegend() {
expandLegend() {
this.isLegendExpanded = !this.isLegendExpanded;
this.legend.set('expanded', this.isLegendExpanded);
this.$emit('expanded', this.isLegendExpanded);

View File

@@ -22,7 +22,6 @@
<template>
<div
class="plot-legend-item"
:aria-label="`Plot Legend Item for ${domainObject?.name}`"
:class="{
'is-stale': isStale,
'is-status--missing': isMissing
@@ -124,14 +123,9 @@ export default {
this.seriesModels = [];
eventHelpers.extend(this);
this.config = this.getConfig();
if (this.domainObject.type === 'telemetry.plot.stacked') {
this.objectComposition = this.openmct.composition.get(this.domainObject);
this.objectComposition.on('add', this.addTelemetryObject);
this.objectComposition.on('remove', this.removeTelemetryObject);
this.objectComposition.load();
} else {
this.registerListeners(this.config);
}
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
this.config.series.forEach(this.onSeriesAdd, this);
this.legend = this.config.legend;
this.loaded = true;
this.setupClockChangedEvent((domainObject) => {
@@ -141,11 +135,6 @@ export default {
},
beforeUnmount() {
this.stopListening();
if (this.objectComposition) {
this.objectComposition.off('add', this.addTelemetryObject);
this.objectComposition.off('remove', this.removeTelemetryObject);
}
},
methods: {
getConfig() {
@@ -153,55 +142,29 @@ export default {
return configStore.get(configId);
},
registerListeners(config) {
//listen to any changes to the telemetry endpoints that are associated with the child
this.listenTo(config.series, 'add', this.onSeriesAdd, this);
this.listenTo(config.series, 'remove', this.onSeriesRemove, this);
config.series.forEach(this.onSeriesAdd, this);
},
addTelemetryObject(object) {
//get the config for each child
const configId = this.openmct.objects.makeKeyString(object.identifier);
const config = configStore.get(configId);
if (config) {
this.registerListeners(config);
onSeriesAdd(series, index) {
this.seriesModels[index] = series;
if (series.keyString === this.seriesKeyString) {
this.listenTo(
series,
'change:color',
(newColor) => {
this.updateColor(newColor);
},
this
);
this.listenTo(
series,
'change:name',
() => {
this.updateName();
},
this
);
this.subscribeToStaleness(series.domainObject);
this.initialize();
}
},
removeTelemetryObject(identifier) {
const configId = this.openmct.objects.makeKeyString(identifier);
const config = configStore.get(configId);
if (config) {
config.series.forEach(this.onSeriesRemove, this);
}
},
onSeriesAdd(series) {
if (series.keyString !== this.seriesKeyString) {
return;
}
const existingSeries = this.getSeries(series.keyString);
if (existingSeries) {
return;
}
this.seriesModels.push(series);
this.listenTo(
series,
'change:color',
(newColor) => {
this.updateColor(newColor);
},
this
);
this.listenTo(
series,
'change:name',
() => {
this.updateName();
},
this
);
this.subscribeToStaleness(series.domainObject);
this.initialize();
},
onSeriesRemove(seriesToRemove) {
const seriesIndexToRemove = this.seriesModels.findIndex(
(series) => series.keyString === seriesToRemove.keyString

View File

@@ -22,7 +22,6 @@
<template>
<tr
class="plot-legend-item"
:aria-label="`Plot Legend Item for ${domainObject?.name}`"
:class="{
'is-stale': isStale,
'is-status--missing': isMissing
@@ -104,7 +103,6 @@ export default {
isMissing: false,
colorAsHexString: '',
name: '',
nameWithUnit: '',
unit: '',
formattedYValue: '',
formattedXValue: '',
@@ -148,16 +146,9 @@ export default {
this.seriesModels = [];
eventHelpers.extend(this);
this.config = this.getConfig();
if (this.domainObject.type === 'telemetry.plot.stacked') {
this.objectComposition = this.openmct.composition.get(this.domainObject);
this.objectComposition.on('add', this.addTelemetryObject);
this.objectComposition.on('remove', this.removeTelemetryObject);
this.objectComposition.load();
} else {
this.registerListeners(this.config);
}
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
this.config.series.forEach(this.onSeriesAdd, this);
this.legend = this.config.legend;
this.loaded = true;
this.setupClockChangedEvent((domainObject) => {
@@ -167,62 +158,31 @@ export default {
},
beforeUnmount() {
this.stopListening();
if (this.objectComposition) {
this.objectComposition.off('add', this.addTelemetryObject);
this.objectComposition.off('remove', this.removeTelemetryObject);
}
},
methods: {
registerListeners(config) {
//listen to any changes to the telemetry endpoints that are associated with the child
this.listenTo(config.series, 'add', this.onSeriesAdd, this);
this.listenTo(config.series, 'remove', this.onSeriesRemove, this);
config.series.forEach(this.onSeriesAdd, this);
},
addTelemetryObject(object) {
//get the config for each child
const configId = this.openmct.objects.makeKeyString(object.identifier);
const config = configStore.get(configId);
if (config) {
this.registerListeners(config);
onSeriesAdd(series, index) {
this.seriesModels[index] = series;
if (series.keyString === this.seriesKeyString) {
this.listenTo(
series,
'change:color',
(newColor) => {
this.updateColor(newColor);
},
this
);
this.listenTo(
series,
'change:name',
() => {
this.updateName();
},
this
);
this.subscribeToStaleness(series.domainObject);
this.initialize();
}
},
removeTelemetryObject(identifier) {
const configId = this.openmct.objects.makeKeyString(identifier);
const config = configStore.get(configId);
if (config) {
config.series.forEach(this.onSeriesRemove, this);
}
},
onSeriesAdd(series) {
if (series.keyString !== this.seriesKeyString) {
return;
}
const existingSeries = this.getSeries(series.keyString);
if (existingSeries) {
return;
}
this.seriesModels.push(series);
this.listenTo(
series,
'change:color',
(newColor) => {
this.updateColor(newColor);
},
this
);
this.listenTo(
series,
'change:name',
() => {
this.updateName();
},
this
);
this.subscribeToStaleness(series.domainObject);
this.initialize();
},
onSeriesRemove(seriesToRemove) {
const seriesIndexToRemove = this.seriesModels.findIndex(
(series) => series.keyString === seriesToRemove.keyString

View File

@@ -401,7 +401,6 @@ describe('the plugin', function () {
const clickEvent = createMouseEvent('click');
legendControl.dispatchEvent(clickEvent);
await nextTick();
let legend = element.querySelectorAll('.plot-wrapper-expanded-legend .plot-legend-item td');
expect(legend.length).toBe(6);

View File

@@ -163,13 +163,13 @@ export default {
},
updateView() {
//If this object is not persistable, then package it with it's parent
const plotObject = this.getPlotObject();
const object = this.getPlotObject();
if (this.openmct.telemetry.isTelemetryObject(plotObject)) {
this.subscribeToStaleness(plotObject);
if (this.openmct.telemetry.isTelemetryObject(object)) {
this.subscribeToStaleness(object);
} else {
// possibly overlay or other composition based plot
this.composition = this.openmct.composition.get(plotObject);
this.composition = this.openmct.composition.get(object);
this.composition.on('add', this.subscribeToStaleness);
this.composition.on('remove', this.triggerUnsubscribeFromStaleness);
@@ -209,63 +209,61 @@ export default {
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);
},
getPlotObject() {
this.checkPlotConfiguration();
// If object is missing, warn
if (this.openmct.objects.isMissing(this.childObject)) {
console.warn('Missing domain object for stacked plot', this.childObject);
}
return this.childObject;
},
checkPlotConfiguration() {
// If the object has its own configuration (like an overlay plot), don't initialize a stacked plot configuration
// and instead use its configuration directly.
// Otherwise ensure we've got a stacked plot item configuration ready for us.
if (
!this.openmct.objects.isMissing(this.childObject) &&
!this.childObject.configuration?.series
) {
this.ensureStackedSeriesConfigInitialization();
}
},
ensureStackedSeriesConfigInitialization() {
const configId = this.openmct.objects.makeKeyString(this.childObject.identifier);
const existingConfig = configStore.get(configId);
if (!existingConfig) {
let persistedSeriesConfig = this.domainObject.configuration.series.find((seriesConfig) => {
return this.openmct.objects.areIdsEqual(
seriesConfig.identifier,
this.childObject.identifier
);
});
if (this.childObject.configuration?.series) {
//If the object has a configuration (like an overlay plot), allow initialization of the config from it's persisted config
return this.childObject;
} else {
//If object is missing, warn and return object
if (this.openmct.objects.isMissing(this.childObject)) {
console.warn('Missing domain object');
if (!persistedSeriesConfig) {
persistedSeriesConfig = {
series: {},
yAxis: {}
};
return this.childObject;
}
const newConfig = new PlotConfigurationModel({
id: configId,
domainObject: {
...this.childObject,
configuration: {
series: [
{
identifier: this.childObject.identifier,
...persistedSeriesConfig.series
}
],
yAxis: persistedSeriesConfig.yAxis
// If the object does not have configuration, initialize the series config with the persisted config from the stacked plot
const configId = this.openmct.objects.makeKeyString(this.childObject.identifier);
let config = configStore.get(configId);
if (!config) {
let persistedSeriesConfig = this.domainObject.configuration.series.find(
(seriesConfig) => {
return this.openmct.objects.areIdsEqual(
seriesConfig.identifier,
this.childObject.identifier
);
}
},
openmct: this.openmct,
palette: this.colorPalette,
callback: (data) => {
this.data = data;
);
if (!persistedSeriesConfig) {
persistedSeriesConfig = {
series: {},
yAxis: {}
};
}
});
configStore.add(configId, newConfig);
config = new PlotConfigurationModel({
id: configId,
domainObject: {
...this.childObject,
configuration: {
series: [
{
identifier: this.childObject.identifier,
...persistedSeriesConfig.series
}
],
yAxis: persistedSeriesConfig.yAxis
}
},
openmct: this.openmct,
palette: this.colorPalette,
callback: (data) => {
this.data = data;
}
});
configStore.add(configId, config);
}
return this.childObject;
}
}
}

View File

@@ -360,7 +360,7 @@ describe('the plugin', function () {
expect(legend[0].innerHTML).toEqual('Test Object');
});
it('Renders an expanded legend for every telemetry', async () => {
it('Renders an expanded legend for every telemetry', () => {
let legendControl = element.querySelector(
'.c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle'
);
@@ -368,8 +368,6 @@ describe('the plugin', function () {
legendControl.dispatchEvent(clickEvent);
await nextTick();
let legend = element.querySelectorAll('.plot-wrapper-expanded-legend .plot-legend-item td');
expect(legend.length).toBe(6);
});

View File

@@ -65,7 +65,6 @@ import PerformanceIndicator from './performanceIndicator/plugin.js';
import CouchDBPlugin from './persistence/couch/plugin.js';
import PlanLayout from './plan/plugin.js';
import PlotPlugin from './plot/plugin.js';
import ReloadAction from './reloadAction/plugin.js';
import RemoteClock from './remoteClock/plugin.js';
import StaticRootPlugin from './staticRootPlugin/plugin.js';
import SummaryWidget from './summaryWidget/plugin.js';
@@ -142,7 +141,6 @@ plugins.Filters = Filters;
plugins.ObjectMigration = ObjectMigration;
plugins.GoToOriginalAction = GoToOriginalAction;
plugins.OpenInNewTabAction = OpenInNewTabAction;
plugins.ReloadAction = ReloadAction;
plugins.ClearData = ClearData;
plugins.WebPage = WebPagePlugin;
plugins.Espresso = Espresso;

View File

@@ -1,37 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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.
*****************************************************************************/
export default class ReloadAction {
constructor(openmct) {
this.name = 'Reload';
this.key = 'reload';
this.description = 'Reload this object and its children';
this.group = 'action';
this.priority = 10;
this.cssClass = 'icon-refresh';
this.openmct = openmct;
}
invoke(objectPath, view) {
const domainObject = objectPath[0];
this.openmct.objectViews.emit('reload', domainObject);
}
}

View File

@@ -1,28 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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 ReloadAction from './ReloadAction.js';
export default function plugin() {
return function install(openmct) {
openmct.actions.register(new ReloadAction(openmct));
};
}

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