Compare commits

...

33 Commits

Author SHA1 Message Date
f4637b8ac7 [Telemetry Tables] Ability to hide manually added Name column (#8042)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
e2e-couchdb / e2e-couchdb (push) Has been cancelled
e2e-perf / e2e-full (push) Has been cancelled
e2e-pr / e2e-full (ubuntu-latest) (push) Has been cancelled
e2e-pr / e2e-full (windows-latest) (push) Has been cancelled
pr-platform / Node lts/hydrogen - x64 on macos-latest (push) Has been cancelled
pr-platform / Node lts/hydrogen - x64 on ubuntu-latest (push) Has been cancelled
pr-platform / Node lts/hydrogen - x64 on windows-latest (push) Has been cancelled
pr-platform / Node lts/iron - x64 on macos-latest (push) Has been cancelled
pr-platform / Node lts/iron - x64 on ubuntu-latest (push) Has been cancelled
pr-platform / Node lts/iron - x64 on windows-latest (push) Has been cancelled
* add name header so it shows in hide columns options
2025-06-13 15:04:38 -07:00
826c7134b5 Make Plot "Export as PNG/JPG" default filenames more clear and make the methods extendable (#8070)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
* add filename argument and update default name
* add filename argument and update default name
* allow the invoke function to accept a filename to pass to the view actions
* tests
* remove invalid chars from filenames, another test for that
* replace periods with underscores as they may be useful in a name
2025-05-20 13:38:55 -07:00
fa1a45b6cd [Inspector Tabs] Updates (#7987)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
* added getTypes to types api, modifying which tabs are shown, working on annoatation tab
* moving hasNumericTelemetry to an api method
* updated types api to return all types, updated annotations api to return annotatable types, cleaned up use of hasNumericTelemetry elsewhere in the code

* Changes for tabs visibility and priority
- Alphanumeric formatting tab set to default priority while editing, low priority during browse.
- Good styling for Format tab contents in browse mode.
- Properties tab set to low priority during editing, default during browse.
- Make Elements pool visible in browse mode, omit edit capabilities.
- Edit and browse mode priorities for Properties and Elements.
- Adjusted edit and browse mode priorities for Properties and Elements.
- Priority set for Gantt view.
- Priorities set for Graph, Lad Table, Scatter Plot, Telem Tables and Time List views.
- Changed several Inspector tab names to 'Config'; tests and other code changed to target `key` instead of `name`:
  - LAD Table
  - Time List - will need regression testing for change noted re. `key` above.
- Created browse mode read-only Inspector views:
  - LAD Table, Lad Table set.
  - Telemetry Table.
  and `showTab` functions; `showTab` has just been set to true for now.
to prevent it from displaying when no filters can be set.
- Plot plugin.js now adds configuration.objectStyles {} for overlay and stacked plots on initialize.
- FiltersView now displays a message for telem sources that don't have filter criteria available.
- Annotations tab now set to never display when a view is being edited.
- Added `objectStyles: {}` to initialize functions for multiple objects:
  - Condition Widget
  - Gauge
  - LAD Table

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2025-05-15 16:55:15 -07:00
10bc8eb55d Condition sets failing to evaluate telemetry where source and key do not match in metadata (#8065)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
normalize should map from source to key
2025-05-14 11:55:47 -07:00
2e7fb94dd5 Condition Sets can incorrectly evaluate telemetry objects that update infrequently (#8041)
* Remove short-circuit logic

* Revert change to remove short-circuit logic

* Restore short-circuit evaluation, pass in current values

* V2, restore short-circuit eval

* Added timestamp checking for individual criteria

* Some optimizations

* Co-authored-by: Pranaykarvi<pranaykarvi@gmail.com>

* Fixed bug that prevented test data from working at all

* Fixed bug introduced by refactor. Oops.

* Fixed bug with test data

* Provided clarification in test description

* Replaced legacy tests with new e2e test of correct telemetry evaluation

* some markup changes to improve testability

* Address review comments

* Fixed linting error

* Fixed broken tests

* Fixed long-standing bug with evaluating enums

* Fixed broken tests

* Fixed linting error

* remove commented code

test name grammar fix

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2025-05-14 08:32:35 -07:00
573bbb041e Fix partial matches (#8047)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Support character escaping
2025-04-24 16:41:12 +00:00
6a450a0e89 Couch search indexes (#8037)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
* Defined search index for object names. Add index for searching by object type
* Feature detect if views are defined to support optimized search. If not, fall back on filter-based search
* Suppress github codedcov annotations for now, they are not accurate and generate noise.
* Allow nested describes. They're good.
* Add a noop search function to couch search folder object provider. Actual search is provided by Couch provider, but need a stub to prevent in-memory indexing
* Adhere to our own interface and ensure identifiers are always returned by default composition provider
2025-04-23 11:58:51 -07:00
e5631c9f6c Marked sanitize html (#8021)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
* chore(deps-dev): bump sanitize-html from 2.12.1 to 2.15.0

Bumps [sanitize-html](https://github.com/apostrophecms/sanitize-html) from 2.12.1 to 2.15.0.
- [Changelog](https://github.com/apostrophecms/sanitize-html/blob/main/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/sanitize-html/compare/2.12.1...2.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

* bump marked

* Added package lock:

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 09:03:23 -07:00
15b5d1405d chore(deps-dev): bump webpack-merge from 5.10.0 to 6.0.1 (#8017)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Bumps [webpack-merge](https://github.com/survivejs/webpack-merge) from 5.10.0 to 6.0.1.
- [Changelog](https://github.com/survivejs/webpack-merge/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/survivejs/webpack-merge/compare/v5.10.0...v6.0.1)

---
updated-dependencies:
- dependency-name: webpack-merge
  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: Andrew Henry <akhenry@gmail.com>
2025-04-01 15:52:06 -07:00
0377788533 chore(deps-dev): bump uuid from 9.0.1 to 11.1.0 (#8018)
Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.1 to 11.1.0.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.1...v11.1.0)

---
updated-dependencies:
- dependency-name: uuid
  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: Andrew Henry <akhenry@gmail.com>
2025-04-01 15:27:29 -07:00
28dcff7e89 chore(deps-dev): bump copy-webpack-plugin from 12.0.2 to 13.0.0 (#8015)
Bumps [copy-webpack-plugin](https://github.com/webpack-contrib/copy-webpack-plugin) from 12.0.2 to 13.0.0.
- [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/v12.0.2...v13.0.0)

---
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: Andrew Henry <akhenry@gmail.com>
2025-04-01 14:30:27 -07:00
b191eb9d64 chore(deps-dev): bump npm-run-all2 from 6.1.2 to 7.0.2 (#8014)
Bumps [npm-run-all2](https://github.com/bcomnes/npm-run-all2) from 6.1.2 to 7.0.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.2...v7.0.2)

---
updated-dependencies:
- dependency-name: npm-run-all2
  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: Andrew Henry <akhenry@gmail.com>
2025-04-01 13:39:07 -07:00
f8e4aba922 Update all remaining dependencies in one go (#8011)
* Update all remaining dependencies in one go

* Support changed marked API

* Adapt to use new library

* Removed commented code

* Fixed typo in anchor tag
2025-04-01 13:21:13 -07:00
28f6987dd7 chore(deps-dev): bump jasmine-core from 5.1.1 to 5.5.0 (#7952)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 5.1.1 to 5.5.0.
- [Release notes](https://github.com/jasmine/jasmine/releases)
- [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md)
- [Commits](https://github.com/jasmine/jasmine/compare/v5.1.1...v5.5.0)

---
updated-dependencies:
- dependency-name: jasmine-core
  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>
2025-03-28 16:19:20 -07:00
f3047093d6 chore(deps): bump codecov/codecov-action from 4 to 5 (#7931)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  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>
2025-03-28 16:01:58 -07:00
34e57ef300 chore(deps-dev): bump nyc from 15.1.0 to 17.1.0 (#7852)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Bumps [nyc](https://github.com/istanbuljs/nyc) from 15.1.0 to 17.1.0.
- [Release notes](https://github.com/istanbuljs/nyc/releases)
- [Changelog](https://github.com/istanbuljs/nyc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/istanbuljs/nyc/compare/v15.1.0...nyc-v17.1.0)

---
updated-dependencies:
- dependency-name: nyc
  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>
2025-03-28 15:26:34 -07:00
88e6557782 chore(deps-dev): bump babel-plugin-istanbul from 6.1.1 to 7.0.0 (#7804)
Bumps [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul) from 6.1.1 to 7.0.0.
- [Release notes](https://github.com/istanbuljs/babel-plugin-istanbul/releases)
- [Changelog](https://github.com/istanbuljs/babel-plugin-istanbul/blob/master/CHANGELOG.md)
- [Commits](https://github.com/istanbuljs/babel-plugin-istanbul/compare/v6.1.1...v7.0.0)

---
updated-dependencies:
- dependency-name: babel-plugin-istanbul
  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>
2025-03-28 15:14:17 -07:00
dbd4abebae chore(deps-dev): bump style-loader from 3.3.3 to 4.0.0 (#7802)
Bumps [style-loader](https://github.com/webpack-contrib/style-loader) from 3.3.3 to 4.0.0.
- [Release notes](https://github.com/webpack-contrib/style-loader/releases)
- [Changelog](https://github.com/webpack-contrib/style-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/style-loader/compare/v3.3.3...v4.0.0)

---
updated-dependencies:
- dependency-name: style-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>
2025-03-28 15:03:21 -07:00
50ca27e54f chore(deps-dev): bump d3-shape and @types/d3-shape (#7605)
* chore(deps-dev): bump d3-shape and @types/d3-shape

Bumps [d3-shape](https://github.com/d3/d3-shape) and [@types/d3-shape](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/d3-shape). These dependencies needed to be updated together.

Updates `d3-shape` from 3.0.0 to 3.2.0
- [Release notes](https://github.com/d3/d3-shape/releases)
- [Commits](https://github.com/d3/d3-shape/compare/v3.0.0...v3.2.0)

Updates `@types/d3-shape` from 3.0.0 to 3.1.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/d3-shape)

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

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps-dev): bump d3-shape and @types/d3-shape

Bumps [d3-shape](https://github.com/d3/d3-shape) and [@types/d3-shape](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/d3-shape). These dependencies needed to be updated together.

Updates `d3-shape` from 3.0.0 to 3.2.0
- [Release notes](https://github.com/d3/d3-shape/releases)
- [Commits](https://github.com/d3/d3-shape/compare/v3.0.0...v3.2.0)

Updates `@types/d3-shape` from 3.0.0 to 3.1.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/d3-shape)

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

Signed-off-by: dependabot[bot] <support@github.com>

* Dependency upgrade changed rendering of SVG. Visually verified that it is correct and updated the test

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2025-03-28 14:25:25 -07:00
2955092c86 chore(deps-dev): bump mini-css-extract-plugin from 2.7.6 to 2.8.1 (#7573)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 2.7.6 to 2.8.1.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v2.7.6...v2.8.1)

---
updated-dependencies:
- dependency-name: mini-css-extract-plugin
  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>
2025-03-26 13:51:10 -07:00
28b5d7c41c Time strip marcus banes line "now line" fix for right y-axis and when now is out of bounds (#7993)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
* Account for right y-axes when calculating now line position.
Don't show the now line if it's out of bounds of the time axis

* Add test for now marker in realtime and out of bounds modes
2025-02-17 18:23:48 +00:00
ecd120387c Independent time conductor related handling for plot synchronization. (#7956)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
* Ensure that the mode set when independent time conductor is enabled/disabled is propagated correctly.
Also ensure that global time conductor changes are not picked up by the independent time conductor when the user has enabled it at least once before

* Use structuredClone instead of deep copy

* Add e2e test

* Assert that you're in fixed mode after sync time conductor

* Comment explaining new time context test

* Change test to be a little less complicated

* Fix linting errors
2025-02-10 21:46:00 +00:00
a6517bb33e migrate from actions/upload-artifact: v3 to v4. (#8000)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
* migrate from actions/upload-artifact: v3 to v4.
https://github.com/actions/upload-artifact/blob/main/docs/MIGRATION.md

* Add names for artifacts and allow overwriting them
2025-02-07 18:03:00 +00:00
1fde0d9e38 Don't disallow mouse events when in compact mode for plots (#7975)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
e2e-couchdb / e2e-couchdb (push) Has been cancelled
e2e-perf / e2e-full (push) Has been cancelled
e2e-pr / e2e-full (ubuntu-latest) (push) Has been cancelled
e2e-pr / e2e-full (windows-latest) (push) Has been cancelled
pr-platform / Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} (x64, lts/hydrogen, macos-latest) (push) Has been cancelled
pr-platform / Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} (x64, lts/hydrogen, ubuntu-latest) (push) Has been cancelled
pr-platform / Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} (x64, lts/hydrogen, windows-latest) (push) Has been cancelled
pr-platform / Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} (x64, lts/iron, macos-latest) (push) Has been cancelled
pr-platform / Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} (x64, lts/iron, ubuntu-latest) (push) Has been cancelled
pr-platform / Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} (x64, lts/iron, windows-latest) (push) Has been cancelled
pr-platform / Node lts/hydrogen - x64 on macos-latest (push) Has been cancelled
pr-platform / Node lts/hydrogen - x64 on ubuntu-latest (push) Has been cancelled
pr-platform / Node lts/hydrogen - x64 on windows-latest (push) Has been cancelled
pr-platform / Node lts/iron - x64 on macos-latest (push) Has been cancelled
pr-platform / Node lts/iron - x64 on ubuntu-latest (push) Has been cancelled
pr-platform / Node lts/iron - x64 on windows-latest (push) Has been cancelled
* Allow highlights and locking highlight points for plots in compact mode, but still disallow pan and zoom.

* Remove unnecessary watch on cursor guides and grid lines

* Test for cursor guides in compact mode
2025-01-18 15:50:24 +00:00
5be103ea72 modified the sanitizeForSerialization method to remove unnecessary re… (#7950)
modified the sanitizeForSerialization method to remove unnecessary recursion, update e2e test to CORRECTLY test the functionality
2024-12-09 20:34:07 +00:00
d74e1b19b6 In progress activities that are out of bounds are shown (#7945)
If an activity is out of bounds, but in progress, display it in the currently visible list.
2024-12-06 22:38:27 +00:00
5bb6a18cd4 [Notebook] Browse Bar holding onto stale model, reverts changes (#7944)
* moving rename methods to appActions

* importing back into original test

* reverting

* add the ability to change the name in the browse bar

* add test to verify entries are not being lost

* addding aria labels for tests

* when an object is changed, store the whole new object, not just the name

* typo!
2024-12-06 14:13:08 -08:00
14b947c101 fix vue reactivity of rows by changing the reference of the updated row (#7940)
* do not call `updateVisibleRows` on horizontal scroll
* add example provider for in place row updates
2024-12-04 11:27:52 -08:00
61b982ab99 [Telemetry API] Prevent Subscriptions with different options from overwriting each other (#7930)
* initial implementation

* cleaning up a bit

* adding the hash method back as we dont want gigantic keys

* adding a line

* added filtering to state generator, updated filters readme to fix error, more robust hash function

* removing unnecessary changes in wrong file

* adding a test to confirm each endpoint has a separate subscription based of filtering

* lint

* adding back in hints, accidentally removed

* remove some redundant code and convert sanitization method into a replacer function for stringify

* tweaking serialize replacer to handle arrays correctly, adding more determinative row addition check to test

* more focused selector for the table

* simplified the serialization method even further and added some more docs
2024-12-04 03:33:15 +00:00
ba4d8a428b [Gauge Plugin] Fix Missing Object handling (#7923)
* checking if the metadata exists before acting on it

* added a test to catch missing object errors in gauges

* remove waitForTimeout and add in check for time conductor successful start offset update

* hardening the test by checking for the time before the time change

* add "pageerror" to cspell
2024-12-03 15:13:51 -08:00
ea9947cab5 Use the disabled attribute on a valid element - the button. (#7914)
* Use the disabled attribute on a valid tag - the button.

* Add e2e test to check for add criteria button being enabled

* Improve test

* Check for add criteria button to have attribute disabled

* Remove focused test
2024-11-05 20:53:28 +00:00
2010f2e377 chore: bump @playwright/test to v1.48.1 (#7913) 2024-10-21 17:03:54 -07:00
3241e9ba57 chore: remove release.yml (#7907) 2024-10-18 15:43:57 -07:00
135 changed files with 3269 additions and 1312 deletions

View File

@ -5,7 +5,7 @@ orbs:
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.47.2-focal
- image: mcr.microsoft.com/playwright:v1.48.1-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
@ -198,7 +198,7 @@ jobs:
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npx playwright@1.47.2 install #Necessary for bare ubuntu machine
- run: npx playwright@1.48.1 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach

View File

@ -483,7 +483,9 @@
"countup",
"darkmatter",
"Undeletes",
"SSSZ"
"SSSZ",
"pageerror",
"annotatable"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"ignorePaths": [

View File

@ -37,7 +37,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.47.2 install
- run: npx playwright@1.48.1 install
- name: Start CouchDB Docker Container and Init with Setup Scripts
run: |
@ -51,12 +51,12 @@ jobs:
env:
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
run: npm run test:e2e:couchdb
- name: Generate Code Coverage Report
run: npm run cov:e2e:report
- name: Publish Results to Codecov.io
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/e2e/lcov.info
@ -66,15 +66,19 @@ jobs:
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: e2e-couchdb-test-results
path: test-results
overwrite: true
- name: Archive html test results
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: e2e-couchdb-html-test-results
path: html-test-results
overwrite: true
- name: Remove pr:e2e:couchdb label (if present)
if: always()

View File

@ -30,7 +30,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.47.2 install
- run: npx playwright@1.48.1 install
- run: npm ci --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times)
@ -38,9 +38,11 @@ jobs:
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: e2e-flakefinder-test-results
path: test-results
overwrite: true
- name: Remove pr:e2e:flakefinder label (if present)
if: always()

View File

@ -28,16 +28,18 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.47.2 install
- run: npx playwright@1.48.1 install
- run: npm ci --no-audit --progress=false
- run: npm run test:perf:localhost
- run: npm run test:perf:contract
- run: npm run test:perf:memory
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: e2e-perf-test-results
path: test-results
overwrite: true
- name: Remove pr:e2e:perf label (if present)
if: always()

View File

@ -45,9 +45,11 @@ jobs:
npm run cov:e2e:full:publish
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: e2e-pr-test-results
path: test-results
overwrite: true
- name: Remove pr:e2e label (if present)
if: always()

View File

@ -1,116 +0,0 @@
# GitHub Actions Workflow for Automated Releases
name: Automated Release Workflow
on:
schedule:
# Nightly builds at 6 PM PST every day
- cron: '0 2 * * *'
release:
types:
- created
- published
jobs:
nightly-build:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
name: Nightly Build and Release
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/iron' # Specify your Node.js version
registry-url: 'https://registry.npmjs.org/'
- name: Install Dependencies
run: npm ci
- name: Bump Version for Nightly
id: bump_version
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
DATE=$(date +%Y%m%d)
NIGHTLY_VERSION=$(echo $PACKAGE_VERSION | awk -F. -v OFS=. '{$NF+=1; print}')-nightly-$DATE
echo "NIGHTLY_VERSION=${NIGHTLY_VERSION}" >> $GITHUB_ENV
- name: Update package.json
run: |
npm version $NIGHTLY_VERSION --no-git-tag-version
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json
git commit -m "chore: bump version to $NIGHTLY_VERSION for nightly build"
- name: Push Changes
uses: ad-m/github-push-action@v0.6.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
- name: Build Project
run: npm run build:prod
- name: Publish Nightly to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm publish --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
prerelease-build:
if: github.event.release.prerelease == true
runs-on: ubuntu-latest
name: Pre-release (Beta) Build and Publish
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: '16' # Specify your Node.js version
registry-url: 'https://registry.npmjs.org/'
- name: Install Dependencies
run: npm ci
- name: Build Project
run: npm run build:prod
- name: Publish Beta to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
stable-release-build:
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
name: Stable Release Build and Publish
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: '16' # Specify your Node.js version
registry-url: 'https://registry.npmjs.org/'
- name: Install Dependencies
run: npm ci
- name: Build Project
run: npm run build:prod
- name: Publish to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

12
API.md
View File

@ -31,6 +31,10 @@
- [`latest` request strategy](#latest-request-strategy)
- [`minmax` request strategy](#minmax-request-strategy)
- [Telemetry Formats](#telemetry-formats)
- [Built-in Formats](#built-in-formats)
- [**Number Format (default):**](#number-format-default)
- [**String Format**](#string-format)
- [**Enum Format**](#enum-format)
- [Registering Formats](#registering-formats)
- [Telemetry Data](#telemetry-data)
- [Telemetry Datums](#telemetry-datums)
@ -59,6 +63,12 @@
- [Custom Indicators](#custom-indicators)
- [Priority API](#priority-api)
- [Priority Types](#priority-types)
- [User API](#user-api)
- [Example](#example)
- [Visibility-Based Rendering in View Providers](#visibility-based-rendering-in-view-providers)
- [Overview](#overview)
- [Implementing Visibility-Based Rendering](#implementing-visibility-based-rendering)
- [Example](#example-1)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@ -1301,9 +1311,11 @@ Open MCT provides some built-in priority values that can be used in the applicat
Currently, the Open MCT Priority API provides (type: numeric value):
- HIGHEST: Infinity
- HIGH: 1000
- Default: 0
- LOW: -1000
- LOWEST: -Infinity
View provider Example:

View File

@ -1,6 +1,10 @@
codecov:
require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass
# Disabling annotations for now. They are incorrectly labelling lines as lacking coverage when they are in fact covered by tests.
github_checks:
annotations: false
coverage:
status:
project:

View File

@ -2,7 +2,6 @@
module.exports = {
extends: ['plugin:playwright/recommended'],
rules: {
'playwright/max-nested-describe': ['error', { max: 1 }],
'playwright/expect-expect': 'off'
},
overrides: [

View File

@ -68,7 +68,11 @@ import { v4 as genUuid } from 'uuid';
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [options.parent='mine'] - The Identifier or uuid of the parent object. Defaults to 'mine' folder
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
async function createDomainObjectWithDefaults(
page,
{ type, name, parent = 'mine' },
additionalOptions = {}
) {
if (!name) {
name = `${type}:${genUuid()}`;
}
@ -89,6 +93,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
await page.getByLabel('Title', { exact: true }).fill('');
await page.getByLabel('Title', { exact: true }).fill(name);
if (additionalOptions) {
for (const [key, value] of Object.entries(additionalOptions)) {
// eslint-disable-next-line playwright/no-raw-locators
await page.locator(`#form-${key}`).fill(value);
}
}
if (page.testNotes) {
// Fill the "Notes" section with information about the
// currently running test and its project.
@ -105,7 +116,7 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
if (await _isInEditMode(page, uuid)) {
// Save (exit edit mode)
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
}
@ -682,6 +693,21 @@ async function linkParameterToObject(page, parameterName, objectName) {
await page.getByLabel('Save').click();
}
/**
* Rename the currently viewed `domainObject` from the browse bar.
*
* @param {import('@playwright/test').Page} page
* @param {string} newName
*/
async function renameCurrentObjectFromBrowseBar(page, newName) {
const nameInput = page.getByLabel('Browse bar object name');
await nameInput.click();
await nameInput.fill('');
await nameInput.fill(newName);
// Click the browse bar container to save changes
await page.getByLabel('Browse bar', { exact: true }).click();
}
export {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
@ -693,6 +719,7 @@ export {
linkParameterToObject,
navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime,
renameCurrentObjectFromBrowseBar,
setEndOffset,
setFixedIndependentTimeConductorBounds,
setFixedTimeMode,

View File

@ -103,25 +103,40 @@ const extendedTest = test.extend({
* Default: `true`
*/
failOnConsoleError: [true, { option: true }],
ignore404s: [[], { option: true }],
/**
* Extends the base page class to enable console log error detection.
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
*/
page: async ({ page, failOnConsoleError }, use) => {
page: async ({ page, failOnConsoleError, ignore404s }, use) => {
// Capture any console errors during test execution
const messages = [];
let messages = [];
page.on('console', (msg) => messages.push(msg));
await use(page);
if (ignore404s.length > 0) {
messages = messages.filter((msg) => {
let keep = true;
if (msg.text().match(/404 \((Object )?Not Found\)/) !== null) {
keep = ignore404s.every((ignoreRule) => {
return msg.location().url.match(ignoreRule) === null;
});
}
return keep;
});
}
// Assert against console errors during teardown
if (failOnConsoleError) {
messages.forEach((msg) =>
messages.forEach((msg) => {
// eslint-disable-next-line playwright/no-standalone-expect
expect
.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)
.not.toEqual('error')
);
.not.toEqual('error');
});
}
}
});

View File

@ -224,7 +224,7 @@ export async function createTimelistWithPlanAndSetActivityInProgress(page, planJ
await page.getByRole('button', { name: 'Edit Object' }).click();
// Find the display properties section in the inspector
await page.getByRole('tab', { name: 'View Properties' }).click();
await page.getByRole('tab', { name: 'Config' }).click();
// Switch to expanded view and save the setting
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });

View File

@ -76,6 +76,7 @@ export async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
export async function testTelemetryItem(page, telemetryItem) {
// Check that telemetry item also received the tag
await page.goto(telemetryItem.url);
await page.getByRole('tab', { name: 'Annotations' }).click();
await expect(page.getByText('No tags to display for this item')).toBeVisible();
@ -93,6 +94,7 @@ export async function testTelemetryItem(page, telemetryItem) {
y: 100
}
});
await page.getByRole('tab', { name: 'Annotations' }).click();
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
@ -107,6 +109,8 @@ export async function basicTagsTests(page) {
// Search for Driving
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('tab', { name: 'Annotations' }).click();
// Clicking elsewhere should cause annotation selection to be cleared
await expect(page.getByText('No tags to display for this item')).toBeVisible();
//
@ -119,6 +123,8 @@ export async function basicTagsTests(page) {
.first()
.click();
await page.getByRole('tab', { name: 'Annotations' }).click();
// Delete Driving Tag
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
@ -155,6 +161,8 @@ export async function basicTagsTests(page) {
}
});
await page.getByRole('tab', { name: 'Annotations' }).click();
//Expect Science to be visible but Driving to be hidden
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
@ -170,7 +178,7 @@ export async function basicTagsTests(page) {
});
// Add Driving Tag again
await page.getByText('Annotations').click();
await page.getByRole('tab', { name: 'Annotations' }).click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();

View File

@ -16,7 +16,7 @@
"devDependencies": {
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.47.2",
"@playwright/test": "1.48.1",
"@axe-core/playwright": "4.8.5"
},
"author": {

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../../appActions.js';
import { MISSION_TIME } from '../../../constants.js';
import { expect, test } from '../../../pluginFixtures.js';
const TELEMETRY_RATE = 2500;
test.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => {
test.beforeEach(async ({ page }) => {
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
await page.goto('./', { waitUntil: 'domcontentloaded' });
await setRealTimeMode(page);
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator with Acknowledge'
});
});
test('Rows are updatable in place', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7938'
});
await test.step('First telemetry datum gets added as new row', async () => {
await page.clock.fastForward(TELEMETRY_RATE);
const rows = page.getByLabel('table content').getByLabel('Table Row');
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
await expect(rows).toHaveCount(1);
await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK');
});
await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => {
await page.clock.fastForward(TELEMETRY_RATE * 2);
const rows = page.getByLabel('table content').getByLabel('Table Row');
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
await expect(rows).toHaveCount(1);
await expect(acknowledgeCell).toHaveAttribute('title', 'OK');
});
});
});

View File

@ -132,7 +132,7 @@ test("View a timelist in expanded view, verify all the activities are displayed
await page.getByRole('button', { name: 'Edit Object' }).click();
// Find the display properties section in the inspector
await page.getByRole('tab', { name: 'View Properties' }).click();
await page.getByRole('tab', { name: 'Config' }).click();
// Switch to expanded view and save the setting
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });

View File

@ -54,8 +54,7 @@ const examplePlanSmall1 = JSON.parse(
const TIME_TO_FROM_COLUMN = 2;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;
const FULL_CIRCLE_PATH =
'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';
const FULL_CIRCLE_PATH = 'M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z';
/**
* The regular expression used to parse the countdown string.

View File

@ -24,7 +24,9 @@ import {
createDomainObjectWithDefaults,
createPlanFromJSON,
navigateToObjectWithFixedTimeBounds,
setFixedIndependentTimeConductorBounds
setFixedIndependentTimeConductorBounds,
setFixedTimeMode,
setTimeConductorBounds
} from '../../../appActions.js';
import { expect, test } from '../../../pluginFixtures.js';
@ -74,21 +76,14 @@ const testPlan = {
};
test.describe('Time Strip', () => {
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5627'
});
// Constant locators
const activityBounds = page.locator('.activity-bounds');
let timestrip;
let plan;
test.beforeEach(async ({ page }) => {
// Goto baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
const timestrip = await test.step('Create a Time Strip', async () => {
timestrip = await test.step('Create a Time Strip', async () => {
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
expect(objectName).toBe(createdTimeStrip.name);
@ -96,7 +91,7 @@ test.describe('Time Strip', () => {
return createdTimeStrip;
});
const plan = await test.step('Create a Plan and add it to the timestrip', async () => {
plan = await test.step('Create a Plan and add it to the timestrip', async () => {
const createdPlan = await createPlanFromJSON(page, {
name: 'Test Plan',
json: testPlan
@ -110,6 +105,22 @@ test.describe('Time Strip', () => {
.dragTo(page.getByLabel('Object View'));
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
return createdPlan;
});
});
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5627'
});
// Constant locators
const activityBounds = page.locator('.activity-bounds');
await test.step('Set time strip to fixed timespan mode and verify activities', async () => {
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
@ -119,8 +130,6 @@ test.describe('Time Strip', () => {
// Verify all events are displayed
const eventCount = await page.locator('.activity-bounds').count();
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
return createdPlan;
});
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
@ -177,4 +186,48 @@ test.describe('Time Strip', () => {
expect(await activityBounds.count()).toEqual(1);
});
});
test('Time strip now line', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7817'
});
await test.step('Is displayed in realtime mode', async () => {
await expect(page.getByLabel('Now Marker')).toBeVisible();
});
await test.step('Is hidden when out of bounds of the time axis', async () => {
// Switch to fixed timespan mode
await setFixedTimeMode(page);
// Get the end bounds
const endBounds = await page.getByLabel('End bounds').textContent();
// Add 2 minutes to end bound datetime and use it as the new end time
let endTimeStamp = new Date(endBounds);
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() + 2);
const endDate = endTimeStamp.toISOString().split('T')[0];
const milliseconds = endTimeStamp.getMilliseconds();
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
// Subtract 1 minute from the end bound and use it as the new start time
let startTimeStamp = new Date(endBounds);
startTimeStamp.setUTCMinutes(startTimeStamp.getUTCMinutes() + 1);
const startDate = startTimeStamp.toISOString().split('T')[0];
const startMilliseconds = startTimeStamp.getMilliseconds();
const startTime = startTimeStamp
.toISOString()
.split('T')[1]
.replace(`.${startMilliseconds}Z`, '');
// Set fixed timespan mode to the future so that "now" is out of bounds.
await setTimeConductorBounds(page, {
startDate,
endDate,
startTime,
endTime
});
await expect(page.getByLabel('Now Marker')).toBeHidden();
});
});
});

View File

@ -27,7 +27,8 @@ demonstrate some playwright for test developers. This pattern should not be re-u
import {
createDomainObjectWithDefaults,
createExampleTelemetryObject
createExampleTelemetryObject,
setRealTimeMode
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
@ -116,7 +117,7 @@ test.describe('Basic Condition Set Use', () => {
await page.getByLabel('Conditions View').click();
await expect(page.getByText('Current Output')).toBeVisible();
});
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
test('ConditionSet produces an output when telemetry is available, and does not when it is not', async ({
page
}) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
@ -281,12 +282,142 @@ test.describe('Basic Condition Set Use', () => {
await page.goto(exampleTelemetry.url);
});
test('Short circuit evaluation does not cause incorrect evaluation https://github.com/nasa/openmct/issues/7992', async ({
page
}) => {
await setRealTimeMode(page);
await page.getByLabel('Create', { exact: true }).click();
await page.getByLabel('State Generator').click();
await page.getByLabel('Title', { exact: true }).fill('P1');
await page.getByLabel('State Duration (seconds)').fill('1');
await page.getByLabel('Save').click();
await page.getByLabel('Create', { exact: true }).click();
await page.getByLabel('State Generator').click();
await page.getByLabel('Title', { exact: true }).fill('P2');
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('1');
await page.getByRole('treeitem', { name: 'Test Condition Set' }).click();
await page.getByLabel('Save').click();
await page.getByLabel('Expand My Items folder').click();
await page.getByRole('treeitem', { name: 'Test Condition Set' }).click();
await page.getByLabel('Edit Object').click();
await page.getByLabel('Add Condition').click();
await page.getByLabel('Condition Name Input').first().fill('P1 IS ON AND P2 IS ON');
await page.getByLabel('Criterion Telemetry Selection').selectOption({ label: 'P1' });
await page.getByLabel('Criterion Metadata Selection').selectOption('value');
await page.getByLabel('Criterion Comparison Selection').selectOption('equalTo');
await page.getByLabel('Criterion Input').fill('1');
await page.getByLabel('Add Criteria - Enabled').click();
await page.getByLabel('Criterion Telemetry Selection').nth(1).selectOption({ label: 'P2' });
await page.getByLabel('Criterion Metadata Selection').nth(1).selectOption('value');
await page.getByLabel('Criterion Comparison Selection').nth(1).selectOption('equalTo');
await page.getByLabel('Criterion Input').nth(1).fill('1');
await page.getByLabel('Add Condition').click();
await page.getByLabel('Condition Name Input').first().fill('P1 IS OFF OR P2 IS OFF');
await page.getByLabel('Condition Trigger').first().selectOption('any');
await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ label: 'P1' });
await page.getByLabel('Criterion Metadata Selection').first().selectOption('value');
await page.getByLabel('Criterion Comparison Selection').first().selectOption('equalTo');
await page.getByLabel('Criterion Input').first().fill('0');
await page.getByLabel('Add Criteria - Enabled').first().click();
await page.getByLabel('Criterion Telemetry Selection').nth(1).selectOption({ label: 'P2' });
await page.getByLabel('Criterion Metadata Selection').nth(1).selectOption('value');
await page.getByLabel('Criterion Comparison Selection').nth(1).selectOption('equalTo');
await page.getByLabel('Criterion Input').nth(1).fill('0');
await page.getByLabel('Condition Name Input').first().dblclick();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.getByLabel('Edit Object').click();
/**
* Create default conditions for test. Start with invalid values to put condition set into
* "default" state
*/
await page.getByLabel('Test Data Telemetry Selection').selectOption({ label: 'P1' });
await page.getByLabel('Test Data Metadata Selection').selectOption({ label: 'Value' });
await page.getByLabel('Test Data Input').fill('3');
await page.getByLabel('Add Test Datum').click();
await page.getByLabel('Test Data Telemetry Selection').nth(1).selectOption({ label: 'P2' });
await page.getByLabel('Test Data Metadata Selection').nth(1).selectOption({ label: 'Value' });
await page.getByLabel('Test Data Input').nth(1).fill('3');
await page.getByLabel('Apply Test Data').nth(1).click();
let activeCondition = page.getByLabel('Active Condition Set Condition');
let activeConditionName = activeCondition.getByLabel('Condition Name Label');
await expect(activeConditionName).toHaveText('Default');
/**
* Set P1 to 0
*/
await page.getByLabel('Test Data Input').nth(0).fill('0');
activeCondition = page.getByLabel('Active Condition Set Condition');
activeConditionName = activeCondition.getByLabel('Condition Name Label');
await expect(activeConditionName).toHaveText('P1 IS OFF OR P2 IS OFF');
/**
* Set P2 to 1
*/
await page.getByLabel('Test Data Input').nth(1).fill('1');
activeCondition = page.getByLabel('Active Condition Set Condition');
activeConditionName = activeCondition.getByLabel('Condition Name Label');
await expect(activeConditionName).toHaveText('P1 IS OFF OR P2 IS OFF');
/**
* Set P1 to 1
*/
await page.getByLabel('Test Data Input').nth(0).fill('1');
activeCondition = page.getByLabel('Active Condition Set Condition');
activeConditionName = activeCondition.getByLabel('Condition Name Label');
await expect(activeConditionName).toHaveText('P1 IS ON AND P2 IS ON');
});
test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7484'
});
});
test('ConditionSet has add criteria button enabled/disabled when composition is and is not available', async ({
page
}) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
await page.getByLabel('Show selected item in tree').click();
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
// Create a condition
await page.locator('#addCondition').click();
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
// Validate that the add criteria button is disabled
await expect(page.getByLabel('Add Criteria - Disabled')).toHaveAttribute('disabled');
// Add Telemetry to ConditionSet
const sineWaveGeneratorTreeItem = page
.getByRole('tree', {
name: 'Main Tree'
})
.getByRole('treeitem', {
name: exampleTelemetry.name
});
const conditionCollection = page.locator('#conditionCollection');
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
// Validate that the add criteria button is enabled and adds a new criterion
await expect(page.getByLabel('Add Criteria - Enabled')).not.toHaveAttribute('disabled');
await page.getByLabel('Add Criteria - Enabled').click();
const numOfUnnamedCriteria = await page.getByLabel('Criterion Telemetry Selection').count();
expect(numOfUnnamedCriteria).toEqual(2);
});
});
test.describe('Condition Set Composition', () => {

View File

@ -236,7 +236,7 @@ test.describe('Display Layout', () => {
name: new RegExp(sineWaveObject.name)
});
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
await page.locator('button[title="Save"]').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
@ -278,7 +278,7 @@ test.describe('Display Layout', () => {
name: new RegExp(sineWaveObject.name)
});
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
await page.locator('button[title="Save"]').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
@ -317,7 +317,7 @@ test.describe('Display Layout', () => {
name: new RegExp(sineWaveObject.name)
});
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
await page.locator('button[title="Save"]').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
@ -358,7 +358,7 @@ test.describe('Display Layout', () => {
name: new RegExp(sineWaveObject.name)
});
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
await page.locator('button[title="Save"]').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
@ -413,7 +413,7 @@ test.describe('Display Layout', () => {
await page.locator('div[title="Resize object width"] > input').click();
await page.locator('div[title="Resize object width"] > input').fill('70');
await page.locator('button[title="Save"]').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const startDate = '2021-12-30 01:01:00.000Z';
@ -473,7 +473,7 @@ test.describe('Display Layout', () => {
await page.getByText('View type').click();
await page.getByText('Overlay Plot').click();
await page.getByLabel('Save').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Time to inspect some network traffic
@ -507,8 +507,140 @@ test.describe('Display Layout', () => {
// In real time mode, we don't fetch annotations at all
await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(0);
});
test('Same objects with different request options have unique subscriptions', async ({
page
}) => {
// Expand My Items
await page.getByLabel('Expand My Items folder').click();
// Create a Display Layout
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Test Display'
});
// Create a State Generator, set to higher frequency updates
const stateGenerator = await createDomainObjectWithDefaults(page, {
type: 'State Generator',
name: 'State Generator'
});
const stateGeneratorTreeItem = page.getByRole('treeitem', {
name: stateGenerator.name
});
await stateGeneratorTreeItem.click({ button: 'right' });
await page.getByLabel('Edit Properties...').click();
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
await page.getByLabel('Save', { exact: true }).click();
// Create a Table for filtering ON values
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
name: 'Table Filter On Value'
});
const tableFilterOnTreeItem = page.getByRole('treeitem', {
name: tableFilterOnValue.name
});
// Create a Table for filtering OFF values
const tableFilterOffValue = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
name: 'Table Filter Off Value'
});
const tableFilterOffTreeItem = page.getByRole('treeitem', {
name: tableFilterOffValue.name
});
// Navigate to ON filtering table and add state generator and setup filters
await page.goto(tableFilterOnValue.url);
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
await selectFilterOption(page, '1');
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Navigate to OFF filtering table and add state generator and setup filters
await page.goto(tableFilterOffValue.url);
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
await selectFilterOption(page, '0');
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Navigate to the display layout and edit it
await page.goto(displayLayout.url);
// Add the tables to the display layout
await page.getByLabel('Edit Object').click();
await tableFilterOffTreeItem.dragTo(page.getByLabel('Layout Grid'), {
targetPosition: { x: 10, y: 300 }
});
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
targetPosition: { x: 400, y: 500 },
// eslint-disable-next-line playwright/no-force-option
force: true
});
await tableFilterOnTreeItem.dragTo(page.getByLabel('Layout Grid'), {
targetPosition: { x: 10, y: 100 }
});
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
targetPosition: { x: 400, y: 300 },
// eslint-disable-next-line playwright/no-force-option
force: true
});
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Get the tables so we can verify filtering is working as expected
const tableFilterOn = page.getByLabel(`${tableFilterOnValue.name} Frame`, {
exact: true
});
const tableFilterOff = page.getByLabel(`${tableFilterOffValue.name} Frame`, {
exact: true
});
// Verify filtering is working correctly
// Check that no filtered values appear for at least 2 seconds
const VERIFICATION_TIME = 2000; // 2 seconds
const CHECK_INTERVAL = 100; // Check every 100ms
// Create a promise that will check for filtered values periodically
const checkForCorrectValues = new Promise((resolve, reject) => {
const interval = setInterval(async () => {
const offCount = await tableFilterOn.locator('td[title="OFF"]').count();
const onCount = await tableFilterOff.locator('td[title="ON"]').count();
if (offCount > 0 || onCount > 0) {
clearInterval(interval);
reject(
new Error(
`Found ${offCount} OFF and ${onCount} ON values when expecting 0 OFF and 0 ON`
)
);
}
}, CHECK_INTERVAL);
// After VERIFICATION_TIME, if no filtered values were found, resolve successfully
setTimeout(() => {
clearInterval(interval);
resolve();
}, VERIFICATION_TIME);
});
await expect(checkForCorrectValues).resolves.toBeUndefined();
});
});
async function selectFilterOption(page, filterOption) {
await page.getByRole('tab', { name: 'Filters' }).click();
await page
.getByLabel('Inspector Views')
.locator('li')
.filter({ hasText: 'State Generator' })
.locator('span')
.click();
await page.getByRole('switch').click();
await page.selectOption('select[name="setSelectionThreshold"]', filterOption);
}
async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);

View File

@ -28,7 +28,9 @@ import { v4 as uuid } from 'uuid';
import {
createDomainObjectWithDefaults,
createExampleTelemetryObject
createExampleTelemetryObject,
setRealTimeMode,
setStartOffset
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
@ -166,6 +168,57 @@ test.describe('Gauge', () => {
);
});
test('Gauge does not break when an object is missing', async ({ page }) => {
// Set up error listeners
const pageErrors = [];
// Listen for uncaught exceptions
page.on('pageerror', (err) => {
pageErrors.push(err.message);
});
await setRealTimeMode(page);
// Create a Gauge
const gauge = await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Gauge with missing object'
});
// Create a Sine Wave Generator in the Gauge with a loading delay
const missingSWG = await createExampleTelemetryObject(page, gauge.uuid);
// Remove the object from local storage
await page.evaluate(
([missingObject]) => {
const mct = localStorage.getItem('mct');
const mctObjects = JSON.parse(mct);
delete mctObjects[missingObject.uuid];
localStorage.setItem('mct', JSON.stringify(mctObjects));
},
[missingSWG]
);
// Verify start bounds
await expect(page.getByLabel('Start offset: 00:30:00')).toBeVisible();
// Nav to the Gauge
await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });
// adjust time bounds and ensure they are updated
await setStartOffset(page, {
startHours: '00',
startMins: '45',
startSecs: '00'
});
// Verify start bounds changed
await expect(page.getByLabel('Start offset: 00:45:00')).toBeVisible();
// // Verify no errors were thrown
expect(pageErrors).toHaveLength(0);
});
test('Gauge enforces composition policy', async ({ page }) => {
// Create a Gauge
await createDomainObjectWithDefaults(page, {

View File

@ -54,8 +54,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
await page.goto(exampleDataVisualizationSource.url);
await page.getByRole('tab', { name: 'Data Visualization' }).click();
await page.getByRole('cell', { name: /First Sine Wave Generator/ }).click();
await page.getByRole('tab', { name: 'Data Visualization' }).click();
await expect(page.getByText('Numeric Data')).toBeVisible();
await expect(
page.locator('span.plot-series-name', { hasText: 'First Sine Wave Generator Hz' })
@ -63,6 +63,7 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
await page.getByRole('cell', { name: /Second Sine Wave Generator/ }).click();
await page.getByRole('tab', { name: 'Data Visualization' }).click();
await expect(page.getByText('Numeric Data')).toBeVisible();
await expect(
page.locator('span.plot-series-name', { hasText: 'Second Sine Wave Generator Hz' })
@ -77,6 +78,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
// ensure our new tab's title is correct
const newPage = await pagePromise;
await newPage.waitForLoadState();
await page.getByRole('tab', { name: 'Data Visualization' }).click();
// expect new tab title to contain 'Second Sine Wave Generator'
await expect(newPage).toHaveTitle('Second Sine Wave Generator');

View File

@ -53,7 +53,6 @@ test.describe('Testing LAD table configuration', () => {
test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => {
// Edit LAD table
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// make sure headers are visible initially
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
@ -114,7 +113,6 @@ test.describe('Testing LAD table configuration', () => {
// Edit LAD table
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// show timestamp column
await page.getByLabel('Timestamp', { exact: true }).check();
@ -142,7 +140,6 @@ test.describe('Testing LAD table configuration', () => {
// Edit LAD table
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// show units, type, and WATCH columns
await page.getByLabel('Units').check();
@ -182,7 +179,6 @@ test.describe('Testing LAD table configuration', () => {
// Edit LAD table
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// make sure Sine Wave headers are visible initially too
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();

View File

@ -26,7 +26,10 @@ This test suite is dedicated to tests which verify the basic operations surround
import { fileURLToPath } from 'url';
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import {
createDomainObjectWithDefaults,
renameCurrentObjectFromBrowseBar
} from '../../../../appActions.js';
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
import * as nbUtils from '../../../../helper/notebookUtils.js';
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
@ -596,4 +599,61 @@ test.describe('Notebook entry tests', () => {
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
});
test('When changing the name of a notebook in the browse bar, new notebook changes are not lost', async ({
page
}) => {
const TEST_TEXT = 'Do not lose me!';
const FIRST_NEW_NAME = 'New Name';
const SECOND_NEW_NAME = 'Second New Name';
await page.goto(notebookObject.url);
await page.getByLabel('Expand My Items folder').click();
await renameCurrentObjectFromBrowseBar(page, FIRST_NEW_NAME);
// verify the name change in tree and browse bar
await verifyNameChange(page, FIRST_NEW_NAME);
// enter one entry
await enterAndCommitTextEntry(page, TEST_TEXT);
// verify the entry is present
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
// change the name
await renameCurrentObjectFromBrowseBar(page, SECOND_NEW_NAME);
// verify the name change in tree and browse bar
await verifyNameChange(page, SECOND_NEW_NAME);
// verify the entry is still present
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
});
});
/**
* Enter text into the last notebook entry and commit it.
*
* @param {import('@playwright/test').Page} page
* @param {string} text
*/
async function enterAndCommitTextEntry(page, text) {
await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, text);
await nbUtils.commitEntry(page);
}
/**
* Verify the name change in the tree and browse bar.
*
* @param {import('@playwright/test').Page} page
* @param {string} newName
*/
async function verifyNameChange(page, newName) {
await expect(
page.getByRole('treeitem').locator('.is-navigated-object .c-tree__item__name')
).toHaveText(newName);
await expect(page.getByLabel('Browse bar object name')).toHaveText(newName);
}

View File

@ -65,9 +65,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
});
test('Can add tags with blank entry', async ({ page }) => {
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
await page.getByRole('tab', { name: 'Annotations' }).click();
await enterTextEntry(page, '');
await page.getByRole('tab', { name: 'Annotations' }).click();
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();

View File

@ -47,8 +47,6 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
});
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
//Ensure we're on the annotations Tab in the inspector
await page.getByText('Annotations').click();
// Expand sidebar
await page.locator('.c-notebook__toggle-nav-button').click();
@ -86,6 +84,9 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
await page.waitForLoadState('networkidle');
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
//Ensure we're on the annotations Tab in the inspector
await page.getByText('Annotations').click();
// Add some tags
// Network Requests are for each tag creation are:
// 1) Getting the original path of the parent object
@ -180,8 +181,8 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
type: 'issue',
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
});
await page.getByText('Annotations').click();
await nbUtils.enterTextEntry(page, 'First Entry');
await page.getByText('Annotations').click();
// Add three tags
await addTagAndAwaitNetwork(page, 'Science');

View File

@ -100,6 +100,9 @@ test.describe('Overlay Plot', () => {
await page.getByLabel('Expand By Default').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the legend is now open
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
@ -111,6 +114,9 @@ test.describe('Overlay Plot', () => {
// Assert that the legend is expanded on page load
await page.reload();
await page.getByRole('tab', { name: 'Config' }).click();
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();

View File

@ -108,4 +108,42 @@ test.describe('Plot Controls', () => {
// Expect before and after plot points to match
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
});
/*
Test to verify that switching a plot's time context from global to
its own independent time context and then back to global context works correctly.
After switching from fixed time mode (ITC) to real time mode (global context),
the pause control for the plot should be available, indicating that it is following the right context.
*/
test('Plots follow the right time context', async ({ page }) => {
// Set global time conductor to real-time mode
await setRealTimeMode(page);
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// Ensure pause control is visible since global time conductor is in Real time mode.
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
// Toggle independent time conductor ON
await page.getByLabel('Enable Independent Time Conductor').click();
// Bring up the independent time conductor popup and switch to fixed time mode
await page.getByLabel('Independent Time Conductor Settings').click();
await page.getByLabel('Independent Time Conductor Mode Menu').click();
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// Ensure pause control is no longer visible since the plot is following the independent time context
await expect(page.getByTitle('Pause incoming real-time data')).toBeHidden();
// Toggle independent time conductor OFF - Note that the global time conductor is still in Real time mode
await page.getByLabel('Disable Independent Time Conductor').click();
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// Ensure pause control is visible since the global time conductor is in real time mode
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
});
});

View File

@ -0,0 +1,58 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2025, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite is dedicated to testing the rendering and interaction of plots.
*
*/
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Plot Controls in compact mode', () => {
let timeStrip;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
timeStrip = await createDomainObjectWithDefaults(page, {
type: 'Time Strip'
});
// Create an overlay plot with a sine wave generator
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: timeStrip.uuid
});
await page.goto(`${timeStrip.url}`);
});
test('Plots show cursor guides', async ({ page }) => {
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// click on cursor guides control
await page.getByTitle('Toggle cursor guides').click();
await page.getByLabel('Plot Canvas').hover();
await expect(page.getByLabel('Vertical cursor guide')).toBeVisible();
await expect(page.getByLabel('Horizontal cursor guide')).toBeVisible();
});
});

View File

@ -0,0 +1,244 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Tests to verify log plot functionality when objects are missing
*/
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
const SWG_NAME = 'Sine Wave Generator';
const OVERLAY_PLOT_NAME = 'Overlay Plot';
const STACKED_PLOT_NAME = 'Stacked Plot';
test.describe('For a default Plot View, Plot View Action:', () => {
let download;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const plot = await createDomainObjectWithDefaults(page, {
type: SWG_NAME,
name: SWG_NAME
});
await page.goto(plot.url);
// Set up dialog handler before clicking the export button
await page.getByLabel('More actions').click();
});
test.afterEach(async ({ page }) => {
if (download) {
await download.cancel();
}
});
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as PNG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);
});
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as JPG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);
});
});
test.describe('For an Overlay Plot View, Plot View Action:', () => {
let download;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: OVERLAY_PLOT_NAME,
name: OVERLAY_PLOT_NAME
});
await createDomainObjectWithDefaults(page, {
type: SWG_NAME,
name: SWG_NAME,
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Set up dialog handler before clicking the export button
await page.getByLabel('More actions').click();
});
test.afterEach(async ({ page }) => {
if (download) {
await download.cancel();
}
});
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as PNG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.png`);
});
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as JPG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.jpeg`);
});
});
test.describe('For a Stacked Plot View, Plot View Action:', () => {
let download;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const stackedPlot = await createDomainObjectWithDefaults(page, {
type: STACKED_PLOT_NAME,
name: STACKED_PLOT_NAME
});
await createDomainObjectWithDefaults(page, {
type: SWG_NAME,
name: SWG_NAME,
parent: stackedPlot.uuid
});
await page.goto(stackedPlot.url);
// Set up dialog handler before clicking the export button
await page.getByLabel('More actions').click();
});
test.afterEach(async ({ page }) => {
if (download) {
await download.cancel();
}
});
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as PNG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.png`);
});
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as JPG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.jpeg`);
});
});
test.describe('Plot View Action:', () => {
let download;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const plot = await createDomainObjectWithDefaults(page, {
type: SWG_NAME,
name: `!@#${SWG_NAME}!@#><`
});
await page.goto(plot.url);
// Set up dialog handler before clicking the export button
await page.getByLabel('More actions').click();
});
test.afterEach(async ({ page }) => {
if (download) {
await download.cancel();
}
});
test('Export as PNG saved filenames will not include invalid characters', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as PNG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);
});
test('Export as JPG saved filenames will not include invalid characters', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as JPG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);
});
});

View File

@ -50,7 +50,7 @@ test.describe('Plots work in Previews', () => {
});
const layoutGridHolder = page.getByLabel('Test Display Layout Layout Grid');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.getByLabel('Save').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// right click on the plot and select view large
@ -67,7 +67,7 @@ test.describe('Plots work in Previews', () => {
await page.getByLabel('Move Sub-object Frame').click();
await page.getByText('View type').click();
await page.getByText('Overlay Plot').click();
await page.getByLabel('Save').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(
page.getByLabel('Test Display Layout Layout', { exact: true }).getByLabel('Plot Canvas')

View File

@ -152,14 +152,14 @@ test.describe('Stacked Plot', () => {
}) => {
await page.goto(stackedPlot.url);
await page.getByRole('tab', { name: 'Config' }).click();
// Click on the 1st plot
await page
.getByLabel('Stacked Plot Item Sine Wave Generator A')
.getByLabel('Plot Canvas')
.click();
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the inspector shows the Y Axis properties for swgA
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
@ -172,6 +172,9 @@ test.describe('Stacked Plot', () => {
.getByLabel('Stacked Plot Item Sine Wave Generator B')
.getByLabel('Plot Canvas')
.click();
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the inspector shows the Y Axis properties for swgB
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
@ -184,6 +187,9 @@ test.describe('Stacked Plot', () => {
.getByLabel('Stacked Plot Item Sine Wave Generator C')
.getByLabel('Plot Canvas')
.click();
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the inspector shows the Y Axis properties for swgB
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
@ -194,7 +200,7 @@ test.describe('Stacked Plot', () => {
// Go into edit mode
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
// await page.getByRole('tab', { name: 'Config' }).click();
// Click on the 1st plot
await page.getByLabel('Stacked Plot Item Sine Wave Generator A').click();
@ -233,11 +239,11 @@ test.describe('Stacked Plot', () => {
// Go into edit mode
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
// Click on canvas for the 1st plot
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
await page.getByRole('tab', { name: 'Config' }).click();
// Expand config for the series
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();
@ -255,6 +261,8 @@ test.describe('Stacked Plot', () => {
// Click on canvas for the 1st plot
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
await page.getByRole('tab', { name: 'Config' }).click();
// Expand config for the series
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();

View File

@ -45,6 +45,8 @@ const setFontFamily = '"Andale Mono", sans-serif';
test.describe('Stacked Plot styling', () => {
let stackedPlot;
let overlayPlot1;
let overlayPlot2;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
@ -54,17 +56,30 @@ test.describe('Stacked Plot styling', () => {
name: 'StackedPlot1'
});
// create two overlay plots
overlayPlot1 = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Overlay Plot 1',
parent: stackedPlot.uuid
});
overlayPlot2 = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Overlay Plot 2',
parent: stackedPlot.uuid
});
// Create two SWGs and attach them to the Stacked Plot
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Sine Wave Generator 1',
parent: stackedPlot.uuid
parent: overlayPlot1.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Sine Wave Generator 2',
parent: stackedPlot.uuid
parent: overlayPlot2.uuid
});
});
@ -138,21 +153,21 @@ test.describe('Stacked Plot styling', () => {
NO_STYLE_RGBA,
NO_STYLE_RGBA,
hexToRGB(setTextColor),
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
page.getByLabel('Stacked Plot Item Overlay Plot 1')
);
await checkStyles(
NO_STYLE_RGBA,
NO_STYLE_RGBA,
hexToRGB(setTextColor),
page.getByLabel('Stacked Plot Item Sine Wave Generator 2')
page.getByLabel('Stacked Plot Item Overlay Plot 2')
);
await checkFontStyles(
setFontSize,
setFontWeight,
setFontFamily,
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
page.getByLabel('Stacked Plot Item Overlay Plot 1')
);
});
@ -169,19 +184,19 @@ test.describe('Stacked Plot styling', () => {
await page.getByRole('tab', { name: 'Styles' }).click();
//Check default styles for SWG1 and SWG2
//Check default styles for overlayPlot1 and overlayPlot2
await checkStyles(
NO_STYLE_RGBA,
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
page.getByLabel('Stacked Plot Item Overlay Plot 1')
);
await checkStyles(
NO_STYLE_RGBA,
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('Stacked Plot Item Sine Wave Generator 2')
page.getByLabel('Stacked Plot Item Overlay Plot 2')
);
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
@ -190,11 +205,11 @@ test.describe('Stacked Plot styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
page.getByLabel('Stacked Plot Item Overlay Plot 1')
);
//Set Font Styles on SWG1 but not SWG2
await page.getByLabel('Stacked Plot Item Sine Wave Generator 1').click();
await page.getByLabel('Stacked Plot Item Overlay Plot 1').click();
//Set Font Size to 72
await page.getByLabel('Set Font Size').click();
await page.getByRole('menuitem', { name: '72px' }).click();

View File

@ -31,6 +31,8 @@ import { expect, test } from '../../pluginFixtures.js';
test.describe('Grand Search', () => {
let grandSearchInput;
test.use({ ignore404s: [/_design\/object_names\/_view\/object_names$/] });
test.beforeEach(async ({ page }) => {
grandSearchInput = page
.getByLabel('OpenMCT Search')
@ -191,7 +193,88 @@ test.describe('Grand Search', () => {
await expect(searchResults).toContainText(folderName);
});
test.describe('Search will test for the presence of the object_names index, and', () => {
test('use index if available @couchdb @network', async ({ page }) => {
await createObjectsForSearch(page);
let isObjectNamesViewAvailable = false;
let isObjectNamesUsedForSearch = false;
page.on('request', async (request) => {
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
const isHeadRequest = request.method().toLowerCase() === 'head';
if (isObjectNamesRequest && isHeadRequest) {
const response = await request.response();
isObjectNamesViewAvailable = response.status() === 200;
}
});
page.on('request', (request) => {
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
const isPostRequest = request.method().toLowerCase() === 'post';
if (isObjectNamesRequest && isPostRequest) {
isObjectNamesUsedForSearch = true;
}
});
// Full search for object
await grandSearchInput.pressSequentially('Clock', { delay: 100 });
// Wait for search to finish
await waitForSearchCompletion(page);
expect(isObjectNamesViewAvailable).toBe(true);
expect(isObjectNamesUsedForSearch).toBe(true);
});
test('fall-back on base index if index not available @couchdb @network', async ({ page }) => {
await page.route('**/_view/object_names', (route) => {
route.fulfill({
status: 404
});
});
await createObjectsForSearch(page);
let isObjectNamesViewAvailable = false;
let isFindUsedForSearch = false;
page.on('request', async (request) => {
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
const isHeadRequest = request.method().toLowerCase() === 'head';
if (isObjectNamesRequest && isHeadRequest) {
const response = await request.response();
isObjectNamesViewAvailable = response.status() === 200;
}
});
page.on('request', (request) => {
const isFindRequest = request.url().endsWith('_find');
const isPostRequest = request.method().toLowerCase() === 'post';
if (isFindRequest && isPostRequest) {
isFindUsedForSearch = true;
}
});
// Full search for object
await grandSearchInput.pressSequentially('Clock', { delay: 100 });
// Wait for search to finish
await waitForSearchCompletion(page);
console.info(
`isObjectNamesViewAvailable: ${isObjectNamesViewAvailable} | isFindUsedForSearch: ${isFindUsedForSearch}`
);
expect(isObjectNamesViewAvailable).toBe(false);
expect(isFindUsedForSearch).toBe(true);
});
});
test('Search results are debounced @couchdb @network', async ({ page }) => {
// Unfortunately 404s are always logged to the JavaScript console and can't be suppressed
// A 404 is now thrown when we test for the presence of the object names view used by search.
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6179'
@ -199,11 +282,17 @@ test.describe('Grand Search', () => {
await createObjectsForSearch(page);
let networkRequests = [];
page.on('request', (request) => {
const searchRequest =
request.url().endsWith('_find') || request.url().includes('by_keystring');
const fetchRequest = request.resourceType() === 'fetch';
if (searchRequest && fetchRequest) {
const isSearchRequest =
request.url().endsWith('object_names') ||
request.url().endsWith('_find') ||
request.url().includes('by_keystring');
const isFetchRequest = request.resourceType() === 'fetch';
// CouchDB search results in a one-time head request to test for the presence of an index.
const isHeadRequest = request.method().toLowerCase() === 'head';
if (isSearchRequest && isFetchRequest && !isHeadRequest) {
networkRequests.push(request);
}
});

View File

@ -1,3 +1,5 @@
/* eslint-disable playwright/no-conditional-in-test */
/* eslint-disable playwright/no-conditional-expect */
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@ -31,6 +33,104 @@ Enim nec dui nunc mattis. Cursus turpis massa tincidunt dui ut. Donec adipiscing
Proin libero nunc consequat interdum varius sit amet mattis vulputate. Metus dictum at tempor commodo ullamcorper a lacus vestibulum sed. Quisque non tellus orci ac auctor augue mauris. Id ornare arcu odio ut. Rhoncus est pellentesque elit ullamcorper dignissim. Senectus et netus et malesuada fames ac turpis egestas. Volutpat ac tincidunt vitae semper quis lectus nulla. Adipiscing elit duis tristique sollicitudin. Ipsum faucibus vitae aliquet nec ullamcorper sit. Gravida neque convallis a cras semper auctor neque vitae tempus. Porttitor leo a diam sollicitudin tempor id. Dictum non consectetur a erat nam at lectus. At volutpat diam ut venenatis tellus in. Morbi enim nunc faucibus a pellentesque sit amet. Cursus in hac habitasse platea. Sed augue lacus viverra vitae.
`;
const viewsTabsMatrix = {
Clock: {
Browse: ['Properties']
},
'Condition Set': {
Browse: ['Properties', 'Elements', 'Annotations'],
Edit: ['Elements', 'Properties']
},
'Condition Widget': {
Browse: ['Properties', 'Styles'],
Edit: ['Styles', 'Properties']
},
'Display Layout': {
Browse: ['Properties', 'Elements', 'Styles'],
Edit: ['Elements', 'Styles', 'Properties']
},
'Event Message Generator': {
Browse: ['Properties']
},
'Event Message Generator with Acknowledge': {
Browse: ['Properties']
},
'Example Imagery': {
Browse: ['Properties', 'Annotations']
},
'Flexible Layout': {
Browse: ['Properties', 'Elements', 'Styles'],
Edit: ['Elements', 'Styles', 'Properties']
},
Folder: {
Browse: ['Properties']
},
'Gantt Chart': {
Browse: ['Properties', 'Config', 'Elements'],
Edit: ['Config', 'Elements', 'Properties']
},
Gauge: {
Browse: ['Properties', 'Elements', 'Styles'],
Edit: ['Elements', 'Styles', 'Properties']
},
Graph: {
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Properties']
},
Hyperlink: {
Browse: ['Properties'],
required: {
url: 'https://www.google.com',
displayText: 'Google'
}
},
'LAD Table': {
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Properties']
},
'LAD Table Set': {
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Properties']
},
Notebook: {
Browse: ['Properties']
},
'Overlay Plot': {
Browse: ['Properties', 'Config', 'Annotations', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Filters', 'Properties']
},
'Scatter Plot': {
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Properties']
},
'Sine Wave Generator': {
Browse: ['Properties', 'Annotations']
},
'Stacked Plot': {
Browse: ['Properties', 'Config', 'Annotations', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Properties']
},
'Tabs View': {
Browse: ['Properties', 'Elements', 'Styles'],
Edit: ['Elements', 'Styles', 'Properties']
},
'Telemetry Table': {
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Filters', 'Properties']
},
'Time List': {
Browse: ['Properties', 'Config', 'Elements'],
Edit: ['Config', 'Elements', 'Properties']
},
'Time Strip': {
Browse: ['Properties', 'Elements'],
Edit: ['Elements', 'Properties']
},
Timer: {
Browse: ['Properties']
}
};
test.describe('Inspector tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
@ -72,4 +172,49 @@ test.describe('Inspector tests', () => {
await expect(lastInspectorPropertyValue).toBeInViewport();
});
test(`Inspector tabs show the correct tabs per view and mode`, async ({ page }) => {
// loop through each view type
for (const view of Object.keys(viewsTabsMatrix)) {
const viewConfig = viewsTabsMatrix[view];
const createOptions = {
type: view,
name: view
};
// create and navigate to view;
const objectInfo = await createDomainObjectWithDefaults(
page,
createOptions,
viewConfig.required ? viewConfig.required : {}
);
await page.goto(objectInfo.url);
// verify correct number of tabs for browse mode
expect(await page.getByRole('tab').count()).toBe(Object.keys(viewConfig.Browse).length);
// verify correct order of tabs for browse mode
for (const [index, value] of Object.entries(viewConfig.Browse)) {
const tab = page.getByRole('tab').nth(index);
await expect(tab).toHaveText(value);
}
// enter Edit if necessary
if (viewConfig.Edit) {
await page.getByLabel('Edit Object').click();
// verify correct number of tabs for edit mode
expect(await page.getByRole('tab').count()).toBe(Object.keys(viewConfig.Edit).length);
// verify correct order of tabs for edit mode
for (const [index, value] of Object.entries(viewConfig.Edit)) {
const tab = page.getByRole('tab').nth(index);
await expect(tab).toHaveText(value);
}
await page.getByLabel('Save').first().click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
}
}
});
});

View File

@ -213,7 +213,6 @@ test.describe('Navigation memory leak is not detected in', () => {
page,
'example-imagery-memory-leak-test'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
@ -317,6 +316,12 @@ test.describe('Navigation memory leak is not detected in', () => {
// Manually invoke the garbage collector once all references are removed.
window.gc();
window.gc();
window.gc();
setTimeout(() => {
window.gc();
}, 1000);
return gcPromise;
});

View File

@ -40,6 +40,9 @@ test.describe('Visual - Inspector @ally @clock', () => {
});
test('Inspector from overlay_plot_with_delay_storage @localStorage', async ({ page, theme }) => {
// navigate to the plot
await page.getByRole('gridcell', { name: 'Overlay Plot with 5s Delay' }).click();
//Expand the Inspector Pane
await page.getByRole('button', { name: 'Inspect' }).click();

View File

@ -83,7 +83,7 @@ test.describe('Grand Search @a11y', () => {
);
// Save and finish editing the Display Layout
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Search for the object

View File

@ -100,9 +100,12 @@ test.describe('Flexible Layout styling @a11y', () => {
);
// Save Flexible Layout
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Select styles tab
await page.getByRole('tab', { name: 'Styles' }).click();
await percySnapshot(
page,
`Saved Styled Flex Layout with Styled StackedPlot (theme: '${theme}')`
@ -124,17 +127,30 @@ test.describe('Stacked Plot styling @a11y', () => {
name: 'StackedPlot1'
});
// Create two SWGs and attach them to the Stacked Plot
// Create an overlay plots to hold the SWGs
const overlayPlot1 = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Overlay Plot 1',
parent: stackedPlot.uuid
});
const overlayPlot2 = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Overlay Plot 2',
parent: stackedPlot.uuid
});
// Create two SWGs and attach them to the overlay plots
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Sine Wave Generator 1',
parent: stackedPlot.uuid
parent: overlayPlot1.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Sine Wave Generator 2',
parent: stackedPlot.uuid
parent: overlayPlot2.uuid
});
});
@ -177,7 +193,7 @@ test.describe('Stacked Plot styling @a11y', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
page.getByLabel('Stacked Plot Item Overlay Plot 1')
);
await percySnapshot(page, `Edit Mode StackedPlot with Styled SWG (theme: '${theme}')`);

View File

@ -46,6 +46,24 @@ class EventMetadataProvider {
]
}
};
const inPlaceUpdateMetadataValue = {
key: 'messageId',
name: 'row identifier',
format: 'string',
useToUpdateInPlace: true
};
const eventAcknowledgeMetadataValue = {
key: 'acknowledge',
name: 'Acknowledge',
format: 'string'
};
const eventGeneratorWithAcknowledge = structuredClone(this.METADATA_BY_TYPE.eventGenerator);
eventGeneratorWithAcknowledge.values.push(inPlaceUpdateMetadataValue);
eventGeneratorWithAcknowledge.values.push(eventAcknowledgeMetadataValue);
this.METADATA_BY_TYPE.eventGeneratorWithAcknowledge = eventGeneratorWithAcknowledge;
}
supportsMetadata(domainObject) {

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
*/
import EventTelemetryProvider from './EventTelemetryProvider.js';
class EventWithAcknowledgeTelemetryProvider extends EventTelemetryProvider {
constructor() {
super();
this.unAcknowledgedData = undefined;
}
generateData(firstObservedTime, count, startTime, duration, name) {
if (this.unAcknowledgedData === undefined) {
const unAcknowledgedData = super.generateData(
firstObservedTime,
count,
startTime,
duration,
name
);
unAcknowledgedData.messageId = unAcknowledgedData.message;
this.unAcknowledgedData = unAcknowledgedData;
return this.unAcknowledgedData;
} else {
const acknowledgedData = {
...this.unAcknowledgedData,
acknowledge: 'OK'
};
this.unAcknowledgedData = undefined;
return acknowledgedData;
}
}
supportsRequest(domainObject) {
return false;
}
supportsSubscribe(domainObject) {
return domainObject.type === 'eventGeneratorWithAcknowledge';
}
}
export default EventWithAcknowledgeTelemetryProvider;

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import EventMetadataProvider from './EventMetadataProvider.js';
import EventTelemetryProvider from './EventTelemetryProvider.js';
import EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js';
export default function EventGeneratorPlugin(options) {
return function install(openmct) {
@ -38,5 +39,20 @@ export default function EventGeneratorPlugin(options) {
});
openmct.telemetry.addProvider(new EventTelemetryProvider());
openmct.telemetry.addProvider(new EventMetadataProvider());
openmct.types.addType('eventGeneratorWithAcknowledge', {
name: 'Event Message Generator with Acknowledge',
description:
'For development use. Creates sample event message data stream and updates the event row with an acknowledgement.',
cssClass: 'icon-generator-events',
creatable: true,
initialize: function (object) {
object.telemetry = {
duration: 2.5
};
}
});
openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider());
};
}

View File

@ -108,6 +108,16 @@ const METADATA_BY_TYPE = {
string: 'ON'
}
],
filters: [
{
singleSelectionThreshold: true,
comparator: 'equals',
possibleValues: [
{ label: 'OFF', value: 0 },
{ label: 'ON', value: 1 }
]
}
],
hints: {
range: 1
}

View File

@ -34,14 +34,16 @@ StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'example.state-generator';
};
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback, options) {
var duration = domainObject.telemetry.duration * 1000;
var interval = setInterval(function () {
var interval = setInterval(() => {
var now = Date.now();
var datum = pointForTimestamp(now, duration, domainObject.name);
datum.value = String(datum.value);
callback(datum);
if (!this.shouldBeFiltered(datum, options)) {
datum.value = String(datum.value);
callback(datum);
}
}, duration);
return function () {
@ -63,9 +65,25 @@ StateGeneratorProvider.prototype.request = function (domainObject, options) {
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, duration, domainObject.name));
const point = pointForTimestamp(start, duration, domainObject.name);
if (!this.shouldBeFiltered(point, options)) {
data.push(point);
}
start += duration;
}
return Promise.resolve(data);
};
StateGeneratorProvider.prototype.shouldBeFiltered = function (point, options) {
const valueToFilter = options?.filters?.state?.equals?.[0];
if (!valueToFilter) {
return false;
}
const { value } = point;
return value !== Number(valueToFilter);
};

1284
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,25 +16,25 @@
],
"devDependencies": {
"@babel/eslint-parser": "7.23.3",
"@braintree/sanitize-url": "6.0.4",
"@braintree/sanitize-url": "7.1.1",
"@types/d3-axis": "3.0.6",
"@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10",
"@types/d3-shape": "3.0.0",
"@types/d3-shape": "3.1.7",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "5.1.2",
"@types/lodash": "4.17.0",
"@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
"babel-plugin-istanbul": "7.0.0",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "12.0.2",
"copy-webpack-plugin": "13.0.0",
"cspell": "7.3.8",
"css-loader": "6.10.0",
"d3-axis": "3.0.0",
"d3-scale": "4.0.2",
"d3-selection": "3.0.0",
"d3-shape": "3.0.0",
"d3-shape": "3.2.0",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-compat": "4.2.0",
@ -51,7 +51,7 @@
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "5.0.0",
"jasmine-core": "5.1.1",
"jasmine-core": "5.6.0",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
"karma-cli": "2.0.0",
@ -64,14 +64,14 @@
"karma-webpack": "5.0.1",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"marked": "12.0.0",
"mini-css-extract-plugin": "2.7.6",
"marked": "15.0.7",
"mini-css-extract-plugin": "2.9.2",
"moment": "2.30.1",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41",
"nano": "10.1.4",
"npm-run-all2": "6.1.2",
"nyc": "15.1.0",
"npm-run-all2": "7.0.2",
"nyc": "17.1.0",
"painterro": "1.2.87",
"plotly.js-basic-dist-min": "2.29.1",
"plotly.js-gl2d-dist-min": "2.20.0",
@ -79,21 +79,21 @@
"prettier-eslint": "16.3.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.12.1",
"sanitize-html": "2.15.0",
"sass": "1.71.1",
"sass-loader": "14.1.1",
"style-loader": "3.3.3",
"style-loader": "4.0.0",
"terser-webpack-plugin": "5.3.9",
"tiny-emitter": "2.1.0",
"typescript": "5.3.3",
"uuid": "9.0.1",
"uuid": "11.1.0",
"vue": "3.4.24",
"vue-eslint-parser": "9.4.2",
"vue-loader": "16.8.3",
"webpack": "5.90.3",
"webpack": "5.98.0",
"webpack-cli": "5.1.1",
"webpack-dev-server": "5.0.2",
"webpack-merge": "5.10.0"
"webpack-merge": "6.0.1"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./e2e/test-results ./.nyc_output ./e2e/.nyc_output",

View File

@ -582,4 +582,15 @@ export default class AnnotationAPI extends EventEmitter {
_.isEqual(targets, otherTargets)
);
}
/**
* Checks if the given type is annotatable
* @param {string} type The type to check
* @returns {boolean} Returns true if the type is annotatable
*/
isAnnotatableType(type) {
const types = this.openmct.types.getAllTypes();
return types[type]?.definition?.annotatable;
}
}

View File

@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { isIdentifier } from '../objects/object-utils';
/**
* @typedef {import('openmct').DomainObject} DomainObject
*/
@ -209,9 +211,15 @@ export default class CompositionCollection {
this.#cleanUpMutables();
const children = await this.#provider.load(this.domainObject);
const childObjects = await Promise.all(
children.map((c) => this.#publicAPI.objects.get(c, abortSignal))
children.map((child) => {
if (isIdentifier(child)) {
return this.#publicAPI.objects.get(child, abortSignal);
} else {
return Promise.resolve(child);
}
})
);
childObjects.forEach((c) => this.add(c, true));
childObjects.forEach((child) => this.add(child, true));
this.#emit('load');
return childObjects;

View File

@ -96,8 +96,9 @@ export default class CompositionProvider {
* object.
* @param {DomainObject} domainObject the domain object
* for which to load composition
* @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition
* @returns {Promise<Identifier[] | DomainObject[]>} a promise for
* the Identifiers or Domain Objects in this composition. If Identifiers are returned,
* they will be automatically resolved to domain objects by the API.
*/
load(domainObject) {
throw new Error('This method must be implemented by a subclass.');

View File

@ -21,7 +21,7 @@
*****************************************************************************/
import { toRaw } from 'vue';
import { makeKeyString } from '../objects/object-utils.js';
import { makeKeyString, parseKeyString } from '../objects/object-utils.js';
import CompositionProvider from './CompositionProvider.js';
/**
@ -75,7 +75,11 @@ export default class DefaultCompositionProvider extends CompositionProvider {
* the Identifiers in this composition
*/
load(domainObject) {
return Promise.all(domainObject.composition);
const identifiers = domainObject.composition
.filter((idOrKeystring) => idOrKeystring !== null && idOrKeystring !== undefined)
.map((idOrKeystring) => parseKeyString(idOrKeystring));
return Promise.all(identifiers);
}
/**
* Attach listeners for changes to the composition of a given domain object.

View File

@ -27,6 +27,7 @@ import ConflictError from './ConflictError.js';
import InMemorySearchProvider from './InMemorySearchProvider.js';
import InterceptorRegistry from './InterceptorRegistry.js';
import MutableDomainObject from './MutableDomainObject.js';
import { isIdentifier, isKeyString } from './object-utils.js';
import RootObjectProvider from './RootObjectProvider.js';
import RootRegistry from './RootRegistry.js';
import Transaction from './Transaction.js';
@ -742,11 +743,19 @@ export default class ObjectAPI {
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
* @returns {Promise<Array<DomainObject>>} a promise containing an array of domain objects
*/
async getOriginalPath(identifier, path = [], abortSignal = null) {
const domainObject = await this.get(identifier, abortSignal);
async getOriginalPath(identifierOrObject, path = [], abortSignal = null) {
let domainObject;
if (isKeyString(identifierOrObject) || isIdentifier(identifierOrObject)) {
domainObject = await this.get(identifierOrObject, abortSignal);
} else {
domainObject = identifierOrObject;
}
if (!domainObject) {
return [];
}
path.push(domainObject);
const { location } = domainObject;
if (location && !this.#pathContainsDomainObject(location, path)) {

View File

@ -21,8 +21,11 @@
*****************************************************************************/
const PRIORITIES = Object.freeze({
HIGHEST: Infinity,
HIGH: 1000,
DEFAULT: 0,
LOW: -1000
LOW: -1000,
LOWEST: -Infinity
});
export default PRIORITIES;

View File

@ -250,6 +250,117 @@ export default class TelemetryAPI {
return options;
}
/**
* Sanitizes objects for consistent serialization by:
* 1. Removing non-plain objects (class instances) and functions
* 2. Sorting object keys alphabetically to ensure consistent ordering
*/
sanitizeForSerialization(key, value) {
// Handle null and primitives directly
if (value === null || typeof value !== 'object') {
return value;
}
// Remove functions and non-plain objects (except arrays)
if (
typeof value === 'function' ||
(Object.getPrototypeOf(value) !== Object.prototype && !Array.isArray(value))
) {
return undefined;
}
// For plain objects, just sort the keys
if (!Array.isArray(value)) {
const sortedObject = {};
const sortedKeys = Object.keys(value).sort();
sortedKeys.forEach((objectKey) => {
sortedObject[objectKey] = value[objectKey];
});
return sortedObject;
}
return value;
}
/**
* Determines whether a domain object has numeric telemetry data.
* A domain object has numeric telemetry if it:
* 1. Has a telemetry property
* 2. Has telemetry metadata with domain values (like timestamps)
* 3. Has range values (measurements) where at least one is numeric
*
* @method hasNumericTelemetry
* @param {import('openmct').DomainObject} domainObject The domain object to check
* @returns {boolean} True if the object has numeric telemetry, false otherwise
*/
hasNumericTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
return false;
}
const metadata = this.openmct.telemetry.getMetadata(domainObject);
const rangeValues = metadata.valuesForHints(['range']);
const domains = metadata.valuesForHints(['domain']);
return (
domains.length > 0 &&
rangeValues.length > 0 &&
!rangeValues.every((value) => value.format === 'string')
);
}
/**
* Generates a numeric hash value for an options object. The hash is consistent
* for equivalent option objects regardless of property order.
*
* This is used to create compact, unique cache keys for telemetry subscriptions with
* different options configurations. The hash function ensures that identical options
* objects will always generate the same hash value, while different options objects
* (even with small differences) will generate different hash values.
*
* @private
* @param {Object} options The options object to hash
* @returns {number} A positive integer hash of the options object
*/
#hashOptions(options) {
const sanitizedOptionsString = JSON.stringify(
options,
this.sanitizeForSerialization.bind(this)
);
let hash = 0;
const prime = 31;
const modulus = 1e9 + 9; // Large prime number
for (let i = 0; i < sanitizedOptionsString.length; i++) {
const char = sanitizedOptionsString.charCodeAt(i);
// Calculate new hash value while keeping numbers manageable
hash = Math.floor((hash * prime + char) % modulus);
}
return Math.abs(hash);
}
/**
* Generates a unique cache key for a telemetry subscription based on the
* domain object identifier and options (which includes strategy).
*
* Uses a hash of the options object to create compact cache keys while still
* ensuring unique keys for different subscription configurations.
*
* @private
* @param {import('openmct').DomainObject} domainObject The domain object being subscribed to
* @param {Object} options The subscription options object (including strategy)
* @returns {string} A unique key string for caching the subscription
*/
#getSubscriptionCacheKey(domainObject, options) {
const keyString = makeKeyString(domainObject.identifier);
return `${keyString}:${this.#hashOptions(options)}`;
}
/**
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
* The request will be modified when it is received and will be returned in it's modified state
@ -418,16 +529,14 @@ export default class TelemetryAPI {
this.#subscribeCache = {};
}
const keyString = makeKeyString(domainObject.identifier);
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
// Override the requested strategy with the strategy supported by the provider
const optionsWithSupportedStrategy = {
...options,
strategy: supportedStrategy
};
// If batching is supported, we need to cache a subscription for each strategy -
// latest and batched.
const cacheKey = `${keyString}:${supportedStrategy}`;
const cacheKey = this.#getSubscriptionCacheKey(domainObject, optionsWithSupportedStrategy);
let subscriber = this.#subscribeCache[cacheKey];
if (!subscriber) {

View File

@ -359,6 +359,18 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @returns {boolean}
* @override
*/
isFixed() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.isFixed(...arguments);
} else {
return super.isFixed(...arguments);
}
}
/**
* @returns {number}
* @override
@ -400,7 +412,7 @@ class IndependentTimeContext extends TimeContext {
}
/**
* Reset the time context to the global time context
* Reset the time context from the global time context
*/
resetContext() {
if (this.upstreamTimeContext) {
@ -428,6 +440,10 @@ class IndependentTimeContext extends TimeContext {
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.getBounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
// Also emit the mode in case it's different from previous time context
if (this.getMode()) {
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
}
}
/**
@ -502,6 +518,10 @@ class IndependentTimeContext extends TimeContext {
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.getBounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
// Also emit the mode in case it's different from the global time context
if (this.getMode()) {
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
}
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
this.globalTimeContext.emit('refreshContext', viewKey);
}

View File

@ -23,6 +23,7 @@
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
import { TIME_CONTEXT_EVENTS } from './constants';
import GlobalTimeContext from './GlobalTimeContext.js';
/**
@ -142,7 +143,7 @@ class TimeAPI extends GlobalTimeContext {
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
//stop following upstream time context since the view has it's own
//stop following upstream time context since the view has its own
timeContext.resetContext();
if (clockKey) {
@ -152,6 +153,9 @@ class TimeAPI extends GlobalTimeContext {
timeContext.setMode(FIXED_MODE_KEY, value);
}
// Also emit the mode in case it's different from the previous time context
timeContext.emit(TIME_CONTEXT_EVENTS.modeChanged, structuredClone(timeContext.getMode()));
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
this.emit('refreshContext', key);

View File

@ -89,6 +89,17 @@ export default class TypeRegistry {
get(typeKey) {
return this.types[typeKey] || UNKNOWN_TYPE;
}
/**
* List all registered types.
* @returns {Type[]} all registered types
*/
getAllTypes() {
return this.types;
}
/**
* Import legacy types.
* @param {TypeDefinition[]} types the types to import
*/
importLegacyTypes(types) {
types
.filter((t) => this.get(t.key) === UNKNOWN_TYPE)

View File

@ -25,10 +25,14 @@
* Originally created by hudsonfoo on 09/02/16
*/
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
function sanitizeFilename(filename) {
const replacedPeriods = filename.replace(/\./g, '_');
const safeFilename = replacedPeriods.replace(/[^a-zA-Z0-9_\-.\s]/g, '');
return filename.replace(regex, '_');
// Handle leading/trailing spaces and periods
const trimmedFilename = safeFilename.trim().replace(/^\.+|\.+$/g, '');
return trimmedFilename;
}
import { saveAs } from 'file-saver';
@ -150,7 +154,7 @@ class ImageExporter {
* @returns {promise}
*/
async exportJPG(element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
const processedFilename = sanitizeFilename(filename);
const img = await this.renderElement(element, {
imageType: 'jpg',
@ -167,7 +171,7 @@ class ImageExporter {
* @returns {promise}
*/
async exportPNG(element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
const processedFilename = sanitizeFilename(filename);
const img = await this.renderElement(element, {
imageType: 'png',

View File

@ -24,6 +24,9 @@ export default function (folderName, couchPlugin, searchFilter) {
location: 'ROOT'
});
}
},
search() {
return Promise.resolve([]);
}
});
@ -35,9 +38,17 @@ export default function (folderName, couchPlugin, searchFilter) {
);
},
load() {
return couchProvider.getObjectsByFilter(searchFilter).then((objects) => {
return objects.map((object) => object.identifier);
});
let searchResults;
if (searchFilter.viewName !== undefined) {
// Use a view to search, instead of an _all_docs find
searchResults = couchProvider.getObjectsByView(searchFilter);
} else {
// Use the _find endpoint to search _all_docs
searchResults = couchProvider.getObjectsByFilter(searchFilter);
}
return searchResults;
}
});
};

View File

@ -41,9 +41,10 @@ export default class LADTableConfiguration extends EventEmitter {
}
getConfiguration() {
const configuration = this.domainObject.configuration || {};
configuration.hiddenColumns = configuration.hiddenColumns || {};
const configuration = this.domainObject.configuration ?? {};
configuration.hiddenColumns = configuration.hiddenColumns ?? {};
configuration.isFixedLayout = configuration.isFixedLayout ?? true;
configuration.objectStyles = configuration.objectStyles ?? {};
return configuration;
}

View File

@ -27,7 +27,7 @@ import LadTableConfiguration from './components/LadTableConfiguration.vue';
export default function LADTableConfigurationViewProvider(openmct) {
return {
key: 'lad-table-configuration',
name: 'LAD Table Configuration',
name: 'Config',
canView(selection) {
if (selection.length !== 1 || selection[0].length === 0) {
return false;
@ -61,7 +61,7 @@ export default function LADTableConfigurationViewProvider(openmct) {
_destroy = destroy;
},
priority() {
return 1;
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
},
destroy() {
if (_destroy) {

View File

@ -22,28 +22,24 @@
<template>
<div class="c-inspect-properties">
<template v-if="isEditing">
<div class="c-inspect-properties__header">Table Column Visibility</div>
<ul class="c-inspect-properties__section">
<li v-for="(title, key) in headers" :key="key" class="c-inspect-properties__row">
<div class="c-inspect-properties__label" title="Show or hide column">
<label :for="key + 'ColumnControl'">{{ title }}</label>
</div>
<div class="c-inspect-properties__value">
<input
:id="key + 'ColumnControl'"
type="checkbox"
:checked="configuration.hiddenColumns[key] !== true"
@change="toggleColumn(key)"
/>
</div>
</li>
</ul>
</template>
<template v-else>
<div class="c-inspect-properties__header">LAD Table Configuration</div>
<div class="c-inspect-properties__row--span-all">Only available in edit mode.</div>
</template>
<div class="c-inspect-properties__header">Table Column Visibility</div>
<ul class="c-inspect-properties__section">
<li v-for="(title, key) in headers" :key="key" class="c-inspect-properties__row">
<div class="c-inspect-properties__label" title="Show or hide column">
<label :for="key + 'ColumnControl'">{{ title }}</label>
</div>
<div class="c-inspect-properties__value">
<input
v-if="isEditing"
:id="key + 'ColumnControl'"
type="checkbox"
:checked="configuration.hiddenColumns[key] !== true"
@change="toggleColumn(key)"
/>
<span v-if="!isEditing && configuration.hiddenColumns[key] !== true">Visible</span>
</div>
</li>
</ul>
</div>
</template>
@ -62,7 +58,8 @@ export default {
isEditing: this.openmct.editor.isEditing(),
configuration: ladTableConfiguration.getConfiguration(),
items: [],
ladTableObjects: []
ladTableObjects: [],
ladTelemetryObjects: {}
};
},
computed: {
@ -150,11 +147,14 @@ export default {
this.ladTableObjects.push(ladTable);
const composition = this.openmct.composition.get(ladTable.domainObject);
composition.on('add', this.addItem);
composition.on('remove', this.removeItem);
composition.load();
this.compositions.push({
composition
composition,
addCallback: this.addItem,
removeCallback: this.removeItem
});
},
removeLadTable(identifier) {

View File

@ -39,6 +39,9 @@ export default function plugin() {
cssClass: 'icon-tabular-lad',
initialize(domainObject) {
domainObject.composition = [];
domainObject.configuration = {
objectStyles: {}
};
}
});

View File

@ -41,7 +41,7 @@ export default function BarGraphInspectorViewProvider(openmct) {
_destroy = destroy;
},
priority: function () {
return openmct.priority.HIGH + 1;
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
},
destroy: function () {
if (_destroy) {

View File

@ -40,7 +40,7 @@ export default function ScatterPlotInspectorViewProvider(openmct) {
_destroy = destroy;
},
priority: function () {
return openmct.priority.HIGH + 1;
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
},
destroy: function () {
if (_destroy) {

View File

@ -44,48 +44,57 @@ import { getLatestTimestamp } from './utils/time.js';
* }
*/
export default class Condition extends EventEmitter {
#definition;
/**
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
* @constructor
* @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
* @param definition: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
* @param openmct
* @param conditionManager
*/
constructor(conditionConfiguration, openmct, conditionManager) {
constructor(definition, openmct, conditionManager) {
super();
this.openmct = openmct;
this.conditionManager = conditionManager;
this.id = conditionConfiguration.id;
this.criteria = [];
this.result = undefined;
this.timeSystems = this.openmct.time.getAllTimeSystems();
if (conditionConfiguration.configuration.criteria) {
this.createCriteria(conditionConfiguration.configuration.criteria);
this.#definition = definition;
if (definition.configuration.criteria) {
this.createCriteria(definition.configuration.criteria);
}
this.trigger = conditionConfiguration.configuration.trigger;
this.trigger = definition.configuration.trigger;
this.summary = '';
this.handleCriterionUpdated = this.handleCriterionUpdated.bind(this);
this.handleOldTelemetryCriterion = this.handleOldTelemetryCriterion.bind(this);
this.handleTelemetryStaleness = this.handleTelemetryStaleness.bind(this);
}
get id() {
return this.#definition.id;
}
get configuration() {
return this.#definition.configuration;
}
updateResult(datum) {
if (!datum || !datum.id) {
updateResult(latestDataTable, telemetryIdThatChanged) {
if (!latestDataTable) {
console.log('no data received');
return;
}
// if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate
if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) {
if (this.hasNoTelemetry() || this.isTelemetryUsed(telemetryIdThatChanged)) {
const currentTimeSystemKey = this.openmct.time.getTimeSystem().key;
this.criteria.forEach((criterion) => {
if (this.isAnyOrAllTelemetry(criterion)) {
criterion.updateResult(datum, this.conditionManager.telemetryObjects);
criterion.updateResult(latestDataTable, this.conditionManager.telemetryObjects);
} else {
if (criterion.usesTelemetry(datum.id)) {
criterion.updateResult(datum);
const relevantDatum = latestDataTable.get(criterion.telemetryObjectIdAsString);
if (criterion.shouldUpdateResult(relevantDatum, currentTimeSystemKey)) {
criterion.updateResult(relevantDatum, currentTimeSystemKey);
}
}
});
@ -102,9 +111,11 @@ export default class Condition extends EventEmitter {
}
hasNoTelemetry() {
return this.criteria.every((criterion) => {
return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === '';
const usesSomeTelemetry = this.criteria.some((criterion) => {
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetry !== '';
});
return !usesSomeTelemetry;
}
isTelemetryUsed(id) {
@ -182,7 +193,7 @@ export default class Condition extends EventEmitter {
findCriterion(id) {
let criterion;
for (let i = 0, ii = this.criteria.length; i < ii; i++) {
for (let i = 0; i < this.criteria.length; i++) {
if (this.criteria[i].id === id) {
criterion = {
item: this.criteria[i],
@ -247,7 +258,7 @@ export default class Condition extends EventEmitter {
this.timeSystems,
this.openmct.time.getTimeSystem()
);
this.conditionManager.updateCurrentCondition(latestTimestamp);
this.conditionManager.updateCurrentCondition(latestTimestamp, this);
}
handleTelemetryStaleness() {

View File

@ -27,6 +27,12 @@ import Condition from './Condition.js';
import { getLatestTimestamp } from './utils/time.js';
export default class ConditionManager extends EventEmitter {
#latestDataTable = new Map();
/**
* @param {import('openmct.js').DomainObject} conditionSetDomainObject
* @param {import('openmct.js').OpenMCT} openmct
*/
constructor(conditionSetDomainObject, openmct) {
super();
this.openmct = openmct;
@ -304,22 +310,6 @@ export default class ConditionManager extends EventEmitter {
this.persistConditions();
}
getCurrentCondition() {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length - 1];
for (let i = 0; i < conditionCollection.length - 1; i++) {
const condition = this.findConditionById(conditionCollection[i].id);
if (condition.result) {
//first condition to be true wins
currentCondition = conditionCollection[i];
break;
}
}
return currentCondition;
}
getCurrentConditionLAD(conditionResults) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length - 1];
@ -410,26 +400,34 @@ export default class ConditionManager extends EventEmitter {
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.getTimeSystem().key;
let timestamp = {};
const currentTimestamp = normalizedDatum[timeSystemKey];
const timestamp = {};
timestamp[timeSystemKey] = currentTimestamp;
this.#latestDataTable.set(normalizedDatum.id, normalizedDatum);
if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {
this.updateConditionResults(normalizedDatum);
this.updateCurrentCondition(timestamp);
const matchingCondition = this.updateConditionResults(normalizedDatum.id);
this.updateCurrentCondition(timestamp, matchingCondition);
}
}
updateConditionResults(normalizedDatum) {
updateConditionResults(keyStringForUpdatedTelemetryObject) {
//We want to stop when the first condition evaluates to true.
this.conditions.some((condition) => {
condition.updateResult(normalizedDatum);
const matchingCondition = this.conditions.find((condition) => {
condition.updateResult(this.#latestDataTable, keyStringForUpdatedTelemetryObject);
return condition.result === true;
});
return matchingCondition;
}
updateCurrentCondition(timestamp) {
const currentCondition = this.getCurrentCondition();
updateCurrentCondition(timestamp, matchingCondition) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
const defaultCondition = conditionCollection[conditionCollection.length - 1];
const currentCondition = matchingCondition || defaultCondition;
this.emit(
'conditionSetResultUpdated',
@ -444,11 +442,13 @@ export default class ConditionManager extends EventEmitter {
);
}
getTestData(metadatum) {
getTestData(metadatum, identifier) {
let data = undefined;
if (this.testData.applied) {
const found = this.testData.conditionTestInputs.find(
(testInput) => testInput.metadata === metadatum.source
(testInput) =>
testInput.metadata === metadatum.source &&
this.openmct.objects.areIdsEqual(testInput.telemetry, identifier)
);
if (found) {
data = found.value;
@ -463,7 +463,7 @@ export default class ConditionManager extends EventEmitter {
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
const testValue = this.getTestData(metadatum);
const testValue = this.getTestData(metadatum, endpoint.identifier);
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
datum[metadatum.key] =
testValue !== undefined
@ -480,7 +480,7 @@ export default class ConditionManager extends EventEmitter {
updateTestData(testData) {
if (!_.isEqual(testData, this.testData)) {
this.testData = testData;
this.testData = JSON.parse(JSON.stringify(testData));
this.openmct.objects.mutate(
this.conditionSetDomainObject,
'configuration.conditionTestData',

View File

@ -53,6 +53,7 @@ describe('The condition', function () {
valueMetadatas: [
{
key: 'some-key',
source: 'some-key',
name: 'Some attribute',
hints: {
range: 2
@ -60,6 +61,7 @@ describe('The condition', function () {
},
{
key: 'utc',
source: 'utc',
name: 'Time',
format: 'utc',
hints: {
@ -88,17 +90,32 @@ describe('The condition', function () {
openmct.telemetry = jasmine.createSpyObj('telemetry', [
'isTelemetryObject',
'subscribe',
'getMetadata'
'getMetadata',
'getValueFormatter'
]);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
openmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
return {
parse(input) {
return input;
}
};
});
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems', 'on', 'off']);
openmct.time = jasmine.createSpyObj('time', [
'getTimeSystem',
'getAllTimeSystems',
'on',
'off'
]);
openmct.time.getTimeSystem.and.returnValue({ key: 'utc' });
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
//openmct.time.getTimeSystem.and.returnValue();
openmct.time.on.and.returnValue(() => {});
openmct.time.off.and.returnValue(() => {});
@ -113,7 +130,7 @@ describe('The condition', function () {
id: '1234-5678-9999-0000',
operation: 'equalTo',
input: ['0'],
metadata: 'value',
metadata: 'testSource',
telemetry: testTelemetryObject.identifier
}
]
@ -156,37 +173,24 @@ describe('The condition', function () {
expect(conditionObj.criteria.length).toEqual(0);
});
it('gets the result of a condition when new telemetry data is received', function () {
conditionObj.updateResult({
value: '0',
utc: 'Hi',
id: testTelemetryObject.identifier.key
});
expect(conditionObj.result).toBeTrue();
});
it('gets the result of a condition when new telemetry data is received', function () {
conditionObj.updateResult({
value: '1',
utc: 'Hi',
id: testTelemetryObject.identifier.key
});
expect(conditionObj.result).toBeFalse();
});
it('keeps the old result new telemetry data is not used by it', function () {
conditionObj.updateResult({
const latestDataTable = new Map();
latestDataTable.set(testTelemetryObject.identifier.key, {
value: '0',
utc: 'Hi',
id: testTelemetryObject.identifier.key
});
conditionObj.updateResult(latestDataTable, testTelemetryObject.identifier.key);
expect(conditionObj.result).toBeTrue();
conditionObj.updateResult({
latestDataTable.set('1234', {
value: '1',
utc: 'Hi',
id: '1234'
});
conditionObj.updateResult(latestDataTable, '1234');
expect(conditionObj.result).toBeTrue();
});
});

View File

@ -24,7 +24,7 @@
<div
class="c-condition-h"
:class="{ 'is-drag-target': draggingOver }"
aria-label="Condition Set Condition"
:aria-label="conditionSetLabel"
@dragover.prevent
@drop.prevent="dropCondition($event, conditionIndex)"
@dragenter="dragEnter($event, conditionIndex)"
@ -53,7 +53,9 @@
@click="expanded = !expanded"
></span>
<span class="c-condition__name">{{ condition.configuration.name }}</span>
<span class="c-condition__name" aria-label="Condition Name Label">{{
condition.configuration.name
}}</span>
<span class="c-condition__summary">
<template v-if="!condition.isDefault && !canEvaluateCriteria"> Define criteria </template>
<span v-else>
@ -160,8 +162,10 @@
</div>
</template>
<div class="c-cdef__separator c-row-separator"></div>
<div class="c-cdef__controls" :disabled="!telemetry.length">
<div class="c-cdef__controls">
<button
:disabled="!telemetry.length"
:aria-label="`Add Criteria - ${!telemetry.length ? 'Disabled' : 'Enabled'}`"
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
@click="addCriteria"
>
@ -257,6 +261,17 @@ export default {
};
},
computed: {
conditionSetLabel() {
let label;
if (this.condition.id === this.currentConditionId) {
label = 'Active Condition Set Condition';
} else {
label = 'Condition Set Condition';
}
return label;
},
triggers() {
const keys = Object.keys(TRIGGER);
const triggerOptions = [];

View File

@ -114,7 +114,7 @@
class="c-button c-button--major icon-plus labeled"
@click="addTestInput"
>
<span class="c-cs-button__label">Add Test Datum</span>
<span class="c-cs-button__label" aria-label="Add Test Datum">Add Test Datum</span>
</button>
</div>
</section>

View File

@ -181,13 +181,20 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
if (validatedData && !this.isStalenessCheck()) {
if (this.isOldCheck()) {
if (this.ageCheck?.[validatedData.id]) {
this.ageCheck[validatedData.id].update(validatedData);
}
Object.keys(this.telemetryDataCache).forEach((objectIdKeystring) => {
if (this.ageCheck?.[objectIdKeystring]) {
this.ageCheck[objectIdKeystring].update(validatedData[objectIdKeystring]);
}
this.telemetryDataCache[validatedData.id] = false;
this.telemetryDataCache[objectIdKeystring] = false;
});
} else {
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
Object.keys(this.telemetryDataCache).forEach((objectIdKeystring) => {
const telemetryObject = telemetryObjects[objectIdKeystring];
this.telemetryDataCache[objectIdKeystring] = this.computeResult(
this.createNormalizedDatum(validatedData[objectIdKeystring], telemetryObject)
);
});
}
}

View File

@ -29,21 +29,29 @@ import { getOperatorText, OPERATIONS } from '../utils/operations.js';
import { checkIfOld } from '../utils/time.js';
export default class TelemetryCriterion extends EventEmitter {
#lastUpdated;
#lastTimeSystem;
#comparator;
/**
* Subscribes/Unsubscribes to telemetry and emits the result
* of operations performed on the telemetry data returned and a given input value.
* @constructor
* @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }
* @param openmct
* @param {import('../../../MCT.js').OpenMCT} openmct
*/
constructor(telemetryDomainObjectDefinition, openmct) {
super();
/**
* @type {import('../../../MCT.js').MCT}
*/
this.openmct = openmct;
this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
this.id = telemetryDomainObjectDefinition.id;
this.telemetry = telemetryDomainObjectDefinition.telemetry;
this.operation = telemetryDomainObjectDefinition.operation;
this.#comparator = this.#findOperation(this.operation);
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.result = undefined;
@ -83,7 +91,6 @@ export default class TelemetryCriterion extends EventEmitter {
if (this.ageCheck) {
this.ageCheck.clear();
}
this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000);
}
@ -153,7 +160,6 @@ export default class TelemetryCriterion extends EventEmitter {
createNormalizedDatum(telemetryDatum, endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
datum[metadatum.key] = formatter.parse(telemetryDatum[metadatum.source]);
@ -179,9 +185,18 @@ export default class TelemetryCriterion extends EventEmitter {
return datum;
}
shouldUpdateResult(datum, timesystem) {
const dataIsDefined = datum !== undefined;
const hasTimeSystemChanged =
this.#lastTimeSystem === undefined || this.#lastTimeSystem !== timesystem;
const isCacheStale = this.#lastUpdated === undefined || datum[timesystem] > this.#lastUpdated;
updateResult(data) {
const validatedData = this.isValid() ? data : {};
return dataIsDefined && (hasTimeSystemChanged || isCacheStale);
}
updateResult(data, currentTimeSystemKey) {
const validatedData = this.isValid()
? this.createNormalizedDatum(data, this.telemetryObject)
: {};
if (!this.isStalenessCheck()) {
if (this.isOldCheck()) {
@ -193,6 +208,8 @@ export default class TelemetryCriterion extends EventEmitter {
} else {
this.result = this.computeResult(validatedData);
}
this.#lastUpdated = data[currentTimeSystemKey];
this.#lastTimeSystem = currentTimeSystemKey;
}
}
@ -236,8 +253,8 @@ export default class TelemetryCriterion extends EventEmitter {
});
}
findOperation(operation) {
for (let i = 0, ii = OPERATIONS.length; i < ii; i++) {
#findOperation(operation) {
for (let i = 0; i < OPERATIONS.length; i++) {
if (operation === OPERATIONS[i].name) {
return OPERATIONS[i].operation;
}
@ -249,15 +266,14 @@ export default class TelemetryCriterion extends EventEmitter {
computeResult(data) {
let result = false;
if (data) {
let comparator = this.findOperation(this.operation);
let params = [];
params.push(data[this.metadata]);
if (this.isValidInput()) {
this.input.forEach((input) => params.push(input));
}
if (typeof comparator === 'function') {
result = Boolean(comparator(params));
if (typeof this.#comparator === 'function') {
result = Boolean(this.#comparator(params));
}
}

View File

@ -106,7 +106,7 @@ describe('The telemetry criterion', function () {
id: 'test-criterion-id',
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
operation: 'textContains',
metadata: 'value',
metadata: 'testSource',
input: ['Hell'],
telemetryObjects: { [testTelemetryObject.identifier.key]: testTelemetryObject }
};

View File

@ -0,0 +1,38 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default function conditionWidgetStylesInterceptor(openmct) {
return {
appliesTo: (identifier, domainObject) => {
return domainObject?.type === 'conditionWidget' && !domainObject.configuration?.objectStyles;
},
invoke: (identifier, domainObject) => {
if (!domainObject.configuration) {
domainObject.configuration = {};
}
domainObject.configuration.objectStyles = {};
return domainObject;
}
};
}

View File

@ -20,11 +20,13 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import conditionWidgetStylesInterceptor from './conditionWidgetStylesInterceptor.js';
import ConditionWidgetViewProvider from './ConditionWidgetViewProvider.js';
export default function plugin() {
return function install(openmct) {
openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct));
openmct.objects.addGetInterceptor(conditionWidgetStylesInterceptor(openmct));
openmct.types.addType('conditionWidget', {
key: 'conditionWidget',
@ -34,7 +36,9 @@ export default function plugin() {
creatable: true,
cssClass: 'icon-condition-widget',
initialize(domainObject) {
domainObject.configuration = {};
domainObject.configuration = {
objectStyles: {}
};
domainObject.label = 'Condition Widget';
domainObject.conditionalLabel = '';
domainObject.url = '';

View File

@ -65,7 +65,9 @@ class AlphanumericFormatView {
}
priority() {
return 1;
return this.openmct.editor.isEditing()
? this.openmct.priority.DEFAULT
: this.openmct.priority.LOW;
}
destroy() {

View File

@ -31,7 +31,8 @@ export default function DisplayLayoutType() {
domainObject.composition = [];
domainObject.configuration = {
items: [],
layoutGrid: [10, 10]
layoutGrid: [10, 10],
objectStyles: {}
};
},
form: [

View File

@ -32,13 +32,16 @@
</div>
<div class="c-inspect-properties__value">
<input
v-if="isEditing"
id="telemetryPrintfFormat"
type="text"
:disabled="!isEditing"
:value="telemetryFormat"
:placeholder="nonMixedFormat ? '' : 'Mixed'"
@change="formatTelemetry"
/>
<template v-if="!isEditing && telemetryFormat?.length">
{{ telemetryFormat }}
</template>
</div>
</li>
</ul>

View File

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

View File

@ -25,6 +25,7 @@ import mount from 'utils/mount';
import CopyToClipboardAction from './actions/CopyToClipboardAction.js';
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
import DisplayLayout from './components/DisplayLayout.vue';
import displayLayoutStylesInterceptor from './displayLayoutStylesInterceptor.js';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutDrawingObjectTypes from './DrawingObjectTypes.js';
@ -123,6 +124,7 @@ export default function DisplayLayoutPlugin(options) {
return 100;
}
});
openmct.objects.addGetInterceptor(displayLayoutStylesInterceptor(openmct));
openmct.types.addType('layout', DisplayLayoutType());
openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct));
openmct.inspectorViews.addProvider(new AlphaNumericFormatViewProvider(openmct, options));

View File

@ -63,7 +63,7 @@ export default function FaultManagementInspectorViewProvider(openmct) {
_destroy = destroy;
},
priority: function () {
return openmct.priority.HIGH + 1;
return openmct.priority.HIGH;
},
destroy: function () {
if (_destroy) {

View File

@ -43,8 +43,6 @@ export default class FiltersInspectorViewProvider {
let openmct = this.openmct;
let _destroy = null;
const domainObject = selection?.[0]?.[0]?.context?.item;
return {
show: function (element) {
const { destroy } = mount(
@ -69,13 +67,6 @@ export default class FiltersInspectorViewProvider {
if (isEditing) {
return true;
}
const metadata = openmct.telemetry.getMetadata(domainObject);
const metadataWithFilters = metadata
? metadata.valueMetadatas.filter((value) => value.filters)
: [];
return metadataWithFilters.length;
},
priority: function () {
return openmct.priority.DEFAULT;

View File

@ -27,9 +27,9 @@ To define a filter, you'll need to add a new `filter` property to the domain obj
singleSelectionThreshold: true,
comparator: 'equals',
possibleValues: [
{ name: 'Apple', value: 'apple' },
{ name: 'Banana', value: 'banana' },
{ name: 'Orange', value: 'orange' }
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Orange', value: 'orange' }
]
}]
}

View File

@ -38,6 +38,9 @@
@update-filters="persistFilters"
/>
</ul>
<span v-else>
This view doesn't include any parameters that have configured filter criteria.
</span>
</template>
<script>

View File

@ -158,6 +158,7 @@ export default {
this.composition.on('remove', this.removeChildObject);
this.composition.on('add', this.addFrame);
this.composition.load();
this.unObserveContainers = this.openmct.objects.observe(
this.domainObject,
'configuration.containers',

View File

@ -0,0 +1,40 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default function flexibleLayoutStylesInterceptor(openmct) {
return {
appliesTo: (identifier, domainObject) => {
return domainObject?.type === 'flexible-layout';
},
invoke: (identifier, domainObject) => {
if (!domainObject.configuration) {
domainObject.configuration = {};
}
if (!domainObject.configuration.objectStyles) {
domainObject.configuration.objectStyles = {};
}
return domainObject;
}
};
}

View File

@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import flexibleLayoutStylesInterceptor from './flexibleLayoutStylesInterceptor.js';
import FlexibleLayoutViewProvider from './flexibleLayoutViewProvider.js';
import ToolBarProvider from './toolbarProvider.js';
import Container from './utils/container.js';
@ -37,11 +38,13 @@ export default function plugin() {
initialize: function (domainObject) {
domainObject.configuration = {
containers: [new Container(50), new Container(50)],
rowsLayout: false
rowsLayout: false,
objectStyles: {}
};
domainObject.composition = [];
}
});
openmct.objects.addGetInterceptor(flexibleLayoutStylesInterceptor(openmct));
let toolbar = ToolBarProvider(openmct);

View File

@ -21,28 +21,10 @@
*****************************************************************************/
export default function GaugeCompositionPolicy(openmct) {
function hasNumericTelemetry(domainObject) {
const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject);
if (!hasTelemetry) {
return false;
}
const metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasDomainAndRange(metadata);
}
function hasDomainAndRange(metadata) {
return (
metadata.valuesForHints(['range']).length > 0 &&
metadata.valuesForHints(['domain']).length > 0
);
}
return {
allow: function (parent, child) {
if (parent.type === 'gauge') {
return hasNumericTelemetry(child);
return openmct.telemetry.hasNumericTelemetry(child);
}
return true;

View File

@ -24,6 +24,7 @@ import mount from 'utils/mount';
import GaugeFormController from './components/GaugeFormController.vue';
import GaugeCompositionPolicy from './GaugeCompositionPolicy.js';
import gaugeStylesInterceptor from './gaugeStylesInterceptor.js';
import GaugeViewProvider from './GaugeViewProvider.js';
export const GAUGE_TYPES = [
@ -37,7 +38,7 @@ export const GAUGE_TYPES = [
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new GaugeViewProvider(openmct));
openmct.objects.addGetInterceptor(gaugeStylesInterceptor(openmct));
openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct));
openmct.types.addType('gauge', {
name: 'Gauge',
@ -59,7 +60,8 @@ export default function () {
max: 100,
min: 0,
precision: 2
}
},
objectStyles: {}
};
},
form: [

View File

@ -649,6 +649,11 @@ export default {
},
request(domainObject = this.telemetryObject) {
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
if (!this.metadata) {
return;
}
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject);
LimitEvaluator.limits().then(this.updateLimits);

View File

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

View File

@ -30,17 +30,30 @@ export default function AnnotationsViewProvider(openmct) {
name: 'Annotations',
canView: function (selection) {
const availableTags = openmct.annotation.getAvailableTags();
const selectionContext = selection?.[0]?.[0]?.context;
const domainObject = selectionContext?.item;
const isLayoutItem = selectionContext?.layoutItem;
if (availableTags.length < 1) {
if (availableTags.length < 1 || isLayoutItem || !domainObject || openmct.editor.isEditing()) {
return false;
}
return selection.length;
const isAnnotatableType = openmct.annotation.isAnnotatableType(domainObject.type);
const metadata = openmct.telemetry.getMetadata(domainObject);
const hasImagery = metadata?.valuesForHints(['image']).length > 0;
const isNotebookEntry = selectionContext?.type === 'notebook-entry-selection';
const hasNumericTelemetry = openmct.telemetry.hasNumericTelemetry(domainObject);
return isAnnotatableType || hasImagery || hasNumericTelemetry || isNotebookEntry;
},
view: function (selection) {
let _destroy = null;
const domainObject = selection?.[0]?.[0]?.context?.item;
const selectionContext = selection?.[0]?.[0]?.context;
const isImageSelection = selectionContext?.type === 'clicked-on-image-selection';
const domainObject = selectionContext?.item;
const isNotebookEntry = selectionContext?.type === 'notebook-entry-selection';
const isConditionSet = domainObject?.type === 'conditionSet';
return {
show: function (element) {
@ -64,6 +77,14 @@ export default function AnnotationsViewProvider(openmct) {
_destroy = destroy;
},
priority: function () {
if (isNotebookEntry || isImageSelection) {
return openmct.priority.HIGHEST;
}
if (isConditionSet) {
return openmct.priority.LOW;
}
return openmct.priority.DEFAULT;
},
destroy: function () {

View File

@ -22,6 +22,7 @@
<template>
<li
v-if="allowDrag"
draggable="true"
:aria-label="`${elementObject.name} Element Item`"
:aria-grabbed="hover"
@ -47,6 +48,22 @@
/>
</div>
</li>
<li v-else :aria-label="`${elementObject.name} Element Item`">
<div
class="c-tree__item c-elements-pool__item js-elements-pool__item"
:class="{
'is-context-clicked': contextClickActive,
hover: hover,
'is-alias': isAlias
}"
>
<ObjectLabel
:domain-object="elementObject"
:object-path="[elementObject, domainObject]"
@context-click-active="setContextClickState"
/>
</div>
</li>
</template>
<script>
@ -74,6 +91,9 @@ export default {
},
allowDrop: {
type: Boolean
},
allowDrag: {
type: Boolean
}
},
emits: ['drop-custom', 'dragstart-custom'],

View File

@ -39,6 +39,7 @@
:key="element.identifier.key"
:index="index"
:element-object="element"
:allow-drag="isEditing"
:allow-drop="allowDrop"
@dragstart-custom="moveFrom(index)"
@drop-custom="moveTo(index)"

View File

@ -31,8 +31,9 @@ export default function ElementsViewProvider(openmct) {
canView: function (selection) {
const hasValidSelection = selection?.length;
const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay';
const isFolder = selection?.[0]?.[0]?.context?.item?.type === 'folder';
return hasValidSelection && !isOverlayPlot;
return hasValidSelection && !isOverlayPlot && !isFolder;
},
view: function (selection) {
let _destroy = null;
@ -62,10 +63,10 @@ export default function ElementsViewProvider(openmct) {
showTab: function (isEditing) {
const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject));
return hasComposition && isEditing;
return hasComposition;
},
priority: function () {
return openmct.priority.DEFAULT;
return openmct.editor.isEditing() ? openmct.priority.DEFAULT : openmct.priority.LOW;
},
destroy: function () {
if (_destroy) {

View File

@ -30,7 +30,9 @@ export default function PropertiesViewProvider(openmct) {
name: 'Properties',
glyph: 'icon-info',
canView: function (selection) {
return selection.length > 0;
const domainObject = selection?.[0]?.[0]?.context?.item;
return domainObject && selection.length > 0;
},
view: function (selection) {
let _destroy = null;
@ -56,7 +58,7 @@ export default function PropertiesViewProvider(openmct) {
_destroy = destroy;
},
priority: function () {
return openmct.priority.DEFAULT;
return openmct.editor.isEditing() ? openmct.priority.LOW : openmct.priority.HIGH;
},
destroy: function () {
if (_destroy) {

View File

@ -25,7 +25,24 @@ import mount from 'utils/mount';
import StylesInspectorView from './StylesInspectorView.vue';
import stylesManager from './StylesManager.js';
const NON_STYLABLE_TYPES = ['folder', 'webPage', 'conditionSet', 'summary-widget', 'hyperlink'];
const NON_STYLABLE_TYPES = [
'clock',
'conditionSet',
'eventGenerator',
'eventGeneratorWithAcknowledge',
'example.imagery',
'folder',
'gantt-chart',
'generator',
'hyperlink',
'notebook',
'restricted-notebook',
'summary-widget',
'time-strip',
'timelist',
'timer',
'webPage'
];
function isLayoutObject(selection, objectType) {
//we allow conditionSets to be styled if they're part of a layout
@ -35,8 +52,8 @@ function isLayoutObject(selection, objectType) {
);
}
function isCreatableObject(object, type) {
return NON_STYLABLE_TYPES.indexOf(object.type) < 0 && type.definition.creatable;
function isCreatableObject(object, typeObject) {
return NON_STYLABLE_TYPES.indexOf(object.type) < 0 && typeObject.definition.creatable;
}
export default function StylesInspectorViewProvider(openmct) {
@ -47,12 +64,13 @@ export default function StylesInspectorViewProvider(openmct) {
canView: function (selection) {
const objectSelection = selection?.[0];
const objectContext = objectSelection?.[0]?.context;
const layoutItem = objectContext?.layoutItem;
const domainObject = objectContext?.item;
const hasStyles = domainObject?.configuration?.objectStyles;
const isFlexibleLayoutContainer =
domainObject?.type === 'flexible-layout' && objectContext.type === 'container';
const isLayoutItem = objectContext?.layoutItem;
if (layoutItem) {
if ((isLayoutItem || hasStyles) && !isFlexibleLayoutContainer) {
return true;
}
@ -60,10 +78,11 @@ export default function StylesInspectorViewProvider(openmct) {
return false;
}
const type = openmct.types.get(domainObject.type);
const typeObject = openmct.types.get(domainObject.type);
return (
isLayoutObject(objectSelection, domainObject.type) || isCreatableObject(domainObject, type)
isLayoutObject(objectSelection, domainObject.type) ||
isCreatableObject(domainObject, typeObject)
);
},
view: function (selection) {
@ -91,8 +110,11 @@ export default function StylesInspectorViewProvider(openmct) {
);
_destroy = destroy;
},
showTab: function (isEditing) {
return true;
},
priority: function () {
return openmct.priority.DEFAULT;
return openmct.editor.isEditing() ? openmct.priority.DEFAULT : openmct.priority.LOW;
},
destroy: function () {
if (_destroy) {

View File

@ -38,6 +38,7 @@ export default function MissingObjectInterceptor(openmct) {
}
return object;
}
},
priority: openmct.priority.HIGH
});
}

View File

@ -45,7 +45,7 @@ function myItemsInterceptor({ openmct, identifierObject, name }) {
return object;
},
priority: openmct.priority.HIGH
priority: openmct.priority.HIGHEST
};
}

View File

@ -344,12 +344,19 @@ export default {
},
beforeMount() {
this.marked = new Marked();
this.renderer = new this.marked.Renderer();
this.marked.use({
breaks: true,
extensions: [
{
name: 'link',
renderer: (options) => {
return this.validateLink(options);
}
}
]
});
},
mounted() {
const originalLinkRenderer = this.renderer.link;
this.renderer.link = this.validateLink.bind(this, originalLinkRenderer);
this.manageEmbedLayout = _.debounce(this.manageEmbedLayout, 400);
if (this.$refs.embedsWrapper) {
@ -437,10 +444,7 @@ export default {
}
},
convertMarkDownToHtml(text = '') {
let markDownHtml = this.marked.parse(text, {
breaks: true,
renderer: this.renderer
});
let markDownHtml = this.marked.parse(text);
markDownHtml = sanitizeHtml(markDownHtml, SANITIZATION_SCHEMA);
return markDownHtml;
},
@ -451,21 +455,19 @@ export default {
this.$refs.entryInput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
},
validateLink(originalLinkRenderer, href, title, text) {
validateLink(options) {
const { href, text } = options;
try {
const domain = new URL(href).hostname;
const urlIsWhitelisted = this.urlWhitelist.some((partialDomain) => {
return domain.endsWith(partialDomain);
});
if (!urlIsWhitelisted) {
return text;
}
const linkHtml = originalLinkRenderer.call(this.renderer, href, title, text);
const linkHtmlWithTarget = linkHtml.replace(
/^<a /,
'<a class="c-hyperlink" target="_blank"'
);
return linkHtmlWithTarget;
return `<a class="c-hyperlink" target="_blank" href="${href}">${text}</a>`;
} catch (error) {
// had error parsing this URL, just return the text
return text;

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