Compare commits

...

29 Commits

Author SHA1 Message Date
David Tsay
cc58dbd5e7 Merge branch 'master' into eval-source-maps 2024-03-25 11:12:17 -07:00
Jesse Mazzella
d68ac31ab5 chore: bump @playwright/test to 1.42.1 (#7627)
* chore: bump `@playwright/test` to `1.42.1`

* chore(circleci): don't try to re-run individual percy tests
2024-03-21 09:27:41 -07:00
Jesse Mazzella
f504ee29cc fix: 🤖 beep boop beep, you forgot an await 🤖 (#7630)
* fix: 🤖 beep boop beep, you forgot an `await` 🤖

* add e2e test

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2024-03-20 20:20:48 +00:00
dependabot[bot]
1d5ddc545e chore(deps-dev): bump @types/lodash from 4.14.192 to 4.17.0 (#7610)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.192 to 4.17.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 22:57:38 -07:00
John Hill
42085a4b70 [CI] Parallelize visual test runs (#7618)
* rename suite and add parallelism

* use test sharding

* expect 2 parallel runs

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2024-03-19 20:50:14 -07:00
Scott Bell
b2b0837592 Handle empty namespaces in import (#7619)
* handle blank namespaces in import

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-19 13:05:14 -07:00
Scott Bell
e305b46d88 Ensure a request for telemetry happens in Condition Sets (#7592)
* request telemetry when subscribing to data in case we have cached subscription

* change back to >=

* revert

* update tests

* fixing tests

* add metadata

* fix test

* another mock required

* one more function needed

* attempt to fix some afterall errors

* add fixme for e2e test

* fail fast on request

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-19 10:44:50 -07:00
Jamie V
fb396ac194 [Telemetry Table] Telemetry mode bug fixes (#7601)
* source maps

* any tables without configuration will default to either default options or configured options

* prevent double unsubscribese

* remove source maps

* update coment

* moving defaults to plugin level

* whoops

* missed a spot, updated omment

* adding config values

* lint

* typos

* fixing broken ref

* fixing broken ref

* actually fixing ref

* setting rowLimit so initial change does not trigger a resubscribe of telemetry that was not subscribed yet
2024-03-19 02:34:00 +00:00
Shefali Joshi
a01f21017f For the setTimeConductorMode, use the close time popup button rather than the submit button to dismiss time popup (#7613)
Use the close time popup button rather than the submit button as the submit button triggers network requests.
2024-03-18 23:48:33 +00:00
Rukmini Bose (Ruki)
b7b9ccbe65 [TC Popup] Fix Calendar so it is not cutoff (#7596) 2024-03-18 23:28:09 +00:00
Scott Bell
f189a4d602 Resize plans properly (#7597)
* resize firing

* ensure watcher fires

* remove unneeded const

* add small visual test resizing plan

* use browser with null viewport

* lint
2024-03-18 15:13:19 -07:00
dependabot[bot]
4027eae299 chore(deps-dev): bump follow-redirects from 1.15.5 to 1.15.6 (#7603)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-17 06:53:20 -07:00
John Hill
d4695178bc remove reference to LGTM (#7591) 2024-03-16 11:33:55 -07:00
David Tsay
c19c4e7065 Merge branch 'master' into eval-source-maps 2024-03-15 10:41:14 -07:00
Rukmini Bose (Ruki)
5fc5c13314 [Plot] Fix plot swatch behavior when vertical space is small (#7493)
Add overflow: hidden such that when vertical space is small, no autoscroll happens on the axis
2024-03-14 16:49:36 +00:00
John Hill
ceeb761d94 [build] Re-enable package lock (#7584)
* include package lock
* migrate to npm ci
* remove cache busting doc and replace with npm run clean

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-14 16:27:31 +00:00
Jamie V
10eb749d32 Prevent Metadata Time System Error for Missing Objects (#7565)
https://github.com/nasa/openmct/pull/7565 Modified Stacked Plots to not show Missing Objects. Added a check in Telemetry Collections for missing objects before displaying telemetry metadata time system error.
2024-03-14 09:05:23 -07:00
Jesse Mazzella
faed27c143 fix(#7552): Fix notebook snapshot image annotations (#7555)
* fix: painterro import

* test(snapshotAnnotation): add minimal e2e test

* chore: add e2e test annotation

* fix: notebook snapshot test

* refactor: put `v-else` on template

* small changes to the test and a visual one

* additional a11y

* fix: html structure

* test(e2e): fix notebook snapshot tests

* Update documentation for file download and JSON testing

* Update stubs and add jpg/png export

* refactor(TimelistComponent): tidy up

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-13 20:27:49 +00:00
David Tsay
3bb4df8d39 enable eval-source-maps 2024-03-13 12:34:29 -07:00
David Tsay
18e976ad12 allow inspector pane content to scroll vertically (#7567)
* allow content to scroll vertically

* add framework for inspector content scrollable e2e test

* fix paths and spelling error

* add aria-label to properties list

* add scroll check to test

* use click, which scrolls if needed

* use scrollbar to scroll

* Closes #7566
- Fixed scrolling to only apply to the area below the Inspector tabs.
- Removed unneeded padding in pane.scss.
- Alignment fixes to related scroll elements in tree.

* cspell: ignore this file because it doesn't understand latin

* fix selectors and test

* lint fix

* driveby: wait for thumbnail bar to finish scrolling before taking snapshot

---------

Co-authored-by: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-03-13 19:04:02 +00:00
Jamie V
64862634f3 [Telemetry Table] Address issues found during testing Table Performance (#7529)
Fix exporting from Limited Mode: #7268 (comment)
Fix UI issues: #7268 (comment)
Apply configuration changes made in Edit Properties.

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-13 09:25:51 -07:00
Jesse Mazzella
cb4c59a464 fix(#7015): Generate source maps for generating code coverage metrics (#7582)
* fix(?): the robot says to do this...

* refactor: remove unused env var

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-12 17:02:41 -07:00
Andrew Henry
5f0bd10c61 Request batch when idle (#7526)
See https://github.com/nasa/openmct/pull/7526 for details
2024-03-12 20:46:06 +00:00
Jesse Mazzella
8c2558bfe0 fix(#7524): Open in New Tab action from a sub-object in a layout (#7542)
* refactor: url tools use named exports

* fix: refactor method and remove customUrlParams

* test(e2e): verify bounds are preserved in data pivoting

* test: remove test as feature is no longer needed

- dataVisualization logic has moved from MMGIS plugin to the open source. As such, we can just use the time conductor bounds

* refactor: autoformat keeps changing this so i'mma just commit it

* refactor: remove unnecessary code

* refactor: simplify, add docs

* Revert "refactor: remove unnecessary code"

This reverts commit 87aef35c510230835fb682b80e89a6006ef2d923.

* a11y: improve aria labels for ITC

* fix: simplify url method

* fix: update ITC app actions

* test: add test to generate test data for display layout w/ overlay plot + ITC enabled

* test(e2e): add suite + test for open in new tab from subobject

- needs cleanup

* a11y: various a11y improvement drivebys

* a11y: clock indicator needs to be quiet

* a11y: add `aria-live` to SuperMenu details

* a11y: greatly improve a11y of Menus and SuperMenus

* test(e2e): clean up test

* fix: improve a11y for context menus, fix test

* chore: remove nop-longer-recommended extension

* feat: provide one more bound option for example data viz

* fix: no need for `mount`, use dynamic rendering instead

* Revert "fix: simplify url method"

This reverts commit b24c7dabc783a9a1c3f2460eada99f452259f566.

* fix: correct time conductor bounds when opening in a new tab from a plot in the inspector

* test: fix e2e tests

* Revert "test: remove test as feature is no longer needed"

This reverts commit 759ebd4667bffb1979d5f62af6b47f349dcd9f77.

* test: move 2p annotation to test

* test: fix e2e

* fix: no words for the word god today

* test: fix e2e

* fix: e2e test

* test: fix test

* driveby: fix perf test

* fix: revert required prop change

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:39:38 -07:00
dependabot[bot]
0eadc7a4ae chore(deps-dev): bump copy-webpack-plugin from 11.0.0 to 12.0.2 (#7574)
Bumps [copy-webpack-plugin](https://github.com/webpack-contrib/copy-webpack-plugin) from 11.0.0 to 12.0.2.
- [Release notes](https://github.com/webpack-contrib/copy-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/copy-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/copy-webpack-plugin/compare/v11.0.0...v12.0.2)

---
updated-dependencies:
- dependency-name: copy-webpack-plugin
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:33:15 -07:00
dependabot[bot]
cad4652a08 chore(deps-dev): bump karma-webpack from 5.0.0 to 5.0.1 (#7575)
Bumps [karma-webpack](https://github.com/webpack-contrib/karma-webpack) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/webpack-contrib/karma-webpack/releases)
- [Changelog](https://github.com/codymikol/karma-webpack/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/karma-webpack/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: karma-webpack
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:03:50 -07:00
dependabot[bot]
8379f2d073 chore(deps-dev): bump imports-loader from 4.0.1 to 5.0.0 (#7576)
Bumps [imports-loader](https://github.com/webpack-contrib/imports-loader) from 4.0.1 to 5.0.0.
- [Release notes](https://github.com/webpack-contrib/imports-loader/releases)
- [Changelog](https://github.com/webpack-contrib/imports-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/imports-loader/compare/v4.0.1...v5.0.0)

---
updated-dependencies:
- dependency-name: imports-loader
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:03:12 -07:00
dependabot[bot]
8ed112a4a8 chore(deps-dev): bump sass from 1.68.0 to 1.71.1 (#7577)
Bumps [sass](https://github.com/sass/dart-sass) from 1.68.0 to 1.71.1.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.68.0...1.71.1)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 15:42:05 -07:00
dependabot[bot]
14c58c4410 chore(deps-dev): bump npm-run-all2 from 6.1.1 to 6.1.2 (#7579)
Bumps [npm-run-all2](https://github.com/bcomnes/npm-run-all2) from 6.1.1 to 6.1.2.
- [Release notes](https://github.com/bcomnes/npm-run-all2/releases)
- [Changelog](https://github.com/bcomnes/npm-run-all2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bcomnes/npm-run-all2/compare/v6.1.1...v6.1.2)

---
updated-dependencies:
- dependency-name: npm-run-all2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 15:40:13 -07:00
116 changed files with 13898 additions and 767 deletions

View File

@@ -1,59 +1,33 @@
version: 2.1
orbs:
node: circleci/node@5.2.0
browser-tools: circleci/browser-tools@1.3.0
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.39.0-focal
- image: mcr.microsoft.com/playwright:v1.42.1-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: "true" # Needed to store the percy browser in cache deps
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)
PERCY_PARALLEL_TOTAL: 2
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!"
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.'
parameters:
node-version:
type: string
steps:
- checkout
- restore_cache_cmd:
node-version: << parameters.node-version >>
- node/install:
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"
parameters:
node-version:
type: string
steps:
- when:
condition:
equal: [false, << pipeline.parameters.BUST_CACHE >>]
steps:
- 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."
parameters:
node-version:
type: string
steps:
- save_cache:
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
paths:
- ~/.npm
- node_modules
- node/install-packages
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,16 +38,13 @@ 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
steps:
- run: npm run cov:e2e:report || true
- run: npm run cov:e2e:<<parameters.suite>>:publish
orbs:
node: circleci/node@5.1.0
browser-tools: circleci/browser-tools@1.3.0
jobs:
npm-audit:
parameters:
@@ -111,8 +82,6 @@ jobs:
TESTFILES=$(circleci tests glob "src/**/*Spec.js")
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
- run: npm run cov:unit:publish
- save_cache_cmd:
node-version: <<parameters.node-version>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
@@ -133,7 +102,7 @@ jobs:
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:
@@ -190,7 +159,7 @@ jobs:
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npx playwright@1.39.0 install #Necessary for bare ubuntu machine
- run: npx playwright@1.42.1 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@@ -252,14 +221,15 @@ jobs:
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
steps:
- generate_and_store_version_and_filesystem_artifacts
visual-a11y-tests:
visual-a11y:
parameters:
suite:
type: string # ci or full
executor: pw-focal-development
parallelism: 2
steps:
- build_and_install:
node-version: lts/hydrogen
node-version: lts/iron
- run: npm run test:e2e:visual:<<parameters.suite>>
- store_test_results:
path: test-results/results.xml
@@ -286,8 +256,8 @@ workflows:
name: e2e-stable
suite: stable
- e2e-mobile
- visual-a11y-tests:
name: visual-a11y-test-ci
- visual-a11y:
name: visual-a11y-ci
suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night
@@ -306,13 +276,13 @@ workflows:
- e2e-mobile
- perf-test
- mem-test
- visual-a11y-tests:
name: visual-a11y-test-nightly
- visual-a11y:
name: visual-a11y-nightly
suite: full
- e2e-couchdb
triggers:
- schedule:
cron: "0 0 * * *"
cron: '0 0 * * *'
filters:
branches:
only:

View File

@@ -17,7 +17,6 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Has this been smoke tested?
* [ ] Have you associated this PR with a `type:` label? Note: this is not necessarily the same as the original issue.
* [ ] Have you associated a milestone with this PR? Note: leave blank if unsure.
* [ ] Is this a breaking change to be called out in the release notes?
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
### Reviewer Checklist

View File

@@ -28,7 +28,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm ci --no-audit --progress=false
- name: Login to DockerHub
uses: docker/login-action@v3
@@ -37,7 +37,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.39.0 install
- run: npx playwright@1.42.1 install
- name: Start CouchDB Docker Container and Init with Setup Scripts
run: |

View File

@@ -30,8 +30,8 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.39.0 install
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npx playwright@1.42.1 install
- run: npm ci --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

View File

@@ -28,8 +28,8 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.39.0 install
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npx playwright@1.42.1 install
- run: npm ci --no-audit --progress=false
- run: npm run test:perf:localhost
- run: npm run test:perf:contract
- run: npm run test:perf:memory

View File

@@ -33,9 +33,9 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.39.0 install
- run: npx playwright@1.42.1 install
- run: npx playwright install chrome-beta
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm ci --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40
- run: npm run cov:e2e:report || true
- shell: bash

View File

@@ -15,7 +15,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
- run: npm install
- run: npm ci
- run: |
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
npm whoami
@@ -31,7 +31,7 @@ jobs:
with:
node-version: lts/hydrogen
registry-url: https://registry.npmjs.org/
- run: npm install
- run: npm ci
- run: npm publish --access=public --tag unstable
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -45,7 +45,7 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ matrix.node_version }}-
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm ci --no-audit --progress=false
- run: npm test

3
.gitignore vendored
View File

@@ -47,6 +47,3 @@ index.html.bak
.nyc_output
coverage
codecov
# :(
package-lock.json

3
.npmrc
View File

@@ -2,6 +2,3 @@ loglevel=warn
#Prevent folks from ignoring an important error when building from source
engine-strict=true
# Dont include lockfile
package-lock=false

View File

@@ -5,7 +5,6 @@
// 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"
],

View File

@@ -5,11 +5,8 @@ information to pull requests.
*/
import config from './webpack.dev.js';
// eslint-disable-next-line no-undef
const CI = process.env.CI === 'true';
config.devtool = CI ? false : undefined;
config.devtool = 'source-map';
config.devServer.hot = false;
config.module.rules.push({

View File

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

View File

@@ -16,8 +16,6 @@ The [CodeQL GitHub Actions workflow](https://github.com/nasa/openmct/blob/master
CodeQL is run for every pull-request in GitHub Actions.
The project is also monitored by [LGTM](https://lgtm.com/projects/g/nasa/openmct/) and is available to public.
### ESLint
Static analysis is run for every push on the master branch and every pull request on all branches in Github Actions.

View File

@@ -91,12 +91,14 @@ There are a few reasons that your GitHub PR could be failing beyond simple faile
### Local=Pass and CI=Fail
Although rare, it is possible that your test can pass locally but fail in CI.
#### Busting Cache
In certain circumstances, the CircleCI cache can become stale. In order to bust the cache, we've implemented a runtime boolean parameter in Circle CI creatively name BUST_CACHE. To execute:
1. Navigate to the branch in Circle CI believed to have stale cache.
1. Click on the 'Trigger Pipeline' button.
1. Add Parameter -> Parameter Type = boolean , Name = BUST_CACHE ,Value = true
1. Click 'Trigger Pipeline'
### Reset your workspace
It's possible that you're running with dependencies or a local environment which is out of sync with the branch you're working on. Make sure to execute the following:
```sh
nvm use
npm run clean
npm install
```
#### Run tests in the same container as CI

View File

@@ -516,6 +516,30 @@ test.describe('foo test suite', () => {
- Working with multiple pages
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
- Working with file downloads and JSON data
Open MCT has the capability of exporting certain objects in the form of a JSON file handled by the chrome browser. The best example of this type of test can be found in the exportAsJson test.
```js
const [download] = await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as JSON').click() // Triggers the download
]);
// Wait for the download process to complete
const path = await download.path();
// Read the contents of the downloaded file using readFile from fs/promises
const fileContents = await fs.readFile(path, 'utf8');
const jsonData = JSON.parse(fileContents);
// Use the function to retrieve the key
const key = getFirstKeyFromOpenMctJson(jsonData);
// Verify the contents of the JSON file
expect(jsonData.openmct[key]).toHaveProperty('name', 'e2e folder');
```
### Reporting
Test Reporting is done through official Playwright reporters and the CI Systems which execute them.

View File

@@ -392,6 +392,8 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.waitForURL(/tc\.mode=local/);
}
//dismiss the time conductor popup
await page.getByLabel('Discard changes and close time popup').click();
}
/**
@@ -505,15 +507,14 @@ async function setTimeConductorBounds(page, startDate, endDate) {
* @param {string} startDate
* @param {string} endDate
*/
async function setIndependentTimeConductorBounds(page, startDate, endDate) {
// Activate Independent Time Conductor in Fixed Time Mode
await page.getByRole('switch').click();
async function setIndependentTimeConductorBounds(page, { start, end }) {
// Activate Independent Time Conductor
await page.getByLabel('Enable Independent Time Conductor').click();
// Bring up the time conductor popup
await page.click('.c-conductor-holder--compact .c-compact-tc');
await page.getByLabel('Independent Time Conductor Settings').click();
await expect(page.locator('.itc-popout')).toBeInViewport();
await setTimeBounds(page, startDate, endDate);
await setTimeBounds(page, start, end);
await page.keyboard.press('Enter');
}
@@ -663,5 +664,6 @@ export {
setRealTimeMode,
setStartOffset,
setTimeConductorBounds,
setTimeConductorMode,
waitForPlotsToRender
};

File diff suppressed because one or more lines are too long

View File

@@ -174,6 +174,6 @@ test.describe('AppActions', () => {
type: 'Folder'
});
await openObjectTreeContextMenu(page, folder.url);
await expect(page.getByLabel('Menu')).toBeVisible();
await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible();
});
});

View File

@@ -33,7 +33,12 @@
import { fileURLToPath } from 'url';
import { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js';
import {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
setIndependentTimeConductorBounds,
setTimeConductorBounds
} from '../../appActions.js';
import { MISSION_TIME } from '../../constants.js';
import { expect, test } from '../../pluginFixtures.js';
@@ -89,6 +94,53 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
});
});
test('Generate display layout with 1 child overlay plot', async ({ page, context }) => {
const parent = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Parent Display Layout'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Child Overlay Plot 1',
parent: parent.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Child SWG 1',
parent: overlayPlot.uuid
});
await page.goto(parent.url, { waitUntil: 'domcontentloaded' });
await setIndependentTimeConductorBounds(page, {
start: '2024-11-12 19:11:11.000Z',
end: '2024-11-12 20:11:11.000Z'
});
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
await setTimeConductorBounds(page, NEW_GLOBAL_START_BOUNDS, NEW_GLOBAL_END_BOUNDS);
// Verify that the global time conductor bounds have been updated
expect(
await page.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
).toEqual(NEW_GLOBAL_START_BOUNDS);
expect(
await page.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
).toEqual(NEW_GLOBAL_END_BOUNDS);
//Save localStorage for future test execution
await context.storageState({
path: fileURLToPath(
new URL(
'../../../e2e/test-data/display_layout_with_child_overlay_plot.json',
import.meta.url
)
)
});
});
test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => {
// Create Display Layout
const parent = await createDomainObjectWithDefaults(page, {

View File

@@ -131,7 +131,10 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
await setIndependentTimeConductorBounds(page, {
start: startBoundString,
end: endBoundString
});
expect(await activityBounds.count()).toEqual(1);
});
@@ -160,7 +163,10 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
await setIndependentTimeConductorBounds(page, {
start: startBoundString,
end: endBoundString
});
// Verify that two events are displayed
expect(await activityBounds.count()).toEqual(2);

View File

@@ -286,12 +286,22 @@ test.describe('Basic Condition Set Use', () => {
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.click('button[title="Change the current view"]');
await page.getByLabel('Open the View Switcher Menu').click();
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
await page.getByLabel('Plot').click();
await expect(
page.getByLabel('Plot Legend Collapsed').getByText('Test Condition Set')
).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Telemetry Table').click();
await expect(page.getByRole('searchbox', { name: 'output filter input' })).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Conditions View').click();
await expect(page.getByText('Current Output')).toBeVisible();
});
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
page
@@ -457,4 +467,11 @@ test.describe('Basic Condition Set Use', () => {
await page.goto(exampleTelemetry.url);
});
test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7484'
});
});
});

View File

@@ -23,6 +23,7 @@ import { fileURLToPath } from 'url';
import {
createDomainObjectWithDefaults,
navigateToObjectWithFixedTimeBounds,
setFixedTimeMode,
setIndependentTimeConductorBounds,
setRealTimeMode,
@@ -30,12 +31,120 @@ import {
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
const LOCALSTORAGE_PATH = fileURLToPath(
const CHILD_LAYOUT_STORAGE_STATE_PATH = fileURLToPath(
new URL('../../../../test-data/display_layout_with_child_layouts.json', import.meta.url)
);
const CHILD_PLOT_STORAGE_STATE_PATH = fileURLToPath(
new URL('../../../../test-data/display_layout_with_child_overlay_plot.json', import.meta.url)
);
const TINY_IMAGE_BASE64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
test.describe('Display Layout Sub-object Actions @localStorage', () => {
const INIT_ITC_START_BOUNDS = '2024-11-12 19:11:11.000Z';
const INIT_ITC_END_BOUNDS = '2024-11-12 20:11:11.000Z';
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
test.use({
storageState: CHILD_PLOT_STORAGE_STATE_PATH
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Expand My Items folder').click();
const waitForMyItemsNavigation = page.waitForURL(`**/mine/?*`);
await page
.getByLabel('Main Tree')
.getByLabel('Navigate to Parent Display Layout layout Object')
.click();
// Wait for the URL to change to the display layout
await waitForMyItemsNavigation;
});
test('Open in New Tab action preserves time bounds @2p', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7524'
});
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6982'
});
const TEST_FIXED_START_TIME = 1731352271000; // 2024-11-11 19:11:11.000Z
const TEST_FIXED_END_TIME = TEST_FIXED_START_TIME + 3600000; // 2024-11-11 20:11:11.000Z
// Verify the ITC has the expected initial bounds
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
// Update the global fixed bounds to 2024-11-11 19:11:11.000Z / 2024-11-11 20:11:11.000Z
const url = page.url().split('?')[0];
await navigateToObjectWithFixedTimeBounds(
page,
url,
TEST_FIXED_START_TIME,
TEST_FIXED_END_TIME
);
// ITC bounds should still match the initial ITC bounds
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
// Open the Child Overlay Plot 1 in a new tab
await page.getByLabel('View menu items').click();
const pagePromise = page.context().waitForEvent('page');
await page.getByLabel('Open In New Tab').click();
const newPage = await pagePromise;
await newPage.waitForLoadState('domcontentloaded');
// Verify that the global time conductor bounds in the new page match the updated global bounds
expect(
await newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
).toEqual(NEW_GLOBAL_START_BOUNDS);
expect(
await newPage.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
).toEqual(NEW_GLOBAL_END_BOUNDS);
// Verify that the ITC is enabled in the new page
await expect(newPage.getByLabel('Disable Independent Time Conductor')).toBeVisible();
// Verify that the ITC bounds in the new page match the original ITC bounds
expect(
await newPage
.getByLabel('Independent Time Conductor Panel')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await newPage
.getByLabel('Independent Time Conductor Panel')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
});
});
test.describe('Display Layout Toolbar Actions @localStorage', () => {
const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';
const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1';
@@ -50,7 +159,7 @@ test.describe('Display Layout Toolbar Actions @localStorage', () => {
await page.getByLabel('Edit Object').click();
});
test.use({
storageState: LOCALSTORAGE_PATH
storageState: CHILD_LAYOUT_STORAGE_STATE_PATH
});
test('can add/remove Text element to a single layout', async ({ page }) => {
@@ -163,7 +272,7 @@ test.describe('Display Layout', () => {
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({
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click();
@@ -336,7 +445,7 @@ test.describe('Display Layout', () => {
const startDate = '2021-12-30 01:01:00.000Z';
const endDate = '2021-12-30 01:11:00.000Z';
await setIndependentTimeConductorBounds(page, startDate, endDate);
await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();

View File

@@ -248,11 +248,10 @@ test.describe('Flexible Layout', () => {
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// flip on independent time conductor
await setIndependentTimeConductorBounds(
page,
'2021-12-30 01:01:00.000Z',
'2021-12-30 01:11:00.000Z'
);
await setIndependentTimeConductorBounds(page, {
start: '2021-12-30 01:01:00.000Z',
end: '2021-12-30 01:11:00.000Z'
});
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
@@ -290,7 +289,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
await page.getByTitle('Add Container').click();
expect(await containerHandles.count()).toEqual(3);
await page.getByTitle('Remove Container').click();
await expect(page.getByRole('dialog', { name: 'Overlay' })).toHaveText(
await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(
'This action will permanently delete this container from this Flexible Layout. Do you want to continue?'
);
await page.getByRole('button', { name: 'OK', exact: true }).click();
@@ -300,7 +299,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(2);
await page.getByRole('group', { name: 'Child Layout 1' }).click();
await page.getByTitle('Remove Frame').click();
await expect(page.getByRole('dialog', { name: 'Overlay' })).toHaveText(
await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(
'This action will remove this frame from this Flexible Layout. Do you want to continue?'
);
await page.getByRole('button', { name: 'OK', exact: true }).click();

View File

@@ -175,13 +175,13 @@ test.describe('Gauge', () => {
});
// Try to create a Folder into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click();
await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('menuitem', { name: /Folder/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.getByLabel('Cancel').click();
// Try to create a Display Layout into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click();
await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('menuitem', { name: /Display Layout/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
});

View File

@@ -37,6 +37,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
});
test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => {
const initStartBounds = await page.getByLabel('Start bounds').textContent();
const initEndBounds = await page.getByLabel('End bounds').textContent();
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
type: 'Example Data Visualization Source'
});
@@ -78,5 +80,9 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
await newPage.waitForLoadState();
// expect new tab title to contain 'Second Sine Wave Generator'
await expect(newPage).toHaveTitle('Second Sine Wave Generator');
// Verify that "Open in New Tab" preserves the time bounds
expect(initStartBounds).toEqual(await newPage.getByLabel('Start bounds').textContent());
expect(initEndBounds).toEqual(await newPage.getByLabel('End bounds').textContent());
});
});

View File

@@ -71,42 +71,89 @@ test.describe('Snapshot Container tests', () => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Notebook
// const notebook = await createDomainObjectWithDefaults(page, {
// type: 'Notebook',
// name: "Test Notebook"
// });
// // Create Overlay Plot
// const snapShotObject = await createDomainObjectWithDefaults(page, {
// type: 'Overlay Plot',
// name: "Dropped Overlay Plot"
// });
await page.getByLabel('Take a Notebook Snapshot').click();
await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await page.getByLabel('Show Snapshots').click();
});
test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'Quick View' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible();
await expect(page.getByLabel('Modal Overlay')).toBeVisible();
await expect(page.getByLabel('Preview Container')).toBeVisible();
});
test('A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7552'
});
//Open Snapshot Object View
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await expect(page.getByRole('dialog', { name: 'Modal Overlay' })).toBeVisible();
await expect(page.locator('#snapshotDescriptor')).toHaveText(
/SNAPSHOT \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/
);
// Open Annotation Editor with Painterro
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
});
test('A snapshot can be Annotated and saved as a JPG and PNG', async ({ page }) => {
//Open Snapshot Object View
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await expect(page.getByRole('dialog', { name: 'Modal Overlay' })).toBeVisible();
// Open Annotation Editor with Painterro
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
// Save as JPG
await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as JPG').click() // Triggers the download
]);
// Save as PNG
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as PNG').click() // Triggers the download
]);
});
test.fixme(
'A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu',
async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();
await page.getByRole('menuitem', { name: ' View Snapshot' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible();
await page.getByTitle('Annotate').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
await page.getByRole('button', { name: '' }).click();
// await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
//await expect(await page.locator)
}
);
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
test.fixme(
'5 Snapshots can be added to a container and Deleted with Delete All action',
@@ -116,10 +163,6 @@ test.describe('Snapshot Container tests', () => {
'A snapshot can be Deleted from Container with 3 dot action menu',
async ({ page }) => {}
);
test.fixme(
'A snapshot can be Navigated To from Container with 3 dot action menu',
async ({ page }) => {}
);
test.fixme(
'A snapshot can be Navigated To Item in Time from Container with 3 dot action menu',
async ({ page }) => {}
@@ -151,11 +194,4 @@ test.describe('Snapshot Container tests', () => {
//Snapshot removed from container?
}
);
test.fixme(
'Verify Embedded options for PNG, JPG, and Annotate work correctly',
async ({ page }) => {
//Add snapshot to container
//Verify PNG, JPG, and Annotate buttons work correctly
}
);
});

View File

@@ -24,138 +24,60 @@
Tests to verify log plot functionality when objects are missing
*/
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Handle missing object for plots', () => {
test('Displays empty div for missing stacked plot item @unstable', async ({
page,
browserName,
openmctConfig
}) => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');
const { myItemsFolderName } = openmctConfig;
const errorLogs = [];
let warningReceived = false;
page.on('console', (message) => {
if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
errorLogs.push(message.text());
warningReceived = true;
}
});
//Make stacked plot
await makeStackedPlot(page, myItemsFolderName);
const stackedPlot = await createDomainObjectWithDefaults(page, {
type: 'Stacked Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: stackedPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: stackedPlot.uuid
});
//Gets local storage and deletes the last sine wave generator in the stacked plot
const localStorage = await page.evaluate(() => window.localStorage);
const parsedData = JSON.parse(localStorage.mct);
const keys = Object.keys(parsedData);
const lastKey = keys[keys.length - 1];
const mct = await page.evaluate(() => window.localStorage.getItem('mct'));
const parsedData = JSON.parse(mct);
const key = Object.entries(parsedData).find(([, value]) => value.type === 'generator')?.[0];
delete parsedData[lastKey];
delete parsedData[key];
//Sets local storage with missing object
await page.evaluate(`window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`);
const jsonData = JSON.stringify(parsedData);
await page.evaluate((data) => {
window.localStorage.setItem('mct', data);
}, jsonData);
//Reloads page and clicks on stacked plot
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.goto(stackedPlot.url);
//Verify Main section is there on load
await expect
.soft(page.locator('.l-browse-bar__object-name'))
.toContainText('Unnamed Stacked Plot');
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(stackedPlot.name);
//Check that there is only one stacked item plot with a plot, the missing one will be empty
await expect(page.locator('.c-plot--stacked-container:has(.gl-plot)')).toHaveCount(1);
//Verify that console.warn is thrown
expect(errorLogs).toHaveLength(1);
await expect(page.getByLabel('Stacked Plot Item')).toHaveCount(1);
//Verify that console.warn was thrown
expect(warningReceived).toBe(true);
});
});
/**
* This is used the create a stacked plot object
* @private
*/
async function makeStackedPlot(page, myItemsFolderName) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('./', { waitUntil: 'domcontentloaded' });
// create stacked plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// save the stacked plot
await saveStackedPlot(page);
// create a sinewave generator
await createSineWaveGenerator(page);
// click on stacked plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
// create a second sinewave generator
await createSineWaveGenerator(page);
// click on stacked plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
}
/**
* This is used to save a stacked plot object
* @private
*/
async function saveStackedPlot(page) {
// save stacked plot
await page
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
.nth(1)
.click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
}
/**
* This is used to create a sine wave generator object
* @private
*/
async function createSineWaveGenerator(page) {
//Create sine wave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
}

View File

@@ -54,7 +54,9 @@ test.describe('Plots work in Previews', () => {
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(/Alpha-numeric telemetry value of.*/).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();

View File

@@ -114,7 +114,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA,
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337
@@ -122,7 +124,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA,
hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
});
@@ -143,7 +147,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -151,7 +157,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
@@ -160,7 +168,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
// Check styles on StackedPlot1
@@ -168,7 +176,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -176,7 +186,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
@@ -191,7 +203,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -199,7 +213,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
});
@@ -241,7 +257,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2 to verify they are the default
@@ -249,7 +267,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Set styles using setStyles function on StackedPlot2
@@ -258,7 +278,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot2 Frame')
page.getByRole('group', { name: 'StackedPlot2 Frame' })
);
// Check styles on StackedPlot2
@@ -266,7 +286,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
@@ -281,7 +303,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -289,7 +313,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Directly navigate to the flexible layout
@@ -326,7 +352,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2 matches previous set colors
@@ -334,7 +362,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
});
@@ -356,7 +386,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
// Check styles using checkStyles function
@@ -364,7 +394,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
@@ -386,7 +418,7 @@ test.describe('Flexible Layout styling', () => {
'No Style',
'No Style',
'No Style',
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
// Check styles using checkStyles function
@@ -394,7 +426,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(inheritedColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
await page.getByRole('button', { name: 'Save' }).click();
@@ -408,7 +442,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(inheritedColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
});

View File

@@ -67,7 +67,7 @@ test.describe('Style Inspector Options', () => {
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
// Select Stacked Layout Column
await page.getByLabel('Stacked Plot Frame').click();
await page.getByRole('group', { name: 'Stacked Plot Frame' }).click();
// The overall Flex Layout or Stacked Plot itself MUST be style-able.
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();

View File

@@ -20,10 +20,31 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';
import {
createDomainObjectWithDefaults,
setTimeConductorBounds,
setTimeConductorMode
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Telemetry Table', () => {
let table;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
});
test('Limits to 50 rows by default', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: table.uuid
});
await page.goto(table.url);
await setTimeConductorMode(page, false);
const rows = page.getByLabel('table content').getByLabel('Table Row');
await expect(rows).toHaveCount(50);
});
test('unpauses and filters data when paused by button and user changes bounds', async ({
page
}) => {
@@ -34,7 +55,6 @@ test.describe('Telemetry Table', () => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: table.uuid
@@ -78,7 +98,6 @@ test.describe('Telemetry Table', () => {
test('Supports filtering telemetry by regular text search', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
@@ -121,7 +140,6 @@ test.describe('Telemetry Table', () => {
test('Supports filtering using Regex', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid

View File

@@ -0,0 +1,64 @@
/*****************************************************************************
* 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,
setIndependentTimeConductorBounds
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
const FIXED_TIME =
'./#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true';
test.describe('Datepicker operations', () => {
test.beforeEach(async ({ page }) => {
await page.goto(FIXED_TIME);
});
test('Verify that user can use the datepicker in the TC', async ({ page }) => {
await page.getByLabel('Time Conductor Mode').click();
// Click on the date picker that is left-most on the screen
await page.getByLabel('Global Time Conductor').locator('a').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
// Click on the first cell
await page.getByText('27 239').click();
// Expect datepicker to close and time conductor date setting to be changed
await expect(page.getByRole('dialog')).toHaveCount(0);
});
test('Verify that user can use the datepicker in the ITC', async ({ page }) => {
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
await page.goto(createdTimeList.url, { waitUntil: 'domcontentloaded' });
await setIndependentTimeConductorBounds(page, {
start: '2024-11-12 19:11:11.000Z',
end: '2024-11-12 20:11:11.000Z'
});
// Open ITC
await page.getByLabel('Start bounds').nth(0).click();
// Click on the datepicker icon
await page.locator('form a').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
// Click on the first cell
await page.getByText('7 342').click();
// Expect datepicker to close and time conductor date setting to be changed
await expect(page.getByRole('dialog')).toHaveCount(0);
});
});

View File

@@ -109,7 +109,7 @@ test.describe('Verify tooltips', () => {
async function getToolTip(object) {
await page.locator('.c-create-button').hover();
await page.getByRole('cell', { name: object.name }).hover();
await page.getByLabel('lad name').getByText(object.name).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}

View File

@@ -0,0 +1,75 @@
/*****************************************************************************
* 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 '../../../baseFixtures.js';
// We don't need cspell to check this. It doesn't know latin.
/* cSpell:disable */
const loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Molestie at elementum eu facilisis sed. Feugiat pretium nibh ipsum consequat. Amet consectetur adipiscing elit duis tristique sollicitudin nibh sit amet. Eget nullam non nisi est sit amet. A pellentesque sit amet porttitor eget dolor morbi non arcu. Ullamcorper sit amet risus nullam eget felis eget nunc. In tellus integer feugiat scelerisque varius morbi enim nunc. Ac feugiat sed lectus vestibulum mattis ullamcorper. Nulla facilisi morbi tempus iaculis urna id volutpat. Massa vitae tortor condimentum lacinia quis vel eros donec. Ornare quam viverra orci sagittis eu. Vestibulum sed arcu non odio. In egestas erat imperdiet sed euismod nisi porta lorem. Vitae auctor eu augue ut lectus arcu bibendum at. Donec adipiscing tristique risus nec feugiat in fermentum posuere urna. Velit euismod in pellentesque massa placerat duis ultricies. Nulla facilisi nullam vehicula ipsum a arcu cursus vitae. Aliquam malesuada bibendum arcu vitae elementum curabitur.
Vel eros donec ac odio tempor orci. Et netus et malesuada fames ac turpis egestas sed tempus. Turpis egestas pretium aenean pharetra magna ac placerat. Euismod elementum nisi quis eleifend. Vitae auctor eu augue ut lectus arcu. At imperdiet dui accumsan sit amet nulla facilisi. Est velit egestas dui id ornare arcu odio ut sem. Ornare arcu dui vivamus arcu felis. Luctus venenatis lectus magna fringilla. At elementum eu facilisis sed. Tristique et egestas quis ipsum suspendisse ultrices gravida dictum. Enim eu turpis egestas pretium aenean pharetra magna ac placerat. Lobortis scelerisque fermentum dui faucibus in. Tempor orci eu lobortis elementum nibh tellus molestie nunc non. Dignissim convallis aenean et tortor at risus. Enim tortor at auctor urna nunc id cursus. Libero volutpat sed cras ornare arcu dui vivamus. Scelerisque fermentum dui faucibus in ornare quam viverra.
Odio ut sem nulla pharetra. Neque vitae tempus quam pellentesque nec. A arcu cursus vitae congue mauris. Turpis nunc eget lorem dolor sed viverra ipsum nunc aliquet. Nibh tellus molestie nunc non blandit massa enim nec. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Pulvinar elementum integer enim neque. Bibendum ut tristique et egestas. Nibh praesent tristique magna sit. Lectus magna fringilla urna porttitor. Eu non diam phasellus vestibulum lorem sed risus. Rhoncus mattis rhoncus urna neque. Rutrum tellus pellentesque eu tincidunt tortor aliquam. Pharetra convallis posuere morbi leo urna molestie at elementum. Quis commodo odio aenean sed adipiscing. Enim sit amet venenatis urna cursus eget nunc.
Enim nec dui nunc mattis. Cursus turpis massa tincidunt dui ut. Donec adipiscing tristique risus nec feugiat in. Eleifend mi in nulla posuere sollicitudin. Donec enim diam vulputate ut pharetra sit. Ultricies mi eget mauris pharetra et ultrices neque. Eros in cursus turpis massa tincidunt dui. Cursus risus at ultrices mi tempus imperdiet nulla malesuada. Morbi enim nunc faucibus a pellentesque sit. Porttitor rhoncus dolor purus non. Ac tortor vitae purus faucibus.
Proin libero nunc consequat interdum varius sit amet mattis vulputate. Metus dictum at tempor commodo ullamcorper a lacus vestibulum sed. Quisque non tellus orci ac auctor augue mauris. Id ornare arcu odio ut. Rhoncus est pellentesque elit ullamcorper dignissim. Senectus et netus et malesuada fames ac turpis egestas. Volutpat ac tincidunt vitae semper quis lectus nulla. Adipiscing elit duis tristique sollicitudin. Ipsum faucibus vitae aliquet nec ullamcorper sit. Gravida neque convallis a cras semper auctor neque vitae tempus. Porttitor leo a diam sollicitudin tempor id. Dictum non consectetur a erat nam at lectus. At volutpat diam ut venenatis tellus in. Morbi enim nunc faucibus a pellentesque sit amet. Cursus in hac habitasse platea. Sed augue lacus viverra vitae.
`;
test.describe('Inspector tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Content in inspector can be scrolled to vertically', async ({ page }) => {
const folderWithOverflowingTitle = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: loremIpsum
});
await page.goto(folderWithOverflowingTitle.url);
const inspectorPropertiesLocator = page
.getByRole('tabpanel', { name: 'Inspector Views' })
.getByLabel('Inspector Properties Details');
const inspectorPropertiesList = inspectorPropertiesLocator.getByRole('list');
const firstInspectorPropertyValue = inspectorPropertiesList
.getByRole('listitem')
.first()
.getByLabel('value', { exact: false });
const lastInspectorPropertyValue = inspectorPropertiesList
.getByRole('listitem')
.last()
.getByLabel('value', { exact: false });
// inspector content partially in viewport, but not all the way in viewport
await expect(inspectorPropertiesLocator).toBeInViewport();
await expect(inspectorPropertiesLocator).not.toBeInViewport({ ratio: 0.9 });
await expect(firstInspectorPropertyValue).toBeInViewport();
await expect(lastInspectorPropertyValue).not.toBeInViewport();
// using page.mouse.wheel to scroll the inspector content by the height of the content
// because click and scrollIntoView will scroll even if scrollbar not available
await inspectorPropertiesLocator.hover();
const offset = await inspectorPropertiesLocator.evaluate((el) => el.offsetHeight);
await page.mouse.wheel(0, offset);
await expect(lastInspectorPropertyValue).toBeInViewport();
});
});

View File

@@ -178,7 +178,7 @@ test.describe('Performance tests', () => {
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
// Click Close Icon
await page.locator('[aria-label="Close"]').click();
await page.getByRole('button', { name: 'Close' }).click();
await page.evaluate(() => window.performance.mark('view-large-close-button'));
//await client.send('HeapProfiler.enable');

View File

@@ -69,14 +69,14 @@ test.describe('Visual - Header @a11y', () => {
});
test('show snapshot button', async ({ page, theme }) => {
await page.getByLabel('Take a Notebook Snapshot').click();
await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {
scope: header
});
await expect(await page.getByLabel('Show Snapshots')).toBeVisible();
await expect(page.getByLabel('Show Snapshots')).toBeVisible();
});
});

View File

@@ -23,6 +23,7 @@
import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../appActions.js';
import { waitForAnimations } from '../../baseFixtures.js';
import { VISUAL_URL } from '../../constants.js';
import { expect, test } from '../../pluginFixtures.js';
@@ -75,11 +76,10 @@ test.describe('Visual - Example Imagery', () => {
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 waitForAnimations(page.locator('.animate-scroll'));
await percySnapshot(page, `Example Imagery in Real Time (theme: ${theme})`);
});

View File

@@ -23,7 +23,7 @@
import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults, expandTreePaneItemByName } from '../../appActions.js';
import { test } from '../../avpFixtures.js';
import { expect, test } from '../../avpFixtures.js';
import { VISUAL_URL } from '../../constants.js';
import { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js';
@@ -39,6 +39,44 @@ test.describe('Visual - Restricted Notebook @a11y', () => {
});
});
test.describe('Visual - Notebook Snapshot @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./?hideTree=true&hideInspector=true', { waitUntil: 'domcontentloaded' });
});
test('Visual check for Snapshot Annotation', async ({ page, theme }) => {
await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await page.getByLabel('Show Snapshots').click();
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
await percySnapshot(page, `Notebook Snapshot with text entry open (theme: '${theme}')`);
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
// Take a snapshot
await percySnapshot(page, `Notebook Snapshot with annotation (theme: '${theme}')`);
});
});
test.describe('Visual - Notebook @a11y', () => {
let notebook;
test.beforeEach(async ({ page }) => {

View File

@@ -72,11 +72,29 @@ test.describe('Visual - Planning', () => {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`);
});
test('Resize Plan View @2p', async ({ browser, theme }) => {
// need to set viewport to null to allow for resizing
const newContext = await browser.newContext({
viewport: null
});
const newPage = await newContext.newPage();
await newPage.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
const plan = await createPlanFromJSON(newPage, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(newPage, examplePlanSmall2, plan.url);
// resize the window
await newPage.setViewportSize({ width: 800, height: 600 });
await percySnapshot(newPage, `Plan View resized (theme: ${theme})`);
});
test('Plan View w/ draft status', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test (Draft)',

View File

@@ -91,7 +91,7 @@ test.describe('Flexible Layout styling @a11y', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
await percySnapshot(

View File

@@ -53,11 +53,11 @@ test.describe('Visual - Telemetry Views', () => {
await page.goto(telemetry.url, { waitUntil: 'domcontentloaded' });
//Click this button to see telemetry display options
await page.getByRole('button', { name: 'Plot' }).click();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Telemetry Table').click();
//Get Table View in place
expect(await page.getByLabel('Expand Columns')).toBeInViewport();
await expect(page.getByLabel('Expand Columns')).toBeInViewport();
await percySnapshot(page, `Default Telemetry Table View (theme: ${theme})`);

View File

@@ -55,6 +55,7 @@
</template>
<script>
const ONE_HOUR = 60 * 60 * 1000;
export default {
inject: ['openmct', 'domainObject'],
data() {
@@ -77,6 +78,10 @@ export default {
selectItem(item, event) {
event.stopPropagation();
const bounds = this.openmct.time.getBounds();
const otherBounds = {
start: bounds.start - ONE_HOUR,
end: bounds.end + ONE_HOUR
};
const selection = [
{
element: this.$el,
@@ -88,6 +93,9 @@ export default {
icon: item.type.cssClass
},
dataRanges: [
{
bounds: otherBounds
},
{
bounds
}

View File

@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

12271
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,20 +10,20 @@
"@braintree/sanitize-url": "6.0.4",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.39.0",
"@playwright/test": "1.42.1",
"@types/d3-axis": "3.0.6",
"@types/d3-shape": "3.0.0",
"@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "5.1.2",
"@types/lodash": "4.14.192",
"@types/lodash": "4.17.0",
"@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "11.0.0",
"copy-webpack-plugin": "12.0.2",
"cspell": "7.3.8",
"css-loader": "6.10.0",
"d3-axis": "3.0.0",
@@ -45,7 +45,7 @@
"flatbush": "4.2.0",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
"imports-loader": "5.0.0",
"jasmine-core": "5.1.1",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
@@ -56,7 +56,7 @@
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.4.0",
"karma-spec-reporter": "0.0.36",
"karma-webpack": "5.0.0",
"karma-webpack": "5.0.1",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"marked": "12.0.0",
@@ -64,7 +64,7 @@
"moment": "2.30.1",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41",
"npm-run-all2": "6.1.1",
"npm-run-all2": "6.1.2",
"nyc": "15.1.0",
"painterro": "1.2.87",
"plotly.js-basic-dist-min": "2.29.1",
@@ -74,7 +74,7 @@
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.12.1",
"sass": "1.68.0",
"sass": "1.71.1",
"sass-loader": "14.1.1",
"sinon": "17.0.0",
"style-loader": "3.3.3",
@@ -91,7 +91,7 @@
"webpack-merge": "5.10.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./test-results ./.nyc_output ",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
@@ -156,4 +156,4 @@
"keywords": [
"nasa"
]
}
}

View File

@@ -29,10 +29,13 @@
:key="action.name"
role="menuitem"
:aria-disabled="action.isDisabled"
:class="action.cssClass"
:aria-label="action.name"
aria-describedby="item-description"
:class="action.cssClass"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItem(action)"
@mouseleave="toggleItem()"
>
{{ action.name }}
</li>
@@ -52,16 +55,23 @@
v-for="action in options.actions"
:key="action.name"
role="menuitem"
aria-describedby="item-description"
:aria-disabled="action.isDisabled"
:class="action.cssClass"
:aria-label="action.name"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItem(action)"
@mouseleave="toggleItem()"
>
{{ action.name }}
</li>
<li v-if="options.actions.length === 0">No actions defined.</li>
</ul>
<div v-if="hoveredItem" id="item-description" class="visually-hidden" aria-live="polite">
<span v-if="hoveredItem.name">{{ hoveredItem.name }}</span>
<span v-if="hoveredItem.description">: {{ hoveredItem.description }}</span>
</div>
</div>
</template>
@@ -70,11 +80,21 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
export default {
mixins: [popupMenuMixin],
inject: ['options'],
data() {
return {
hoveredItem: null
};
},
computed: {
optionsLabel() {
const label = this.options.label ? `${this.options.label} Menu` : 'Menu';
const label = this.options.label ? `${this.options.label} Context Menu` : 'Context Menu';
return label;
}
},
methods: {
toggleItem(action) {
this.hoveredItem = action ?? null;
}
}
};
</script>

View File

@@ -38,8 +38,8 @@
:key="action.name"
role="menuitem"
:aria-disabled="action.isDisabled"
aria-describedby="item-description"
:class="action.cssClass"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -64,7 +64,7 @@
role="menuitem"
:class="action.cssClass"
:aria-label="action.name"
:title="action.description"
aria-describedby="item-description"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -74,13 +74,13 @@
<li v-if="options.actions.length === 0">No actions defined.</li>
</ul>
<div class="c-super-menu__item-description">
<div :class="['l-item-description__icon', 'bg-' + hoveredItem.cssClass]"></div>
<div aria-live="polite" class="c-super-menu__item-description">
<div :class="itemDescriptionIconClass"></div>
<div class="l-item-description__name">
{{ hoveredItem.name }}
{{ hoveredItemName }}
</div>
<div class="l-item-description__description">
{{ hoveredItem.description }}
<div id="item-description" class="l-item-description__description">
{{ hoveredItemDescription }}
</div>
</div>
</div>
@@ -90,26 +90,39 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
export default {
mixins: [popupMenuMixin],
inject: ['options'],
data: function () {
data() {
return {
hoveredItem: {}
hoveredItem: null
};
},
computed: {
optionsLabel() {
const label = this.options.label ? `${this.options.label} Super Menu` : 'Super Menu';
return label;
},
itemDescriptionIconClass() {
const iconClass = ['l-item-description__icon'];
if (this.hoveredItem) {
iconClass.push('bg-' + this.hoveredItem.cssClass);
}
return iconClass;
},
hoveredItemName() {
return this.hoveredItem?.name ?? '';
},
hoveredItemDescription() {
return this.hoveredItem?.description ?? '';
}
},
methods: {
toggleItemDescription(action = {}) {
toggleItemDescription(action = null) {
const hoveredItem = {
name: action.name,
description: action.description,
cssClass: action.cssClass
name: action?.name,
description: action?.description,
cssClass: action?.cssClass
};
this.hoveredItem = Object.assign({}, this.hoveredItem, hoveredItem);
this.hoveredItem = hoveredItem;
}
}
};

View File

@@ -249,7 +249,7 @@ export default class ObjectAPI {
.get(identifier, abortSignal)
.then((domainObject) => {
delete this.cache[keystring];
if (!domainObject && abortSignal.aborted) {
if (!domainObject && abortSignal?.aborted) {
// we've aborted the request
return;
}

View File

@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-overlay js-overlay">
<div class="c-overlay js-overlay" role="dialog" aria-modal="true" aria-label="Modal Overlay">
<div class="c-overlay__blocker" @click="destroy"></div>
<div class="c-overlay__outer">
<button
@@ -34,9 +34,6 @@
ref="element"
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0"
aria-modal="true"
aria-label="Overlay"
role="dialog"
></div>
<div v-if="buttons" class="c-overlay__button-bar">
<button
@@ -61,7 +58,7 @@
export default {
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
emits: ['destroy'],
data: function () {
data() {
return {
focusIndex: -1
};

View File

@@ -20,7 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import installWorker from './WebSocketWorker.js';
const DEFAULT_RATE_MS = 1000;
/**
* Describes the strategy to be used when batching WebSocket messages
*
@@ -51,11 +50,21 @@ const DEFAULT_RATE_MS = 1000;
*
* @memberof module:openmct.telemetry
*/
// Shim for Internet Explorer, I mean Safari. It doesn't support requestIdleCallback, but it's in a tech preview, so it will be dropping soon.
const requestIdleCallback =
// eslint-disable-next-line compat/compat
window.requestIdleCallback ?? ((fn, { timeout }) => setTimeout(fn, timeout));
const ONE_SECOND = 1000;
const FIVE_SECONDS = 5 * ONE_SECOND;
class BatchingWebSocket extends EventTarget {
#worker;
#openmct;
#showingRateLimitNotification;
#rate;
#maxBatchSize;
#applicationIsInitializing;
#maxBatchWait;
#firstBatchReceived;
constructor(openmct) {
super();
@@ -66,7 +75,10 @@ class BatchingWebSocket extends EventTarget {
this.#worker = new Worker(workerUrl);
this.#openmct = openmct;
this.#showingRateLimitNotification = false;
this.#rate = DEFAULT_RATE_MS;
this.#maxBatchSize = Number.POSITIVE_INFINITY;
this.#maxBatchWait = ONE_SECOND;
this.#applicationIsInitializing = true;
this.#firstBatchReceived = false;
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
this.#worker.addEventListener('message', routeMessageToHandler);
@@ -78,6 +90,20 @@ class BatchingWebSocket extends EventTarget {
},
{ once: true }
);
openmct.once('start', () => {
// An idle callback is a pretty good indication that a complex display is done loading. At that point set the batch size more conservatively.
// Force it after 5 seconds if it hasn't happened yet.
requestIdleCallback(
() => {
this.#applicationIsInitializing = false;
this.setMaxBatchSize(this.#maxBatchSize);
},
{
timeout: FIVE_SECONDS
}
);
});
}
/**
@@ -129,14 +155,6 @@ class BatchingWebSocket extends EventTarget {
});
}
/**
* When using batching, sets the rate at which batches of messages are released.
* @param {Number} rate the amount of time to wait, in ms, between batches.
*/
setRate(rate) {
this.#rate = rate;
}
/**
* @param {Number} maxBatchSize the maximum length of a batch of messages. For example,
* the maximum number of telemetry values to batch before dropping them
@@ -151,12 +169,29 @@ class BatchingWebSocket extends EventTarget {
* 15 would probably be a better batch size.
*/
setMaxBatchSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize;
if (!this.#applicationIsInitializing) {
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
}
}
setMaxBatchWait(wait) {
this.#maxBatchWait = wait;
this.#sendBatchWaitToWorker(this.#maxBatchWait);
}
#sendMaxBatchSizeToWorker(maxBatchSize) {
this.#worker.postMessage({
type: 'setMaxBatchSize',
maxBatchSize
});
}
#sendBatchWaitToWorker(maxBatchWait) {
this.#worker.postMessage({
type: 'setMaxBatchWait',
maxBatchWait
});
}
/**
* Disconnect the associated WebSocket. Generally speaking there is no need to call
* this manually.
@@ -169,7 +204,9 @@ class BatchingWebSocket extends EventTarget {
#routeMessageToHandler(message) {
if (message.data.type === 'batch') {
if (message.data.batch.dropped === true && !this.#showingRateLimitNotification) {
this.start = Date.now();
const batch = message.data.batch;
if (batch.dropped === true && !this.#showingRateLimitNotification) {
const notification = this.#openmct.notifications.alert(
'Telemetry dropped due to client rate limiting.',
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
@@ -179,16 +216,45 @@ class BatchingWebSocket extends EventTarget {
this.#showingRateLimitNotification = false;
});
}
this.dispatchEvent(new CustomEvent('batch', { detail: message.data.batch }));
setTimeout(() => {
this.#readyForNextBatch();
}, this.#rate);
this.dispatchEvent(new CustomEvent('batch', { detail: batch }));
this.#waitUntilIdleAndRequestNextBatch(batch);
} else if (message.data.type === 'message') {
this.dispatchEvent(new CustomEvent('message', { detail: message.data.message }));
} else if (message.data.type === 'reconnected') {
this.dispatchEvent(new CustomEvent('reconnected'));
} else {
throw new Error(`Unknown message type: ${message.data.type}`);
}
}
#waitUntilIdleAndRequestNextBatch(batch) {
requestIdleCallback(
(state) => {
if (this.#firstBatchReceived === false) {
this.#firstBatchReceived = true;
}
const now = Date.now();
const waitedFor = now - this.start;
if (state.didTimeout === true) {
if (document.visibilityState === 'visible') {
console.warn(`Event loop is too busy to process batch.`);
this.#waitUntilIdleAndRequestNextBatch(batch);
} else {
// After ingesting a telemetry batch, wait until the event loop is idle again before
// informing the worker we are ready for another batch.
this.#readyForNextBatch();
}
} else {
if (waitedFor > ONE_SECOND) {
console.warn(`Warning, batch processing took ${waitedFor}ms`);
}
this.#readyForNextBatch();
}
},
{ timeout: ONE_SECOND }
);
}
}
export default BatchingWebSocket;

View File

@@ -85,6 +85,7 @@ const SUBSCRIBE_STRATEGY = {
export default class TelemetryAPI {
#isGreedyLAD;
#subscribeCache;
#hasReturnedFirstData;
get SUBSCRIBE_STRATEGY() {
return SUBSCRIBE_STRATEGY;
@@ -108,6 +109,7 @@ export default class TelemetryAPI {
this.#isGreedyLAD = true;
this.BatchingWebSocket = BatchingWebSocket;
this.#subscribeCache = {};
this.#hasReturnedFirstData = false;
}
abortAllRequests() {
@@ -383,7 +385,10 @@ export default class TelemetryAPI {
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
try {
const telemetry = await provider.request(...arguments);
if (!this.#hasReturnedFirstData) {
this.#hasReturnedFirstData = true;
performance.mark('firstHistoricalDataReturned');
}
return telemetry;
} catch (error) {
if (error.name !== 'AbortError') {

View File

@@ -442,8 +442,12 @@ export default class TelemetryCollection extends EventEmitter {
} else {
this.timeKey = undefined;
this._warn(TIMESYSTEM_KEY_WARNING);
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
// missing objects will never have a domain, if one happens to get through
// to this point this warning/notification does not apply
if (!this.openmct.objects.isMissing(this.domainObject)) {
this._warn(TIMESYSTEM_KEY_WARNING);
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
}
}
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);

View File

@@ -21,6 +21,7 @@
*****************************************************************************/
/* eslint-disable max-classes-per-file */
export default function installWorker() {
const ONE_SECOND = 1000;
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
/**
@@ -44,6 +45,13 @@ export default function installWorker() {
#currentWaitIndex = 0;
#messageCallbacks = [];
#wsUrl;
#reconnecting = false;
#worker;
constructor(worker) {
super();
this.#worker = worker;
}
/**
* Establish a new WebSocket connection to the given URL
@@ -62,6 +70,9 @@ export default function installWorker() {
this.#isConnecting = true;
this.#webSocket = new WebSocket(url);
//Exposed to e2e tests so that the websocket can be manipulated during tests. Cannot find any other way to do this.
// Playwright does not support forcing websocket state changes.
this.#worker.currentWebSocket = this.#webSocket;
const boundConnected = this.#connected.bind(this);
this.#webSocket.addEventListener('open', boundConnected);
@@ -100,12 +111,17 @@ export default function installWorker() {
}
#connected() {
console.debug('Websocket connected.');
console.info('Websocket connected.');
this.#isConnected = true;
this.#isConnecting = false;
this.#currentWaitIndex = 0;
this.dispatchEvent(new Event('connected'));
if (this.#reconnecting) {
this.#worker.postMessage({
type: 'reconnected'
});
this.#reconnecting = false;
}
this.#flushQueue();
}
@@ -138,6 +154,7 @@ export default function installWorker() {
if (this.#reconnectTimeoutHandle) {
return;
}
this.#reconnecting = true;
this.#reconnectTimeoutHandle = setTimeout(() => {
this.connect(this.#wsUrl);
@@ -207,6 +224,9 @@ export default function installWorker() {
case 'setMaxBatchSize':
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
break;
case 'setMaxBatchWait':
this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait);
break;
default:
throw new Error(`Unknown message type: ${type}`);
}
@@ -245,7 +265,6 @@ export default function installWorker() {
}
routeMessageToHandler(data) {
//Implement batching here
if (this.#messageBatcher.shouldBatchMessage(data)) {
this.#messageBatcher.addMessageToBatch(data);
} else {
@@ -267,12 +286,15 @@ export default function installWorker() {
#maxBatchSize;
#readyForNextBatch;
#worker;
#throttledSendNextBatch;
constructor(worker) {
this.#maxBatchSize = 10;
// No dropping telemetry unless we're explicitly told to.
this.#maxBatchSize = Number.POSITIVE_INFINITY;
this.#readyForNextBatch = false;
this.#worker = worker;
this.#resetBatch();
this.setMaxBatchWait(ONE_SECOND);
}
#resetBatch() {
this.#batch = {};
@@ -310,23 +332,29 @@ export default function installWorker() {
const batchId = this.#batchingStrategy.getBatchIdFromMessage(message);
let batch = this.#batch[batchId];
if (batch === undefined) {
this.#hasBatch = true;
batch = this.#batch[batchId] = [message];
} else {
batch.push(message);
}
if (batch.length > this.#maxBatchSize) {
console.warn(
`Exceeded max batch size of ${this.#maxBatchSize} for ${batchId}. Dropping value.`
);
batch.shift();
this.#batch.dropped = this.#batch.dropped || true;
this.#batch.dropped = true;
}
if (this.#readyForNextBatch) {
this.#sendNextBatch();
} else {
this.#hasBatch = true;
this.#throttledSendNextBatch();
}
}
setMaxBatchSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize;
}
setMaxBatchWait(maxBatchWait) {
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), maxBatchWait);
}
/**
* Indicates that client code is ready to receive the next batch of
* messages. If a batch is available, it will be immediately sent.
@@ -335,7 +363,7 @@ export default function installWorker() {
*/
readyForNextBatch() {
if (this.#hasBatch) {
this.#sendNextBatch();
this.#throttledSendNextBatch();
} else {
this.#readyForNextBatch = true;
}
@@ -352,7 +380,34 @@ export default function installWorker() {
}
}
const websocket = new ResilientWebSocket();
function throttle(callback, wait) {
let last = 0;
let throttling = false;
return function (...args) {
if (throttling) {
return;
}
const now = performance.now();
const timeSinceLast = now - last;
if (timeSinceLast >= wait) {
last = now;
callback(...args);
} else if (!throttling) {
throttling = true;
setTimeout(() => {
last = performance.now();
throttling = false;
callback(...args);
}, wait - timeSinceLast);
}
};
}
const websocket = new ResilientWebSocket(self);
const messageBatcher = new MessageBatcher(self);
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
@@ -363,4 +418,6 @@ export default function installWorker() {
websocket.registerMessageCallback((data) => {
websocketBroker.routeMessageToHandler(data);
});
self.websocketInstance = websocket;
}

View File

@@ -24,11 +24,13 @@
<tr
ref="tableRow"
class="js-lad-table__body__row c-table__selectable-row"
aria-label="lad row"
@click="clickedRow"
@contextmenu.prevent="showContextMenu"
>
<td
ref="tableCell"
aria-label="lad name"
class="js-first-data"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
@@ -58,7 +60,7 @@
const CONTEXT_MENU_ACTIONS = ['viewDatumAction', 'viewHistoricalData', 'remove'];
const BLANK_VALUE = '---';
import identifierToString from '/src/tools/url.js';
import { objectPathToUrl } from '/src/tools/url.js';
import PreviewAction from '@/ui/preview/PreviewAction.js';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
@@ -260,7 +262,7 @@ export default {
event.preventDefault();
this.preview(this.objectPath);
} else {
const resultUrl = identifierToString(this.openmct, this.objectPath);
const resultUrl = objectPathToUrl(this.openmct, this.objectPath);
this.openmct.router.navigate(resultUrl);
}
},

View File

@@ -22,7 +22,7 @@
<template>
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver" :class="staleClass">
<table class="c-table c-lad-table" :class="applyLayoutClass">
<table aria-label="lad table" class="c-table c-lad-table" :class="applyLayoutClass">
<thead>
<tr>
<th>Name</th>

View File

@@ -25,6 +25,7 @@
aria-label="Clock Indicator"
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
role="complementary"
aria-live="off"
>
<span class="label c-indicator__label">
{{ timeTextValue }}

View File

@@ -56,20 +56,38 @@ export default class ConditionManager extends EventEmitter {
);
}
subscribeToTelemetry(endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
if (this.subscriptions[id]) {
console.log('subscription already exists');
async requestLatestValue(endpoint) {
const options = {
size: 1,
strategy: 'latest'
};
const latestData = await this.openmct.telemetry.request(endpoint, options);
if (!latestData) {
throw new Error('Telemetry request failed by returning a falsy response');
}
if (latestData.length === 0) {
return;
}
this.telemetryReceived(endpoint, latestData[0]);
}
subscribeToTelemetry(endpoint) {
const telemetryKeyString = this.openmct.objects.makeKeyString(endpoint.identifier);
if (this.subscriptions[telemetryKeyString]) {
return;
}
const metadata = this.openmct.telemetry.getMetadata(endpoint);
this.telemetryObjects[id] = Object.assign({}, endpoint, {
this.telemetryObjects[telemetryKeyString] = Object.assign({}, endpoint, {
telemetryMetaData: metadata ? metadata.valueMetadatas : []
});
this.subscriptions[id] = this.openmct.telemetry.subscribe(
// get latest telemetry value (in case subscription is cached and no new data is coming in)
this.requestLatestValue(endpoint);
this.subscriptions[telemetryKeyString] = this.openmct.telemetry.subscribe(
endpoint,
this.telemetryReceived.bind(this, endpoint)
);
@@ -91,7 +109,7 @@ export default class ConditionManager extends EventEmitter {
//force re-computation of condition set result as we might be in a state where
// there is no telemetry datum coming in for a while or at all.
let latestTimestamp = getLatestTimestamp(
const latestTimestamp = getLatestTimestamp(
{},
{},
this.timeSystems,
@@ -334,57 +352,54 @@ export default class ConditionManager extends EventEmitter {
return currentCondition;
}
requestLADConditionSetOutput(options) {
async requestLADConditionSetOutput(options) {
if (!this.conditions.length) {
return Promise.resolve([]);
return [];
}
return this.compositionLoad.then(() => {
let latestTimestamp;
let conditionResults = {};
let nextLegOptions = { ...options };
delete nextLegOptions.onPartialResponse;
await this.compositionLoad;
const conditionRequests = this.conditions.map((condition) =>
condition.requestLADConditionResult(nextLegOptions)
let latestTimestamp;
let conditionResults = {};
let nextLegOptions = { ...options };
delete nextLegOptions.onPartialResponse;
const results = await Promise.all(
this.conditions.map((condition) => condition.requestLADConditionResult(nextLegOptions))
);
results.forEach((resultObj) => {
const {
id,
data,
data: { result }
} = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = Boolean(result);
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
return Promise.all(conditionRequests).then((results) => {
results.forEach((resultObj) => {
const {
id,
data,
data: { result }
} = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = Boolean(result);
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) {
return [];
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
const currentOutput = Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
latestTimestamp
);
return [currentOutput];
});
});
if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) {
return [];
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
const currentOutput = {
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id,
...latestTimestamp
};
return [currentOutput];
}
isTelemetryUsed(endpoint) {
@@ -409,7 +424,7 @@ export default class ConditionManager extends EventEmitter {
}
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.timeSystem().key;
const timeSystemKey = this.openmct.time.getTimeSystem().key;
let timestamp = {};
const currentTimestamp = normalizedDatum[timeSystemKey];
timestamp[timeSystemKey] = currentTimestamp;

View File

@@ -40,12 +40,10 @@ export default class ConditionSetTelemetryProvider {
return domainObject.type === 'conditionSet';
}
request(domainObject, options) {
async request(domainObject, options) {
let conditionManager = this.getConditionManager(domainObject);
return conditionManager.requestLADConditionSetOutput(options).then((latestOutput) => {
return latestOutput;
});
let latestOutput = await conditionManager.requestLADConditionSetOutput(options);
return latestOutput;
}
subscribe(domainObject, callback) {

View File

@@ -66,7 +66,8 @@ describe('the plugin', function () {
format: 'utc',
hints: {
domain: 1
}
},
source: 'utc'
},
{
key: 'testSource',
@@ -720,6 +721,23 @@ describe('the plugin', function () {
});
it('should evaluate as old when telemetry is not received in the allotted time', (done) => {
openmct.telemetry = jasmine.createSpyObj('telemetry', [
'subscribe',
'getMetadata',
'request',
'getValueFormatter',
'abortAllRequests'
]);
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: []
});
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) {
return value;
}
});
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
@@ -741,6 +759,20 @@ describe('the plugin', function () {
});
it('should not evaluate as old when telemetry is received in the allotted time', (done) => {
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: testTelemetryObject.telemetry.values
});
const testDatum = {
'some-key2': '',
utc: 1,
testSource: '',
'some-key': null,
id: 'test-object'
};
openmct.telemetry.request = jasmine.createSpy('request');
openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum]));
const date = 1;
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input =
['0.4'];
@@ -750,9 +782,7 @@ describe('the plugin', function () {
'test-object': testTelemetryObject
};
conditionMgr.updateConditionTelemetryObjects();
conditionMgr.telemetryReceived(testTelemetryObject, {
utc: date
});
conditionMgr.telemetryReceived(testTelemetryObject, testDatum);
setTimeout(() => {
expect(mockListener).toHaveBeenCalledWith({
output: 'Default',
@@ -868,6 +898,12 @@ describe('the plugin', function () {
it('should stop evaluating conditions when a condition evaluates to true', () => {
const date = Date.now();
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: []
});
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
'test-object': testTelemetryObject

View File

@@ -22,6 +22,7 @@
<template>
<div
aria-label="sub object frame"
class="l-layout__frame c-frame"
:class="{
'no-frame': !item.hasFrame,

View File

@@ -37,24 +37,24 @@
:style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
aria-label="Alpha-numeric telemetry"
@contextmenu.prevent="showContextMenu"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
<div
class="is-status__indicator"
:aria-label="`This item is ${status}`"
:title="`This item is ${status}`"
></div>
<div class="is-status__indicator"></div>
<div v-if="showLabel" class="c-telemetry-view__label">
<div class="c-telemetry-view__label-text">
<div
class="c-telemetry-view__label-text"
:aria-label="`Alpha-numeric telemetry name for ${domainObject.name}`"
>
{{ domainObject.name }}
</div>
</div>
<div
v-if="showValue"
:aria-label="fieldName"
:aria-label="`Alpha-numeric telemetry value of ${telemetryValue}`"
:title="fieldName"
class="c-telemetry-view__value"
:class="[telemetryClass]"

View File

@@ -220,6 +220,7 @@
lengthAdjust="spacing"
text-anchor="middle"
dominant-baseline="middle"
:aria-label="`gauge value of ${curVal}`"
x="50%"
y="50%"
>

View File

@@ -150,16 +150,15 @@ export default class ImportAsJSONAction {
* @param {string} namespace
* @returns {object}
*/
_generateNewIdentifiers(tree, namespace) {
_generateNewIdentifiers(tree, newNamespace) {
// For each domain object in the file, generate new ID, replace in tree
Object.keys(tree.openmct).forEach((domainObjectId) => {
const newId = {
namespace,
key: uuid()
};
const oldId = parseKeyString(domainObjectId);
const newId = {
namespace: newNamespace,
key: uuid()
};
tree = this._rewriteId(oldId, newId, tree);
}, this);
@@ -228,22 +227,32 @@ export default class ImportAsJSONAction {
_rewriteId(oldId, newId, tree) {
let newIdKeyString = this.openmct.objects.makeKeyString(newId);
let oldIdKeyString = this.openmct.objects.makeKeyString(oldId);
tree = JSON.stringify(tree).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString);
return JSON.parse(tree, (key, value) => {
const newTreeString = JSON.stringify(tree).replace(
new RegExp(oldIdKeyString, 'g'),
newIdKeyString
);
const newTree = JSON.parse(newTreeString, (key, value) => {
if (
value !== undefined &&
value !== null &&
Object.prototype.hasOwnProperty.call(value, 'key') &&
Object.prototype.hasOwnProperty.call(value, 'namespace') &&
value.key === oldId.key &&
value.namespace === oldId.namespace
Object.prototype.hasOwnProperty.call(value, 'namespace')
) {
return newId;
} else {
return value;
// first check if key is messed up from regex and contains a colon
// if it does, repair it
if (value.key.includes(':')) {
const splitKey = value.key.split(':');
value.key = splitKey[1];
value.namespace = splitKey[0];
}
// now check if we need to replace the id
if (value.key === oldId.key && value.namespace === oldId.namespace) {
return newId;
}
}
return value;
});
return newTree;
}
/**
* @private

View File

@@ -135,11 +135,75 @@ describe('The import JSON action', function () {
selectFile: {
name: 'imported object',
// eslint-disable-next-line prettier/prettier
body: "{\"openmct\":{\"c28d230d-e909-4a3e-9840-d9ef469dda70\":{\"identifier\":{\"key\":\"c28d230d-e909-4a3e-9840-d9ef469dda70\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[],\"configuration\":{\"series\":[]},\"modified\":1695837546833,\"location\":\"mine\",\"created\":1695837546833,\"persisted\":1695837546833,\"__proto__\":{\"toString\":\"foobar\"}}},\"rootId\":\"c28d230d-e909-4a3e-9840-d9ef469dda70\"}"
body: '{"openmct":{"c28d230d-e909-4a3e-9840-d9ef469dda70":{"identifier":{"key":"c28d230d-e909-4a3e-9840-d9ef469dda70","namespace":""},"name":"Unnamed Overlay Plot","type":"telemetry.plot.overlay","composition":[],"configuration":{"series":[]},"modified":1695837546833,"location":"mine","created":1695837546833,"persisted":1695837546833,"__proto__":{"toString":"foobar"}}},"rootId":"c28d230d-e909-4a3e-9840-d9ef469dda70"}'
}
};
return Promise.resolve(pollutedResponse);
}
});
it('preserves the integrity of the namespace and key during import', async () => {
const incomingObject = {
openmct: {
'7323f02a-06ac-438d-bd58-6d6e33b8741e': {
name: 'Some Folder',
type: 'folder',
composition: [
{
key: '9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6',
namespace: ''
}
],
modified: 1710843256162,
location: 'mine',
created: 1710843243471,
persisted: 1710843256162,
identifier: {
namespace: '',
key: '7323f02a-06ac-438d-bd58-6d6e33b8741e'
}
},
'9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6': {
name: 'Some Clock',
type: 'clock',
configuration: {
baseFormat: 'YYYY/MM/DD hh:mm:ss',
use24: 'clock12',
timezone: 'UTC'
},
modified: 1710843256152,
location: '7323f02a-06ac-438d-bd58-6d6e33b8741e',
created: 1710843256152,
persisted: 1710843256152,
identifier: {
namespace: '',
key: '9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6'
}
}
},
rootId: '7323f02a-06ac-438d-bd58-6d6e33b8741e'
};
const targetDomainObject = {
identifier: {
namespace: 'starJones',
key: '84438cda-a071-48d1-b9bf-d77bd53e59ba'
},
type: 'folder'
};
spyOn(openmct.objects, 'save').and.callFake((model) => Promise.resolve(model));
try {
await importFromJSONAction.onSave(targetDomainObject, {
selectFile: { body: JSON.stringify(incomingObject) }
});
for (const callArgs of openmct.objects.save.calls.allArgs()) {
const savedObject = callArgs[0]; // Assuming the first argument is the object being saved.
expect(savedObject.identifier.key.includes(':')).toBeFalse(); // Ensure no colon in the key.
expect(savedObject.identifier.namespace).toBe(targetDomainObject.identifier.namespace);
}
} catch (error) {
fail(error);
}
});
});

View File

@@ -25,7 +25,18 @@
<div class="c-inspect-properties">
<div class="c-inspect-properties__header">Numeric Data</div>
</div>
<div ref="numericDataView"></div>
<div ref="numericDataView">
<TelemetryFrame
v-for="plotObject of plotObjects"
:key="plotObject.identifier.key"
:bounds="bounds"
:telemetry-object="plotObject"
:path="[plotObject]"
:render-when-visible="plotObject.renderWhenVisible"
>
<Plot />
</TelemetryFrame>
</div>
<div v-if="!hasNumericData">
{{ noNumericDataText }}
@@ -33,13 +44,15 @@
</div>
</template>
<script>
import mount from 'utils/mount';
import VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';
import Plot from '../plot/PlotView.vue';
import TelemetryFrame from './TelemetryFrame.vue';
export default {
components: {
TelemetryFrame,
Plot
},
inject: ['openmct', 'domainObject', 'timeFormatter'],
props: {
bounds: {
@@ -90,16 +103,19 @@ export default {
this.clearPlots();
this.unregisterTimeContextList = [];
this.componentsList = [];
this.elementsList = [];
this.visibilityObservers = [];
this.telemetryKeys.forEach(async (telemetryKey) => {
const plotObject = await this.openmct.objects.get(telemetryKey);
const visibilityObserver = new VisibilityObserver(
this.$refs.numericDataView,
this.openmct.element
);
plotObject.renderWhenVisible = visibilityObserver.renderWhenVisible;
this.visibilityObservers.push(visibilityObserver);
this.plotObjects.push(plotObject);
this.unregisterTimeContextList.push(this.setIndependentTimeContextForComponent(plotObject));
this.renderPlot(plotObject);
});
},
setIndependentTimeContextForComponent(plotObject) {
@@ -110,63 +126,14 @@ export default {
// set the time context of the object to the selected time range
return this.openmct.time.addIndependentContext(keyString, this.bounds);
},
renderPlot(plotObject) {
const wrapper = document.createElement('div');
const visibilityObserver = new VisibilityObserver(wrapper, this.openmct.element);
const { destroy } = mount(
{
components: {
TelemetryFrame,
Plot
},
provide: {
openmct: this.openmct,
path: [plotObject],
renderWhenVisible: visibilityObserver.renderWhenVisible
},
data() {
return {
plotObject,
bounds: this.bounds
};
},
template: `<TelemetryFrame
:bounds="bounds"
:telemetry-object="plotObject"
>
<Plot />
</TelemetryFrame>`
},
{
app: this.openmct.app,
element: wrapper
}
);
this.componentsList.push(destroy);
this.elementsList.push(wrapper);
this.visibilityObservers.push(visibilityObserver);
this.$refs.numericDataView.append(wrapper);
},
clearPlots() {
if (this.componentsList?.length) {
this.componentsList.forEach((destroy) => destroy());
delete this.componentsList;
}
if (this.elementsList?.length) {
this.elementsList.forEach((element) => element.remove());
delete this.elementsList;
}
if (this.visibilityObservers?.length) {
this.visibilityObservers.forEach((visibilityObserver) => visibilityObserver.destroy());
delete this.visibilityObservers;
}
if (this.plotObjects?.length) {
this.plotObjects = [];
this.plotObjects.splice(0, this.plotObjects.length);
}
if (this.unregisterTimeContextList?.length) {

View File

@@ -70,7 +70,9 @@ export default {
inject: ['openmct'],
provide() {
return {
domainObject: this.telemetryObject
domainObject: this.telemetryObject,
path: this.path,
renderWhenVisible: this.renderWhenVisible
};
},
props: {
@@ -81,6 +83,14 @@ export default {
telemetryObject: {
type: Object,
default: () => {}
},
path: {
type: Array,
default: () => []
},
renderWhenVisible: {
type: Function,
required: true
}
},
data() {
@@ -110,7 +120,10 @@ export default {
'tc.mode': 'fixed'
};
const newTabAction = this.openmct.actions.getAction('newTab');
newTabAction.invoke([sourceTelemObject], urlParams);
// No view context needed, so pass undefined.
// The urlParams arg will override the global time bounds with the data visualization
// plot bounds.
newTabAction.invoke([sourceTelemObject], undefined, urlParams);
this.showMenu = false;
},
previewTelemetry() {

View File

@@ -22,7 +22,10 @@
<template>
<div>
<div class="c-inspector__properties c-inspect-properties">
<div
class="c-inspector__properties c-inspect-properties"
aria-label="Inspector Properties Details"
>
<div class="c-inspect-properties__header">Details</div>
<ul v-if="hasDetails" class="c-inspect-properties__section">
<Component

View File

@@ -23,6 +23,7 @@
<div
ref="notebookEmbed"
class="c-snapshot c-ne__embed"
:aria-label="`${embed.name} Notebook Embed`"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
@@ -52,7 +53,7 @@
import Moment from 'moment';
import mount from 'utils/mount';
import objectPathToUrl from '@/tools/url';
import { objectPathToUrl } from '@/tools/url';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
import ImageExporter from '../../../exporters/ImageExporter.js';

View File

@@ -23,11 +23,11 @@
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-icon-button c-button--menu icon-camera"
aria-label="Take a Notebook Snapshot"
title="Take a Notebook Snapshot"
:aria-label="snapshotMenuLabel"
:title="snapshotMenuLabel"
@click.stop.prevent="showMenu"
>
<span title="Take Notebook Snapshot" class="c-icon-button__label"> Snapshot </span>
<span class="c-icon-button__label">Snapshot</span>
</button>
</div>
</template>
@@ -72,6 +72,11 @@ export default {
notebookTypes: []
};
},
computed: {
snapshotMenuLabel() {
return 'Open the Notebook Snapshot Menu';
}
},
mounted() {
validateNotebookStorageObject();

View File

@@ -1,36 +1,32 @@
<div class="c-notebook-snapshot">
<!-- parent container sets up this for flex column layout -->
<div class="c-notebook-snapshot__header l-browse-bar">
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w">
<span class="c-object-label l-browse-bar__object-name">
<span class="c-object-label__type-icon" v-bind:class="cssClass"></span>
<span class="c-object-label__type-icon" :class="cssClass"></span>
<span class="c-object-label__name">{{ name }}</span>
</span>
</div>
</div>
<div id="snapshotDescriptor" class="l-browse-bar__snapshot-datetime">
SNAPSHOT {{ createdOn }}
</div>
<div class="c-button-set c-button-set--strip-h" role="toolbar">
<button class="c-button icon-download" aria-label="Export as PNG" @click="exportImage('png')">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button icon-download" aria-label="Export as JPG" @click="exportImage('jpg')">
<span class="c-button__label">JPG</span>
</button>
</div>
<div class="l-browse-bar__end">
<div class="l-browse-bar__snapshot-datetime">SNAPSHOT {{ createdOn }}</div>
<span class="c-button-set c-button-set--strip-h">
<button
class="c-button icon-download"
title="Export This View's Data as PNG"
@click="exportImage('png')"
>
<span class="c-button__label">PNG</span>
</button>
<button class="c-button" title="Export This View's Data as JPG" @click="exportImage('jpg')">
<span class="c-button__label">JPG</span>
</button>
</span>
<a
<button
class="l-browse-bar__annotate-button c-button icon-pencil"
title="Annotate"
aria-label="Annotate this snapshot"
@click="annotateSnapshot"
>
<span class="title-label">Annotate</span>
</a>
</button>
</div>
</div>
@@ -38,5 +34,7 @@
ref="snapshot-image"
class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + src + ')' }"
role="img"
alt="Annotatable Snapshot"
></div>
</div>

View File

@@ -169,6 +169,8 @@ describe('Notebook plugin:', () => {
openmct.editor = {};
openmct.editor.isEditing = () => false;
openmct.editor.on = () => {};
openmct.editor.off = () => {};
const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]);
notebookViewProvider = applicableViews.find(

View File

@@ -46,7 +46,7 @@ export default class PainterroInstance {
this.config.id = this.elementId;
this.config.saveHandler = this.saveHandler.bind(this);
this.painterro = Painterro(this.config);
this.painterro = Painterro.default(this.config);
}
save(callback) {

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectPathToUrl from '/src/tools/url.js';
import { objectPathToUrl } from '/src/tools/url.js';
export default class OpenInNewTab {
constructor(openmct) {
this.name = 'Open In New Tab';
@@ -31,8 +31,26 @@ export default class OpenInNewTab {
this._openmct = openmct;
}
invoke(objectPath, urlParams = undefined) {
let url = objectPathToUrl(this._openmct, objectPath, urlParams);
window.open(url);
/**
* Invokes the "Open in New Tab" action. This will open the object in a new
* browser tab. The URL for the new tab is determined by the current object
* path and any custom time bounds.
*
* @param {import('@/api/objects/ObjectAPI').DomainObject[]} objectPath The current object path
* @param {ViewContext} _view The view context for the object being opened (unused)
* @param {Object<string, string | number>} customUrlParams Provides the ability to override
* the global time conductor bounds. It is an object with the following key/value pairs:
* ```
* {
* 'tc.start': <number>,
* 'tc.end': <number>,
* 'tc.mode': 'fixed' | 'local' | <string>
* }
* ```
*/
invoke(objectPath, _view, customUrlParams) {
const url = objectPathToUrl(this._openmct, objectPath, customUrlParams);
window.open(url, undefined, 'noopener');
}
}

View File

@@ -46,6 +46,11 @@ export default class SeriesCollection extends Collection {
this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this);
const domainObject = this.plot.get('domainObject');
if (this.openmct.objects.isMissing(domainObject)) {
return;
}
if (domainObject.telemetry) {
this.addTelemetryObject(domainObject);
} else {

View File

@@ -219,6 +219,11 @@ export default {
},
addChild(child) {
if (this.openmct.objects.isMissing(child)) {
console.warn('Missing domain object for stacked plot: ', child);
return;
}
const id = this.openmct.objects.makeKeyString(child.identifier);
this.tickWidthMap[id] = {

View File

@@ -170,6 +170,10 @@ export default {
//If this object is not persistable, then package it with it's parent
const plotObject = this.getPlotObject();
if (plotObject === null) {
return;
}
if (this.openmct.telemetry.isTelemetryObject(plotObject)) {
this.subscribeToStaleness(plotObject);
} else {
@@ -215,10 +219,6 @@ export default {
},
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() {

View File

@@ -25,7 +25,7 @@ import mount from 'utils/mount';
import TableConfigurationComponent from './components/TableConfiguration.vue';
import TelemetryTableConfiguration from './TelemetryTableConfiguration.js';
export default function TableConfigurationViewProvider(openmct) {
export default function TableConfigurationViewProvider(openmct, options) {
return {
key: 'table-configuration',
name: 'Config',
@@ -45,7 +45,7 @@ export default function TableConfigurationViewProvider(openmct) {
return {
show: function (element) {
tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct);
tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct, options);
const { destroy } = mount(
{
el: element,

View File

@@ -32,14 +32,14 @@ import TelemetryTableRow from './TelemetryTableRow.js';
import TelemetryTableUnitColumn from './TelemetryTableUnitColumn.js';
export default class TelemetryTable extends EventEmitter {
constructor(domainObject, openmct) {
constructor(domainObject, openmct, options) {
super();
this.domainObject = domainObject;
this.openmct = openmct;
this.tableComposition = undefined;
this.datumCache = [];
this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
this.configuration = new TelemetryTableConfiguration(domainObject, openmct, options);
this.telemetryMode = this.configuration.getTelemetryMode();
this.rowLimit = this.configuration.getRowLimit();
this.paused = false;
@@ -114,7 +114,11 @@ export default class TelemetryTable extends EventEmitter {
this.clearAndResubscribe();
}
updateRowLimit() {
updateRowLimit(rowLimit) {
if (rowLimit) {
this.rowLimit = rowLimit;
}
if (this.telemetryMode === 'performance') {
this.tableRows.setLimit(this.rowLimit);
} else {

View File

@@ -24,11 +24,12 @@ import EventEmitter from 'EventEmitter';
import _ from 'lodash';
export default class TelemetryTableConfiguration extends EventEmitter {
constructor(domainObject, openmct) {
constructor(domainObject, openmct, options) {
super();
this.domainObject = domainObject;
this.openmct = openmct;
this.defaultOptions = options;
this.columns = {};
this.removeColumnsForObject = this.removeColumnsForObject.bind(this);
@@ -48,10 +49,12 @@ export default class TelemetryTableConfiguration extends EventEmitter {
configuration.columnOrder = configuration.columnOrder || [];
configuration.cellFormat = configuration.cellFormat || {};
configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize;
// anything that doesn't have a telemetryMode existed before the change and should stay as it was for consistency
configuration.telemetryMode = configuration.telemetryMode ?? 'unlimited';
configuration.persistModeChange = configuration.persistModeChange ?? true;
configuration.rowLimit = configuration.rowLimit ?? 50;
// anything that doesn't have a telemetryMode existed before the change and should
// take the properties of any passed in defaults or the defaults from the plugin
configuration.telemetryMode = configuration.telemetryMode ?? this.defaultOptions.telemetryMode;
configuration.persistModeChange =
configuration.persistModeChange ?? this.defaultOptions.persistModeChange;
configuration.rowLimit = configuration.rowLimit ?? this.defaultOptions.rowLimit;
return configuration;
}

View File

@@ -20,8 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default function getTelemetryTableType(options = {}) {
const { telemetryMode = 'performance', persistModeChanges = true, rowLimit = 50 } = options;
export default function getTelemetryTableType(options) {
let { telemetryMode, persistModeChange, rowLimit } = options;
return {
name: 'Telemetry Table',
@@ -32,12 +32,12 @@ export default function getTelemetryTableType(options = {}) {
form: [
{
key: 'telemetryMode',
name: 'Telemetry Mode',
name: 'Data Mode',
control: 'select',
options: [
{
value: 'performance',
name: 'Performance Mode'
name: 'Limited (Performance) Mode'
},
{
value: 'unlimited',
@@ -48,15 +48,15 @@ export default function getTelemetryTableType(options = {}) {
property: ['configuration', 'telemetryMode']
},
{
name: 'Persist Telemetry Mode Changes',
name: 'Persist Data Mode Changes',
control: 'toggleSwitch',
cssClass: 'l-input',
key: 'persistModeChanges',
property: ['configuration', 'persistModeChanges']
key: 'persistModeChange',
property: ['configuration', 'persistModeChange']
},
{
name: 'Performance Mode Row Limit',
control: 'toggleSwitch',
name: 'Limited Data Mode Row Limit',
control: 'numberfield',
cssClass: 'l-input',
key: 'rowLimit',
property: ['configuration', 'rowLimit']
@@ -68,7 +68,7 @@ export default function getTelemetryTableType(options = {}) {
columnWidths: {},
hiddenColumns: {},
telemetryMode,
persistModeChanges,
persistModeChange,
rowLimit
};
}

View File

@@ -33,7 +33,7 @@ export default class TelemetryTableView {
this.component = null;
Object.defineProperty(this, 'table', {
value: new TelemetryTable(domainObject, openmct),
value: new TelemetryTable(domainObject, openmct, options),
enumerable: false,
configurable: false
});

View File

@@ -287,7 +287,7 @@
<script>
import _ from 'lodash';
import { toRaw } from 'vue';
import { onMounted, ref, toRaw } from 'vue';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
@@ -295,7 +295,7 @@ import CSVExporter from '../../../exporters/CSVExporter.js';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import Search from '../../../ui/components/SearchComponent.vue';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import throttle from '../../../utils/throttle';
import { useResizeObserver } from '../../../ui/composables/resize.js';
import SizingRow from './SizingRow.vue';
import TableColumnHeader from './TableColumnHeader.vue';
import TableFooterIndicator from './TableFooterIndicator.vue';
@@ -303,7 +303,6 @@ import TelemetryTableRow from './TableRow.vue';
const VISIBLE_ROW_COUNT = 100;
const ROW_HEIGHT = 17;
const RESIZE_POLL_INTERVAL = 200;
const AUTO_SCROLL_TRIGGER_HEIGHT = ROW_HEIGHT * 3;
export default {
@@ -354,6 +353,15 @@ export default {
}
},
emits: ['marked-rows-updated', 'filter'],
setup() {
const root = ref(null);
const { size: containerSize, startObserving } = useResizeObserver();
onMounted(() => {
startObserving(root.value);
});
return { containerSize, root };
},
data() {
let configuration = this.table.configuration.getConfiguration();
@@ -390,14 +398,17 @@ export default {
totalNumberOfRows: 0,
rowContext: {},
telemetryMode: configuration.telemetryMode,
persistModeChanges: configuration.persistModeChanges
rowLimit: configuration.rowLimit,
persistModeChange: configuration.persistModeChange,
afterLoadActions: [],
existingConfiguration: configuration
};
},
computed: {
dropTargetStyle() {
return {
top: this.$refs.headersTable.offsetTop + 'px',
height: this.totalHeight + this.$refs.headersTable.offsetHeight + 'px',
top: this.$refs.headersHolderEl.offsetTop + 'px',
height: this.totalHeight + this.$refs.headersHolderEl.offsetHeight + 'px',
left: this.dropOffsetLeft && this.dropOffsetLeft + 'px'
};
},
@@ -441,12 +452,17 @@ export default {
}
},
watch: {
//This should be refactored so that it doesn't require an explicit watch. Should be doable.
containerSize: {
handler() {
this.debouncedRescaleToContainer();
},
deep: true
},
loading: {
handler(isLoading) {
if (isLoading) {
this.setLoadingPromise();
} else {
this.loadFinishResolve();
if (!isLoading) {
this.runAfterLoadActions();
}
if (this.viewActionsCollection) {
@@ -500,9 +516,10 @@ export default {
this.filterTelemetry = _.debounce(this.filterTelemetry, 500);
},
mounted() {
this.throttledUpdateVisibleRows = _.throttle(this.updateVisibleRows, 1000, { leading: true });
this.debouncedRescaleToContainer = _.debounce(this.rescaleToContainer, 300);
this.csvExporter = new CSVExporter();
this.rowsAdded = _.throttle(this.rowsAdded, 200);
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
this.scroll = _.throttle(this.scroll, 100);
if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {
@@ -515,8 +532,6 @@ export default {
});
}
this.updateVisibleRows = throttle(this.updateVisibleRows, 1000);
this.table.on('object-added', this.addObject);
this.table.on('object-removed', this.removeObject);
this.table.on('refresh', this.clearRowsAndRerender);
@@ -524,10 +539,12 @@ export default {
this.table.on('outstanding-requests', this.outstandingRequests);
this.table.on('telemetry-staleness', this.handleStaleness);
this.table.configuration.on('change', this.handleConfigurationChanges);
this.table.tableRows.on('add', this.rowsAdded);
this.table.tableRows.on('remove', this.rowsRemoved);
this.table.tableRows.on('sort', this.updateVisibleRows);
this.table.tableRows.on('filter', this.updateVisibleRows);
this.table.tableRows.on('sort', this.throttledUpdateVisibleRows);
this.table.tableRows.on('filter', this.throttledUpdateVisibleRows);
this.openmct.time.on('bounds', this.boundsChanged);
@@ -540,10 +557,10 @@ export default {
this.table.configuration.on('change', this.updateConfiguration);
this.calculateTableSize();
this.pollForResize();
this.calculateScrollbarWidth();
this.table.initialize();
this.rescaleToContainer();
},
beforeUnmount() {
this.table.off('object-added', this.addObject);
@@ -553,27 +570,62 @@ export default {
this.table.off('outstanding-requests', this.outstandingRequests);
this.table.off('telemetry-staleness', this.handleStaleness);
this.table.configuration.off('change', this.handleConfigurationChanges);
this.table.tableRows.off('add', this.rowsAdded);
this.table.tableRows.off('remove', this.rowsRemoved);
this.table.tableRows.off('sort', this.updateVisibleRows);
this.table.tableRows.off('filter', this.updateVisibleRows);
this.table.tableRows.off('sort', this.throttledUpdateVisibleRows);
this.table.tableRows.off('filter', this.throttledUpdateVisibleRows);
this.table.configuration.off('change', this.updateConfiguration);
this.openmct.time.off('bounds', this.boundsChanged);
clearInterval(this.resizePollHandle);
this.table.configuration.destroy();
this.table.destroy();
},
methods: {
setLoadingPromise() {
this.loadFinishResolve = null;
this.isFinishedLoading = new Promise((resolve, reject) => {
this.loadFinishResolve = resolve;
});
addToAfterLoadActions(func) {
this.afterLoadActions.push(func);
},
runAfterLoadActions() {
if (this.afterLoadActions.length > 0) {
this.afterLoadActions.forEach((action) => action());
this.afterLoadActions = [];
}
},
handleConfigurationChanges(changes) {
const { rowLimit, telemetryMode, persistModeChange } = changes;
const telemetryModeChanged = this.existingConfiguration.telemetryMode !== telemetryMode;
let rowLimitChanged = false;
this.persistModeChange = persistModeChange;
// both rowLimit changes and telemetryMode changes
// require a re-request of telemetry
if (this.rowLimit !== rowLimit) {
rowLimitChanged = true;
this.rowLimit = rowLimit;
this.table.updateRowLimit(rowLimit);
}
// check for telemetry mode change, because you could technically have persist mode changes
// set to false, which could create a state where the configuration saved telemetry mode is
// different from the currently set telemetry mode
if (telemetryModeChanged && this.telemetryMode !== telemetryMode) {
this.telemetryMode = telemetryMode;
// this method also re-requests telemetry
this.table.updateTelemetryMode(telemetryMode);
}
if (rowLimitChanged && !telemetryModeChanged) {
this.table.clearAndResubscribe();
}
this.existingConfiguration = changes;
},
updateVisibleRows() {
if (!this.updatingView) {
@@ -684,7 +736,7 @@ export default {
this.table.sortBy(this.sortOptions);
},
scroll() {
this.updateVisibleRows();
this.throttledUpdateVisibleRows();
this.synchronizeScrollX();
if (this.shouldAutoScroll()) {
@@ -757,11 +809,11 @@ export default {
this.initiateAutoScroll();
}
this.updateVisibleRows();
this.throttledUpdateVisibleRows();
},
rowsRemoved(rows) {
this.setHeight();
this.updateVisibleRows();
this.throttledUpdateVisibleRows();
},
/**
* Calculates height based on total number of rows, and sets table height.
@@ -880,35 +932,27 @@ export default {
dropTargetActive(isActive) {
this.isDropTargetActive = isActive;
},
pollForResize() {
let el = this.$refs.root;
let width = el.clientWidth;
let height = el.clientHeight;
rescaleToContainer() {
let scrollTop = this.scrollable.scrollTop;
this.resizePollHandle = setInterval(() => {
this.renderWhenVisible(() => {
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
this.calculateTableSize();
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
// Need to preserve scroll position in this case.
if (this.autoScroll) {
this.initiateAutoScroll();
} else {
this.scrollable.scrollTop = scrollTop;
}
width = el.clientWidth;
height = el.clientHeight;
this.renderWhenVisible(() => {
if (this.isAutosizeEnabled) {
this.calculateTableSize();
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
// Need to preserve scroll position in this case.
if (this.autoScroll) {
this.initiateAutoScroll();
} else {
this.scrollable.scrollTop = scrollTop;
}
}
scrollTop = this.scrollable.scrollTop;
});
}, RESIZE_POLL_INTERVAL);
scrollTop = this.scrollable.scrollTop;
});
},
clearRowsAndRerender() {
this.visibleRows = [];
this.$nextTick().then(this.updateVisibleRows);
this.$nextTick().then(this.throttledUpdateVisibleRows);
},
pause(byButton) {
if (byButton) {
@@ -1038,7 +1082,7 @@ export default {
let row = allRows[i];
row.marked = true;
if (row !== baseRow) {
if (row !== baseRow && this.markedRows.indexOf(row) === -1) {
this.markedRows.push(row);
}
}
@@ -1162,11 +1206,9 @@ export default {
{
label,
emphasis: true,
callback: async () => {
callback: () => {
this.addToAfterLoadActions(callback);
this.updateTelemetryMode();
await this.isFinishedLoading;
callback();
dialog.dismiss();
}
@@ -1183,7 +1225,7 @@ export default {
updateTelemetryMode() {
this.telemetryMode = this.telemetryMode === 'unlimited' ? 'performance' : 'unlimited';
if (this.persistModeChanges) {
if (this.persistModeChange) {
this.table.configuration.setTelemetryMode(this.telemetryMode);
}

View File

@@ -40,7 +40,7 @@
:title="rowCountTitle"
class="c-table-indicator__elem c-table-indicator__row-count"
>
{{ rowCount }} Rows
{{ rowCount }}
</span>
<span
@@ -113,7 +113,7 @@ export default {
}
},
rowCount() {
return this.isUnlimitedMode ? this.totalRows : 'LATEST 50';
return this.isUnlimitedMode ? `${this.totalRows} ROWS` : `LATEST ${this.totalRows} ROWS`;
},
rowCountTitle() {
return this.isUnlimitedMode
@@ -121,12 +121,12 @@ export default {
: 'performance mode limited to 50 rows';
},
telemetryModeButtonLabel() {
return this.isUnlimitedMode ? 'SHOW LATEST 50' : 'SHOW ALL';
return this.isUnlimitedMode ? 'SHOW LIMITED' : 'SHOW UNLIMITED';
},
telemetryModeButtonTitle() {
return this.isUnlimitedMode
? 'Change to Performance mode (latest 50 values)'
: 'Change to show all values';
? 'Change to Limited (Performance) Mode'
: 'Change to Unlimited Mode';
},
title() {
if (this.hasMixedFilters) {

View File

@@ -24,6 +24,7 @@
:style="{ top: rowTop }"
class="noselect"
:class="[rowClass, { 'is-selected': marked }]"
:aria-label="ariaLabel"
v-on="listeners"
>
<component
@@ -99,6 +100,9 @@ export default {
};
},
computed: {
ariaLabel() {
return this.marked ? 'Selected Table Row' : 'Table Row';
},
listeners() {
let listenersObject = {
click: this.markRow

View File

@@ -25,10 +25,12 @@ import getTelemetryTableType from './TelemetryTableType.js';
import TelemetryTableViewProvider from './TelemetryTableViewProvider.js';
import TelemetryTableViewActions from './ViewActions.js';
export default function plugin(options) {
export default function plugin(
options = { telemetryMode: 'performance', persistModeChange: true, rowLimit: 50 }
) {
return function install(openmct) {
openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options));
openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct));
openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct, options));
openmct.types.addType('table', getTelemetryTableType(options));
openmct.composition.addPolicy((parent, child) => {
if (parent.type === 'table') {

View File

@@ -195,7 +195,10 @@ describe('the plugin', () => {
utc: false,
'some-key': false,
'some-other-key': false
}
},
persistModeChange: true,
rowLimit: 50,
telemetryMode: 'performance'
}
};
const testTelemetry = [

View File

@@ -35,7 +35,6 @@ import utcMultiTimeFormat from './utcMultiTimeFormat.js';
const PADDING = 1;
const DEFAULT_DURATION_FORMATTER = 'duration';
const RESIZE_POLL_INTERVAL = 200;
const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
@@ -92,7 +91,6 @@ export default {
//Respond to changes in conductor
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
beforeUnmount() {
clearInterval(this.resizeTimer);

View File

@@ -148,7 +148,10 @@ export default {
}
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.handleNewBounds = _.throttle(this.handleNewBounds, 300, {
leading: true,
trailing: false
});
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();

View File

@@ -29,7 +29,7 @@
}"
>
<a class="c-icon-button icon-calendar" @click="toggle"></a>
<div v-if="open" class="c-menu c-menu--mobile-modal c-datetime-picker">
<div v-if="open" role="dialog" class="c-menu c-menu--mobile-modal c-datetime-picker">
<div class="c-datetime-picker__close-button">
<button class="c-click-icon icon-x-in-circle" @click="toggle"></button>
</div>

View File

@@ -21,7 +21,7 @@
/>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
class="c-ctrl-wrapper--menus-right"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
@@ -87,7 +87,7 @@
></button>
<button
class="c-button icon-x"
aria-label="Discard time bounds"
aria-label="Discard changes and close time popup"
@click.prevent="hide"
></button>
</div>

View File

@@ -132,7 +132,7 @@
></button>
<button
class="c-button icon-x"
aria-label="Discard time offsets"
aria-label="Discard changes and close time popup"
@click.prevent="hide"
></button>
</div>

View File

@@ -454,6 +454,12 @@
color: $colorTimeRealtimeFg;
}
}
.c-ctrl-wrapper--menus-up{ // A bit hacky, but we are rewriting the CSS class here for ITC such that the calendar opens at the bottom to avoid cutoff
.c-menu {
top: auto;
bottom: revert !important;
};
}
}
}

View File

@@ -27,6 +27,7 @@
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode',
{ 'is-expanded': independentTCEnabled }
]"
aria-label="Independent Time Conductor Panel"
>
<ToggleSwitch
id="independentTCToggle"

View File

@@ -35,42 +35,39 @@
:item-properties="itemProperties"
:execution-state="persistedActivityStates[item.id]"
@click.stop="setSelectionForActivity(item, $event.currentTarget)"
>
</expanded-view-item>
/>
</template>
<div v-else class="c-table c-table--sortable c-list-view c-list-view--sticky-header sticky">
<table class="c-table__body js-table__body">
<thead class="c-table__header">
<tr>
<list-header
v-for="headerItem in headerItems"
:key="headerItem.property"
:direction="
defaultSort.property === headerItem.property
? defaultSort.defaultDirection
: headerItem.defaultDirection
"
:is-sortable="headerItem.isSortable"
:aria-label="headerItem.name"
:title="headerItem.name"
:property="headerItem.property"
:current-sort="defaultSort.property"
@sort="sort"
<template v-else>
<div class="c-table c-table--sortable c-list-view c-list-view--sticky-header sticky">
<table class="c-table__body js-table__body">
<thead class="c-table__header">
<tr>
<list-header
v-for="headerItem in headerItems"
:key="headerItem.property"
:direction="getSortDirection(headerItem)"
:is-sortable="headerItem.isSortable"
:aria-label="headerItem.name"
:title="headerItem.name"
:property="headerItem.property"
:current-sort="defaultSort.property"
@sort="sort"
/>
</tr>
</thead>
<tbody>
<list-item
v-for="item in sortedItems"
:key="item.key"
:class="{ '--is-in-progress': persistedActivityStates[item.id] === 'in-progress' }"
:item="item"
:item-properties="itemProperties"
@click.stop="setSelectionForActivity(item, $event.currentTarget)"
/>
</tr>
</thead>
<tbody>
<list-item
v-for="item in sortedItems"
:key="item.key"
:class="{ '--is-in-progress': persistedActivityStates[item.id] === 'in-progress' }"
:item="item"
:item-properties="itemProperties"
@click.stop="setSelectionForActivity(item, $event.currentTarget)"
/>
</tbody>
</table>
</div>
</tbody>
</table>
</div>
</template>
</div>
</template>
@@ -526,20 +523,16 @@ export default {
return activities.map(this.styleActivity);
},
setSort() {
const sortOrder = SORT_ORDER_OPTIONS[this.domainObject.configuration.sortOrderIndex];
const property = sortOrder.property;
const direction = sortOrder.direction.toLowerCase() === 'asc';
const { property, direction } =
SORT_ORDER_OPTIONS[this.domainObject.configuration.sortOrderIndex];
this.defaultSort = {
property,
defaultDirection: direction
defaultDirection: direction.toLowerCase() === 'asc'
};
},
sortItems(activities) {
let sortedItems = _.sortBy(activities, this.defaultSort.property);
if (!this.defaultSort.defaultDirection) {
sortedItems = sortedItems.reverse();
}
return sortedItems;
const sortedItems = _.sortBy(activities, this.defaultSort.property);
return this.defaultSort.defaultDirection ? sortedItems : sortedItems.reverse();
},
setStatus(status) {
this.status = status;
@@ -548,10 +541,7 @@ export default {
this.isEditing = isEditing;
this.setViewFromConfig(this.domainObject.configuration);
},
sort(data) {
const property = data.property;
const direction = data.direction;
sort({ property, direction }) {
if (this.defaultSort.property === property) {
this.defaultSort.defaultDirection = !this.defaultSort.defaultDirection;
} else {
@@ -565,10 +555,10 @@ export default {
this.openmct.selection.select(
[
{
element: element,
element,
context: {
type: 'activity',
activity: activity
activity
}
},
{
@@ -581,6 +571,11 @@ export default {
],
multiSelect
);
},
getSortDirection(headerItem) {
return this.defaultSort.property === headerItem.property
? this.defaultSort.defaultDirection
: headerItem.defaultDirection;
}
}
};

View File

@@ -23,27 +23,46 @@
import EventEmitter from 'EventEmitter';
import _ from 'lodash';
/**
* @typedef {Object} Selectable
* @property {HTMLElement} element The HTML element that is selectable
* @property {Object} context The context of the selectable, which may include a DomainObject
*/
/**
* @typedef {import('../../openmct').OpenMCT} OpenMCT
*/
/**
* Manages selection state for Open MCT
* @private
*/
export default class Selection extends EventEmitter {
/**
* @param {OpenMCT} openmct The Open MCT instance
*/
constructor(openmct) {
super();
/** @type {OpenMCT} */
this.openmct = openmct;
/** @type {Selectable[]} */
this.selected = [];
}
/**
* Gets the selected object.
* @returns {Selectable[]} The currently selected objects
* @public
*/
get() {
return this.selected;
}
/**
* Selects the selectable object and emits the 'change' event.
*
* @param {Selectable|Selectable[]} selectable An object or array of objects with element and context properties
* @param {object} selectable an object with element and context properties
* @param {Boolean} isMultiSelectEvent flag indication shift key is pressed or not
* @private

View File

@@ -499,7 +499,9 @@ select {
color: $colorSelectFg;
box-shadow: $shdwSelect;
background-repeat: no-repeat, no-repeat;
background-position: right 0.4em top 80%, 0 0;
background-position:
right 0.4em top 80%,
0 0;
border: none;
border-radius: $controlCr;
padding: 2px 20px 2px $interiorMargin;
@@ -718,15 +720,15 @@ select {
.c-super-menu__item-description {
flex: 1 1 70%;
[class*="__icon"] {
[class*='__icon'] {
display: none !important;
}
[class*="__name"] {
[class*='__name'] {
margin-top: 0 !important;
}
[class*="__item-description"] {
[class*='__item-description'] {
min-width: 200px;
}
}
@@ -1133,7 +1135,7 @@ input[type='range'] {
// Hidden by default; requires a hover 1 - 3 levels above to display
@include transition(opacity, $transOutTime);
opacity: 0;
pointer-events: none;
pointer-events: auto;
}
}

View File

@@ -68,6 +68,18 @@ div {
flex: 1 1 auto;
}
.visually-hidden {
// Provides a way to add accessible text to elements
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
/******************************************************** BROWSER ELEMENTS */
body.desktop {
::-webkit-scrollbar {
@@ -361,7 +373,7 @@ body.desktop .has-local-controls {
}
}
[aria-disabled = 'true'],
[aria-disabled='true'],
*[disabled],
.disabled {
opacity: $controlDisabledOpacity;
@@ -397,7 +409,7 @@ body.desktop .has-local-controls {
position: absolute;
top: 0;
bottom: 0;
content: "";
content: '';
right: 0;
width: $fadeTruncateW * 1.5;
z-index: 2;

View File

@@ -141,6 +141,7 @@ mct-plot {
.plot-wrapper-axis-and-display-area {
position: relative;
flex: 1 1 auto;
overflow: hidden;
//min-height: $plotMinH;
}

View File

@@ -711,6 +711,20 @@
}
}
&[class*='--menus-down'] {
.c-menu {
top: auto;
bottom: 100%;
}
}
&[class*='--menus-right'] {
.c-menu {
left: 0;
right: auto;
}
}
&[class*='--menus-left'],
&[class*='menus-to-left'] {
.c-menu {

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